Skip to content

Commit

Permalink
feat: Send New Relic events via Butler REST API
Browse files Browse the repository at this point in the history
Implements #441
  • Loading branch information
mountaindude committed May 20, 2022
1 parent 7ff2e94 commit 0da0d85
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 1 deletion.
79 changes: 79 additions & 0 deletions src/api/newrelic_event.js
Original file line number Diff line number Diff line change
@@ -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,
};
1 change: 1 addition & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) });
Expand Down
2 changes: 1 addition & 1 deletion src/butler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
122 changes: 122 additions & 0 deletions src/routes/newrelic_event.js
Original file line number Diff line number Diff line change
@@ -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);
}
};

0 comments on commit 0da0d85

Please sign in to comment.