Skip to content

Commit

Permalink
implement generateConfig and update applyConfig to use yaml PUT endpo…
Browse files Browse the repository at this point in the history
…int (#40)

* implement generateConfig and update applyConfig to use yaml PUT endpoint

* improved error messages and simplify the UX for calling apply/validateConfig

* bump to version 2.4.0
  • Loading branch information
aflanagan authored May 7, 2024
1 parent 3587f62 commit 57d93cf
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 119 deletions.
114 changes: 59 additions & 55 deletions lib/cronitor.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
const axios = require('axios');
const yaml = require('js-yaml');
const fs = require('fs');
const fs = require('fs').promises;
const path = require('path');

const Monitor = require('./monitor');
const Event = require('./event');
const Errors = require('./errors');

const CONFIG_KEYS = ['api_key', 'api_version', 'environment'];
const MONITOR_TYPES = ['job', 'heartbeat', 'check'];
const YAML_KEYS = CONFIG_KEYS + MONITOR_TYPES.map(t => `${t}s`);
const YAML_KEYS = MONITOR_TYPES.map(t => `${t}s`);

function Cronitor(apiKey, config = {}) {
if (!(this instanceof Cronitor)) return new Cronitor(apiKey, config);
apiKey = apiKey || process.env.CRONITOR_API_KEY;

const path = config.config || process.env.CRONITOR_CONFIG;
if (path) this.config = this.readConfig({ path });

const version = config.apiVersion || process.env.CRONITOR_API_VERSION || null;

const timeout = config.timeout || process.env.CRONITOR_TIMEOUT || 10000;

const env = config.env || process.env.CRONITOR_ENV || 'production';
const headers = {
'User-Agent': 'cronitor-js',
'Authorization': 'Basic ' + new Buffer.from(apiKey + ':').toString('base64'),
};

if (path) {
this.config = this.readConfig({ path });
this.path = path;
}

if (version) headers['Cronitor-Version'] = version;

this._api = {
key: apiKey,
version,
env: config.environment || process.env.CRONITOR_ENVIRONMENT || null,
env: env,
pingUrl: (key) => `https://cronitor.link/ping/${apiKey}/${key}`,
monitorUrl: (key) => `https://cronitor.io/api/monitors${key ? '/' + key : ''}`,
axios: axios.create({
Expand All @@ -45,70 +49,69 @@ function Cronitor(apiKey, config = {}) {
this.Monitor._api = this._api;
this.Event._api = this._api;

this.generateConfig = async () => {
throw new Error('generateConfig not implemented. Contact [email protected] and we will help.');
this.generateConfig = async ({path=null, group=null}={}) => {
let url = 'https://cronitor.io/api/monitors.yaml';

if (!this.path && !path) {
throw new Errors.ConfigError('Must initialize Cronitor with a "config" keyword arg as a valid file path or pass `path` as a keyword arg to generateConfig.');
} else if (!path) {
path = this.path;
}

if (group) { url += `?group=${group}` }

try {
// Make HTTP GET request to fetch the YAML file
const response = await this._api.axios.get(url, { responseType: 'blob' });
await fs.writeFile(path, response.data, 'utf8');
return true
} catch (error) {
console.error('Failed to download or save the YAML file:', error);
return false
}
};


this.applyConfig = async function(rollback = false) {
if (!this.config.monitors) throw new Errors.ConfigError('Must call cronitor.readConfig(\'path/to/config\') before calling applyConfig().');
this.applyConfig = async function({ path = this.path, rollback = false } = {}) {
if (!path) throw new Errors.ConfigError('Must include a path to config file e.g. cronitor.applyConfig({path: \'./cronitor.yaml\'})');

try {
config = await this.readConfig({ path, output: true});
} catch (err) {
console.error('Error reading config:', err);
return false
}

try {
return await Monitor.put(this.config.monitors, rollback);
await Monitor.put(config, {rollback, format: Monitor.requestType.YAML});
console.log(`Cronitor config ${rollback ? 'validated' : 'applied'} successfully.`)
return true
} catch (err) {
console.log(`Error applying config: ${err}`);
console.error(`Error applying config: ${err}`);
return false
}
};

this.validateConfig = async () => {
return this.applyConfig(true);
this.validateConfig = async ({ path = this.path} = {}) => {
return this.applyConfig({ path, rollback: true });
};

this.readConfig = function(pathCfg = null, output = false) {
if (!pathCfg) throw new Errors.ConfigError('Must include a path to config file e.g. cronitor.readConfig(\'./cronitor.yaml\')');
this.readConfig = async function({path = null, output = false}={}) {
if (!path) throw new Errors.ConfigError('Must include a path to config file e.g. cronitor.readConfig({path: \'./cronitor.yaml\'})');
if (!this.path) this.path = path;

try {
const doc = yaml.load(fs.readFileSync(pathCfg, 'utf8'));
this.config = this._parseConfig(doc);
if (this.config.apiKey) this._api.key = this.config.apiKey;
if (this.config.apiVersion) this._api.version = this.config.apiVersion;
if (this.config.environment) this._api.env = this.config.environment;
let configFile = await fs.readFile(path, 'utf8')
this.config = yaml.load(configFile);
if (output) return this.config;
return true
} catch (err) {
console.log(err);
console.error('Error reading Cronitor config file:', err);
return false
}
};

this._parseConfig = function(data) {
Object.keys(data).forEach((k) => {
if (!YAML_KEYS.includes(k)) throw new Errors.ConfigError(`Invalid configuration variable ${k}`);
});

const monitors = [];
MONITOR_TYPES.forEach((t) => {
let toParse = null;
const pluralT = `${t}s`;

if (data[t]) {
toParse = data[t];
} else if (data[pluralT]) {
toParse = data[pluralT];
}

if (toParse) {
if (typeof toParse != 'object') throw new Errors.ConfigError('An object with keys corresponding to monitor keys is expected.');

Object.keys(toParse).forEach(k => {
toParse[k].key = k;
toParse[k].type = t;
monitors.push(toParse[k]);
});
}
});

data.monitors = monitors;
return data;
};


this.wrap = function(key, callback) {
const _cronitor = this;
return async function(args) {
Expand Down Expand Up @@ -144,6 +147,7 @@ function Cronitor(apiKey, config = {}) {
job.start();
};
} else {
console.log(lib.CronJob)
throw new Errors.ConfigError(`Unsupported library ${lib.name}`);
}

Expand Down
47 changes: 36 additions & 11 deletions lib/monitor.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const qs = require('qs');
const Errors = require('./errors');
const yaml = require('js-yaml');

class Monitor {
static get State() {
Expand All @@ -11,26 +12,50 @@ class Monitor {
};
}

static async put(data, rollback = false) {
if (typeof data == 'object') {
if (!Array.isArray(data)) {
data = [data];
}
} else {
throw new Errors.MonitorNotCreated('Invalid monitor data.');
}
static get requestType() {
return {
JSON: 'json',
YAML: 'yaml',
};
}

try {
const resp = await this._api.axios.put(this._api.monitorUrl(), { monitors: data, rollback });
static async put(data, {rollback = false, format = Monitor.requestType.JSON} = {}) {

if (format === Monitor.requestType.YAML) {
return this.putYaml(yaml.dump({...data, rollback}))

}

// if a user passed a single monitor object, wrap it in an array
if (!Array.isArray(data)) {
data = [data];
}

try {
const resp = await this._api.axios.put(this._api.monitorUrl(), {monitors: data, rollback});
const monitors = resp.data.monitors.map((_m) => {
const m = new Monitor(_m.key);
m.data = _m;
return m;
});
return monitors.length > 1 ? monitors : monitors[0];
return monitors.length > 1 ? monitors : monitors[0];
} catch (err) {
throw new Errors.MonitorNotCreated(err.message);
}
}

static async putYaml(payload) {
try {
const resp = await this._api.axios.put(
this._api.monitorUrl(),
payload,
{ headers: {'Content-Type': 'application/yaml'} }
);
return yaml.load(resp.data);
} catch (err) {
throw new Errors.MonitorNotCreated(err.message);
}

}

constructor(key) {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "cronitor",
"version": "2.3.6",
"description": "A small library for reliably monitoring cron jobs, control-loops, or other important system events with Cronitor.",
"version": "2.4.0",
"description": "A simple library for reliably monitoring cron jobs, control-loops, or other important system events with Cronitor.",
"main": "lib/cronitor.js",
"repository": {
"type": "git",
Expand Down
Loading

0 comments on commit 57d93cf

Please sign in to comment.