diff --git a/src/api/newrelic_event.js b/src/api/newrelic_event.js new file mode 100644 index 00000000..10d287f3 --- /dev/null +++ b/src/api/newrelic_event.js @@ -0,0 +1,79 @@ +const apiPostNewRelicEvent = { + schema: { + summary: 'Post events to New Relic.', + description: 'This endpoint posts events to the New Relic event API.', + body: { + type: 'object', + properties: { + eventType: { + type: 'string', + description: 'Event type. Can be a combination of alphanumeric characters, _ underscores, and : colons.', + example: 'relead-failed', + maxLength: 254, + }, + timestamp: { + type: 'number', + description: + "The event's start time in Unix time. Uses UTC time zone. This field also support seconds, microseconds, and nanoseconds. However, the data will be converted to milliseconds for storage and query. Events reported with a timestamp older than 48 hours ago or newer than 24 hours from the time they are reported are dropped by New Relic. If left empty Butler will use the current time as timestamp.", + example: 1642164296053, + }, + attributes: { + type: 'array', + description: 'Dimensions/attributs that will be associated with the event.', + items: { + type: 'object', + properties: { + name: { + type: 'string', + example: 'host.name', + maxLength: 254, + }, + value: { + type: 'string', + example: 'dev.server.com', + maxLength: 4096, + }, + }, + }, + }, + }, + required: ['eventType'], + }, + response: { + 202: { + description: 'Data accepted and sent to New Relic.', + type: 'object', + properties: { + newRelicResultCode: { type: 'number', example: '202' }, + newRelicResultText: { type: 'string', example: 'Data accepted.' }, + }, + }, + 400: { + description: 'Required parameter missing.', + type: 'object', + properties: { + statusCode: { type: 'number' }, + code: { type: 'string' }, + error: { type: 'string' }, + message: { type: 'string' }, + time: { type: 'string' }, + }, + }, + 500: { + description: 'Internal error.', + type: 'object', + properties: { + statusCode: { type: 'number' }, + code: { type: 'string' }, + error: { type: 'string' }, + message: { type: 'string' }, + time: { type: 'string' }, + }, + }, + }, + }, +}; + +module.exports = { + apiPostNewRelicEvent, +}; diff --git a/src/app.js b/src/app.js index c62718eb..be540986 100644 --- a/src/app.js +++ b/src/app.js @@ -184,6 +184,7 @@ async function build(opts = {}) { restServer.register(require('./routes/disk_utils'), { options: Object.assign({}, opts) }); restServer.register(require('./routes/key_value_store'), { options: Object.assign({}, opts) }); restServer.register(require('./routes/mqtt_publish_message'), { options: Object.assign({}, opts) }); + restServer.register(require('./routes/newrelic_event'), { options: Object.assign({}, opts) }); restServer.register(require('./routes/newrelic_metric'), { options: Object.assign({}, opts) }); restServer.register(require('./routes/scheduler'), { options: Object.assign({}, opts) }); restServer.register(require('./routes/sense_app'), { options: Object.assign({}, opts) }); diff --git a/src/butler.js b/src/butler.js index 3c59312e..72175151 100644 --- a/src/butler.js +++ b/src/butler.js @@ -28,7 +28,7 @@ const start = async () => { (err, address) => { if (err) { globals.logger.error(`MAIN: Background REST server could not listen on ${address}`); - globals.logger.error(`MAIN: ${err}`); + globals.logger.error(`MAIN: ${err.stack}`); restServer.log.error(err); process.exit(1); } diff --git a/src/routes/newrelic_event.js b/src/routes/newrelic_event.js new file mode 100644 index 00000000..390a757a --- /dev/null +++ b/src/routes/newrelic_event.js @@ -0,0 +1,122 @@ +const httpErrors = require('http-errors'); +const axios = require('axios'); + +// Load global variables and functions +const globals = require('../globals'); +const { logRESTCall } = require('../lib/log_rest_call'); +const { apiPostNewRelicEvent } = require('../api/newrelic_event'); + +// eslint-disable-next-line consistent-return +async function handlerPostNewRelicEvent(request, reply) { + try { + logRESTCall(request); + + let payload = []; + const attributes = {}; + const ts = new Date().getTime(); // Timestamp in millisec + + // TODO sanity check parameters in REST call + + // Add static fields to attributes + if (globals.config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static')) { + const staticAttributes = globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.attribute.static'); + + if (staticAttributes !== null && staticAttributes.length > 0) { + // eslint-disable-next-line no-restricted-syntax + for (const item of staticAttributes) { + attributes[item.name] = item.value; + } + } + } + + // Add attributes passed as parameters + if (request.body.attributes && request.body.attributes.length > 0) { + if (request.body.attributes !== null && typeof request.body.attributes === 'object') { + // eslint-disable-next-line no-restricted-syntax + for (const item of request.body.attributes) { + attributes[item.name] = item.value; + } + } + } + + const tsEvent = request.body.timestamp > 0 ? request.body.timestamp : ts; + + const event = { + timestamp: tsEvent, + eventType: request.body.eventType, + }; + + Object.assign(event, attributes); + + // Build final payload + payload = event; + + globals.logger.debug(`NEWRELIC EVENT: Payload: ${JSON.stringify(payload, null, 2)}`); + + // Preapare call to remote host + + // Build final URL + const remoteUrl = + globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.url').slice(-1) === '/' + ? globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.url') + : `${globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.url')}/`; + + const eventApiUrl = `${remoteUrl}v1/accounts/${globals.config.get('Butler.thirdPartyToolsCredentials.newRelic.accountId')}/events`; + + // Add headers + const headers = { + 'Content-Type': 'application/json; charset=utf-8', + 'Api-Key': globals.config.get('Butler.thirdPartyToolsCredentials.newRelic.insertApiKey'), + }; + + if (globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header') !== null) { + // eslint-disable-next-line no-restricted-syntax + for (const header of globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.header')) { + headers[header.name] = header.value; + } + } + + // Build body for HTTP POST + const axiosRequest = { + url: eventApiUrl, + method: 'post', + timeout: 5000, + data: event, + headers, + }; + + const res = await axios.request(axiosRequest); + globals.logger.debug(`NEWRELIC EVENT: Result code from posting event to New Relic: ${res.status}, ${res.statusText}`); + + if (res.status === 200) { + // Posting done without error + globals.logger.verbose(`NEWRELIC EVENT: Sent event to New Relic`); + reply.type('text/plain').code(202).send(res.statusText); + // reply.type('application/json; charset=utf-8').code(201).send(JSON.stringify(request.body)); + } else { + reply.send(httpErrors(res.status, `Failed posting event to New Relic: ${res.statusText}`)); + } + + // Required parameter is missing + } catch (err) { + globals.logger.error( + `NEWRELIC EVENT: Failed posting event to New Relic: ${JSON.stringify(request.body, null, 2)}, error is: ${JSON.stringify( + err, + null, + 2 + )}` + ); + reply.send(httpErrors(500, 'Failed posting event to New Relic')); + } +} + +// eslint-disable-next-line no-unused-vars +module.exports = async (fastify, options) => { + if ( + globals.config.has('Butler.restServerEndpointsEnable.newRelic.postNewRelicEvent') && + globals.config.get('Butler.restServerEndpointsEnable.newRelic.postNewRelicEvent') === true + ) { + globals.logger.debug('Registering REST endpoint POST /v4/newrelic/event'); + fastify.post('/v4/newrelic/event', apiPostNewRelicEvent, handlerPostNewRelicEvent); + } +}; diff --git a/src/routes/newrelic_metric.js b/src/routes/newrelic_metric.js index 7fa053f8..5ac311e5 100644 --- a/src/routes/newrelic_metric.js +++ b/src/routes/newrelic_metric.js @@ -22,9 +22,11 @@ async function handlerPostNewRelicMetric(request, reply) { if (globals.config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static')) { const staticAttributes = globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static'); - // eslint-disable-next-line no-restricted-syntax - for (const item of staticAttributes) { - attributes[item.name] = item.value; + if (staticAttributes !== null && staticAttributes.length > 0) { + // eslint-disable-next-line no-restricted-syntax + for (const item of staticAttributes) { + attributes[item.name] = item.value; + } } } @@ -62,7 +64,7 @@ async function handlerPostNewRelicMetric(request, reply) { globals.logger.debug(`NEWRELIC METRIC: Payload: ${JSON.stringify(payload, null, 2)}`); // Preapare call to remote host - const remoteUrl = globals.config.get('Butler.uptimeMonitor.storeNewRelic.url'); + const remoteUrl = globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.url'); // Add headers const headers = { @@ -70,9 +72,11 @@ async function handlerPostNewRelicMetric(request, reply) { 'Api-Key': globals.config.get('Butler.thirdPartyToolsCredentials.newRelic.insertApiKey'), }; - // eslint-disable-next-line no-restricted-syntax - for (const header of globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header')) { - headers[header.name] = header.value; + if (globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header') !== null) { + // eslint-disable-next-line no-restricted-syntax + for (const header of globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header')) { + headers[header.name] = header.value; + } } const res = await axios.post(remoteUrl, payload, { headers, timeout: 5000 });