Skip to content

Commit

Permalink
feat: Send New Relic metrics, events and logs to zero, one or more Ne…
Browse files Browse the repository at this point in the history
…w Relic accounts

Implements #489
  • Loading branch information
mountaindude committed Jun 17, 2022
1 parent 88b5bbf commit d3b8968
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 81 deletions.
42 changes: 30 additions & 12 deletions src/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ program
)
.option('-c, --configfile <file>', 'path to config file')
.addOption(new Option('-l, --loglevel <level>', 'log level').choices(['error', 'warn', 'info', 'verbose', 'debug', 'silly']))
.option('--new-relic-api-key <key>', 'insert API key to use with New Relic')
.option('--new-relic-account-id <id>', 'New Relic account ID')
.option(
'--new-relic-account-name <name...>',
'New Relic account name. Used within Butler to differentiate between different target New Relic accounts'
)
.option('--new-relic-api-key <key...>', 'insert API key to use with New Relic')
.option('--new-relic-account-id <id...>', 'New Relic account ID')
.option('--test-email-address <address>', 'send test email to this address. Used to verify email settings in the config file.')
.option(
'--test-email-from-address <address>',
Expand Down Expand Up @@ -94,16 +98,6 @@ if (options.loglevel && options.loglevel.length > 0) {
config.Butler.logLevel = options.loglevel;
}

// Is there a New Relic API key specified on the command line?
if (options.newRelicApiKey && options.newRelicApiKey.length > 0) {
config.Butler.thirdPartyToolsCredentials.newRelic.insertApiKey = options.newRelicApiKey;
}

// Is there a New Relic account ID specified on the command line?
if (options.newRelicAccountId && options.newRelicAccountId.length > 0) {
config.Butler.thirdPartyToolsCredentials.newRelic.accountId = options.newRelicAccountId;
}

// Set up logger with timestamps and colors, and optional logging to disk file
const logTransports = [];

Expand Down Expand Up @@ -146,6 +140,30 @@ const logger = winston.createLogger({
// Function to get current logging level
const getLoggingLevel = () => logTransports.find((transport) => transport.name === 'console').level;

// Are there New Relic account name(s), API key(s) and account ID(s) specified on the command line?
// There must be the same number of each specified!
// If so, replace any info from the config file with data from command line options
if (
options?.newRelicAccountName?.length > 0 &&
options?.newRelicApiKey?.length > 0 &&
options?.newRelicAccountId?.length > 0 &&
options?.newRelicAccountName?.length === options?.newRelicApiKey?.length &&
options?.newRelicApiKey?.length === options?.newRelicAccountId?.length
) {
config.Butler.thirdPartyToolsCredentials.newRelic = [];

for (let index = 0; index < options.newRelicApiKey.length; index++) {
const accountName = options.newRelicAccountName[index];
const accountId = options.newRelicAccountId[index];
const insertApiKey = options.newRelicApiKey[index];

config.Butler.thirdPartyToolsCredentials.newRelic.push({ accountName, accountId, insertApiKey });
}
} else if (options?.newRelicAccountName?.length > 0 || options?.newRelicApiKey?.length > 0 || options?.newRelicAccountId?.length > 0) {
logger.error('Incorrect command line parameters: Number of New Relic account names/IDs/API keys must match.');
process.exit(1);
}

// Are we running as standalone app or not?
logger.verbose(`Running as standalone app: ${isPkg}`);

Expand Down
120 changes: 91 additions & 29 deletions src/lib/incident_mgmt/new_relic.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ function getReloadFailedEventConfig() {
// 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.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header') !== null) {
Expand Down Expand Up @@ -137,7 +136,6 @@ function getReloadFailedLogConfig() {
// 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.incidentTool.newRelic.reloadTaskFailure.sharedSettings.header') !== null) {
Expand Down Expand Up @@ -202,7 +200,6 @@ function getReloadAbortedEventConfig() {
// 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.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header') !== null) {
Expand Down Expand Up @@ -270,7 +267,6 @@ function getReloadAbortedLogConfig() {
// 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.incidentTool.newRelic.reloadTaskAborted.sharedSettings.header') !== null) {
Expand Down Expand Up @@ -321,11 +317,6 @@ function getReloadAbortedLogConfig() {

async function sendNewRelicEvent(incidentConfig, reloadParams) {
try {
// Build final URL
const eventApiUrl = `${incidentConfig.url}v1/accounts/${globals.config.get(
'Butler.thirdPartyToolsCredentials.newRelic.accountId'
)}/events`;

// Build final payload
const payload = Object.assign(incidentConfig.attributes, reloadParams);
payload.eventType = incidentConfig.eventType;
Expand All @@ -347,17 +338,54 @@ async function sendNewRelicEvent(incidentConfig, reloadParams) {
// Remove log timestamp field from payload as it is no longer needed
delete payload.logTimeStamp;

// Build body for HTTP POST
const axiosRequest = {
url: eventApiUrl,
method: 'post',
timeout: 10000,
data: payload,
headers: incidentConfig.headers,
};
const { headers } = incidentConfig;

//
// Send data to all New Relic accounts that are enabled for this metric/event
//
// Get New Relic accounts
const nrAccounts = globals.config.Butler.thirdPartyToolsCredentials.newRelic;

// eslint-disable-next-line no-restricted-syntax
for (const accountName of globals.config.Butler.incidentTool.newRelic.destinationAccount.event) {
globals.logger.debug(`NEWRELIC EVENT: Current loop New Relic config=${JSON.stringify(accountName)}`);

// Is there any config available for the current account?
const newRelicConfig = nrAccounts.filter((item) => item.accountName === accountName);
if (newRelicConfig.length === 0) {
globals.logger.error(`NEWRELIC EVENT: New Relic config "${accountName}" does not exist in the Butler config file.`);
} else {
headers['Api-Key'] = newRelicConfig[0].insertApiKey;
const newRelicAccountId = newRelicConfig[0].accountId;

const eventApiUrl = `${incidentConfig.url}v1/accounts/${newRelicAccountId}/events`;

// Build body for HTTP POST
const axiosRequest = {
url: eventApiUrl,
method: 'post',
timeout: 10000,
data: payload,
headers,
};

// eslint-disable-next-line no-await-in-loop
const res = await axios.request(axiosRequest);
globals.logger.debug(
`NEWRELIC EVENT: Result code from posting event to New Relic account ${newRelicConfig[0].accountId}: ${res.status}, ${res.statusText}`
);

const response = await axios.request(axiosRequest);
globals.logger.debug(`NEWRELIC: Response from API: ${response.status}, ${response.statusText}`);
if (res.status === 200 || res.status === 202) {
// Posting done without error
globals.logger.verbose(`NEWRELIC EVENT: Sent event New Relic account ${newRelicConfig[0].accountId}`);
// reply.type('application/json; charset=utf-8').code(201).send(JSON.stringify(request.body));
} else {
globals.logger.error(
`NEWRELIC EVENT: Error code from posting event to New Relic account ${newRelicConfig[0].accountId}: ${res.status}, ${res.statusText}`
);
}
}
}
} catch (err) {
globals.logger.error(`NEWRELIC: ${JSON.stringify(err, null, 2)}`);
}
Expand Down Expand Up @@ -417,17 +445,51 @@ async function sendNewRelicLog(incidentConfig, reloadParams) {
// Remove log timestamp field from payload as it is no longer needed
delete payload.logTimeStamp;

// Build body for HTTP POST
const axiosRequest = {
url: logApiUrl,
method: 'post',
timeout: 10000,
data: payload,
headers: incidentConfig.headers,
};
const { headers } = incidentConfig;

//
// Send data to all New Relic accounts that are enabled for this metric/event
//
// Get New Relic accounts
const nrAccounts = globals.config.Butler.thirdPartyToolsCredentials.newRelic;

// eslint-disable-next-line no-restricted-syntax
for (const accountName of globals.config.Butler.incidentTool.newRelic.destinationAccount.log) {
globals.logger.debug(`NEWRELIC LOG: Current loop New Relic config=${JSON.stringify(accountName)}`);

// Is there any config available for the current account?
const newRelicConfig = nrAccounts.filter((item) => item.accountName === accountName);
if (newRelicConfig.length === 0) {
globals.logger.error(`NEWRELIC LOG: New Relic config "${accountName}" does not exist in the Butler config file.`);
} else {
headers['Api-Key'] = newRelicConfig[0].insertApiKey;

// Build body for HTTP POST
const axiosRequest = {
url: logApiUrl,
method: 'post',
timeout: 10000,
data: payload,
headers: incidentConfig.headers,
};

// eslint-disable-next-line no-await-in-loop
const res = await axios.request(axiosRequest);
globals.logger.debug(
`NEWRELIC LOG: Result code from posting log to New Relic account ${newRelicConfig[0].accountId}: ${res.status}, ${res.statusText}`
);

const response = await axios.request(axiosRequest);
globals.logger.debug(`NEWRELIC: Response from API: ${response.status}, ${response.statusText}`);
if (res.status === 200 || res.status === 202) {
// Posting done without error
globals.logger.verbose(`NEWRELIC LOG: Sent log New Relic account ${newRelicConfig[0].accountId}`);
// reply.type('application/json; charset=utf-8').code(201).send(JSON.stringify(request.body));
} else {
globals.logger.error(
`NEWRELIC LOG: Error code from posting log to New Relic account ${newRelicConfig[0].accountId}: ${res.status}, ${res.statusText}`
);
}
}
}
} catch (err) {
globals.logger.error(`NEWRELIC: ${JSON.stringify(err, null, 2)}`);
}
Expand Down
34 changes: 30 additions & 4 deletions src/lib/post_to_new_relic.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,43 @@ async function postButlerUptimeToNewRelic(fields) {
// Add headers
const headers = {
'Content-Type': 'application/json; charset=utf-8',
'Api-Key': globals.config.get('Butler.thirdPartyToolsCredentials.newRelic.insertApiKey'),
};

// eslint-disable-next-line no-restricted-syntax
for (const header of globals.config.get('Butler.uptimeMonitor.storeNewRelic.header')) {
headers[header.name] = header.value;
}

const res = await axios.post(remoteUrl, payload, { headers, timeout: 5000 });
globals.logger.debug(`UPTIME NEW RELIC: Result code from posting to New Relic: ${res.status}, ${res.statusText}`);
globals.logger.verbose(`UPTIME NEW RELIC: Sent Butler memory usage data to New Relic`);
//
// Send data to all New Relic accounts that are enabled for this metric/event
//
// Get New Relic accounts
const nrAccounts = globals.config.Butler.thirdPartyToolsCredentials.newRelic;
globals.logger.debug(`UPTIME NEW RELIC: Complete New Relic config=${JSON.stringify(nrAccounts)}`);

// eslint-disable-next-line no-restricted-syntax
for (const accountName of globals.config.Butler.uptimeMonitor.storeNewRelic.destinationAccount) {
globals.logger.debug(`UPTIME NEW RELIC: Current loop New Relic config=${JSON.stringify(accountName)}`);

// Is there any config available for the current account?
const newRelicConfig = nrAccounts.filter((item) => item.accountName === accountName);
globals.logger.debug(`UPTIME NEW RELIC: New Relic config=${JSON.stringify(newRelicConfig)}`);

if (newRelicConfig.length === 0) {
globals.logger.error(`UPTIME NEW RELIC: New Relic config "${accountName}" does not exist in the Butler config file.`);
} else {
headers['Api-Key'] = newRelicConfig[0].insertApiKey;

// eslint-disable-next-line no-await-in-loop
const res = await axios.post(remoteUrl, payload, { headers, timeout: 5000 });
globals.logger.debug(
`UPTIME NEW RELIC: Result code from posting to New Relic account ${newRelicConfig[0].accountId}: ${res.status}, ${res.statusText}`
);
globals.logger.verbose(
`UPTIME NEW RELIC: Sent Butler memory usage data to New Relic account ${newRelicConfig[0].accountId}`
);
}
}
} catch (error) {
// handle error
globals.logger.error(`UPTIME NEW RELIC: Error sending uptime: ${error}`);
Expand Down
68 changes: 44 additions & 24 deletions src/routes/newrelic_event.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,9 @@ async function handlerPostNewRelicEvent(request, reply) {
? 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) {
Expand All @@ -76,28 +73,51 @@ async function handlerPostNewRelicEvent(request, reply) {
}
}

// 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}`));
//
// Send data to all New Relic accounts that are enabled for this metric/event
//
// Get New Relic accounts
const nrAccounts = globals.config.Butler.thirdPartyToolsCredentials.newRelic;

// eslint-disable-next-line no-restricted-syntax
for (const accountName of globals.config.Butler.restServerEndpointsConfig.newRelic.postNewRelicEvent.destinationAccount) {
globals.logger.debug(`NEWRELIC EVENT: Current loop New Relic config=${JSON.stringify(accountName)}`);

// Is there any config available for the current account?
const newRelicConfig = nrAccounts.filter((item) => item.accountName === accountName);
if (newRelicConfig.length === 0) {
globals.logger.error(`NEWRELIC EVENT: New Relic config "${accountName}" does not exist in the Butler config file.`);
} else {
headers['Api-Key'] = newRelicConfig[0].insertApiKey;
const newRelicAccountId = newRelicConfig[0].accountId;

const eventApiUrl = `${remoteUrl}v1/accounts/${newRelicAccountId}/events`;

// Build body for HTTP POST
const axiosRequest = {
url: eventApiUrl,
method: 'post',
timeout: 5000,
data: event,
headers,
};

// eslint-disable-next-line no-await-in-loop
const res = await axios.request(axiosRequest);
globals.logger.debug(
`NEWRELIC EVENT: Result code from posting event to New Relic account ${newRelicConfig[0].accountId}: ${res.status}, ${res.statusText}`
);

if (res.status === 200 || res.status === 202) {
// Posting done without error
globals.logger.verbose(`NEWRELIC EVENT: Sent event New Relic account ${newRelicConfig[0].accountId}`);
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(
Expand Down
Loading

0 comments on commit d3b8968

Please sign in to comment.