Skip to content

Commit

Permalink
feat: fully functional New Relic sink (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
smoya authored Oct 30, 2023
2 parents aaba61c + c27d406 commit 77ad552
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 29 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ root = true
end_of_line = lf
insert_final_newline = true

[*.js]
[*.js,*.ts]
indent_size = 2
indent_style = space
quote_type = single
quote_type = single
82 changes: 82 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"eslint-plugin-security": "^1.5.0",
"eslint-plugin-sonarjs": "^0.15.0",
"jest": "^29.0.2",
"jest-fetch-mock": "^3.0.3",
"markdown-toc": "^1.2.0",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './recorder';
export * from './metrics';
export * from './sink';
export * from './sink';
export * from './sinks/newrelic';
26 changes: 0 additions & 26 deletions src/sink.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Metrics } from './metrics';

// Define the MetricSink interface
export interface Sink {
send(metrics: Metrics): Promise<void>;
}
Expand All @@ -12,28 +11,3 @@ export class StdOutSink implements Sink {
});
}
}

// Implement the MetricSink interface for the New Relic Metrics API
export class NewRelicSink implements Sink {
async send(metrics: Metrics): Promise<void> {
// TODO convert metrics to New Relic format
const convertedMetrics = metrics;

// Send the metric to the New Relic Metrics API
const response = await fetch('https://metric-api.newrelic.com/metric/v1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-License-Key': 'YOUR_LICENSE_KEY_HERE'
},
body: JSON.stringify({
metrics: convertedMetrics
})
});

// Check the response status and throw an error if it's not successful
if (!response.ok) {
throw new Error(`Failed to send metric to New Relic Metrics API: ${response.status} ${response.statusText}`);
}
}
}
61 changes: 61 additions & 0 deletions src/sinks/newrelic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Sink } from '../sink';
import { Metrics, MetricType } from '../metrics';

enum NRMetricType {
Count = 'count',
Distribution = 'distribution',
Gauge = 'gauge',
Summary = 'summary',
UniqueCount = 'uniqueCount',
}

class NRMetric {
'interval.ms': number;
constructor(
protected readonly name: string,
protected readonly type: NRMetricType,
protected readonly value: any = 1,
protected attributes = {},
protected timestamp = Date.now(),
) {
this['interval.ms'] = 1;
}
}

export class NewRelicSink implements Sink {
constructor(
protected readonly licenseKey: string,
protected readonly apiEndpoint = 'https://metric-api.eu.newrelic.com/metric/v1'
) {}

async send(metrics: Metrics): Promise<void> {
const nrMetrics = [];
for (const metric of metrics) {
switch (metric.type) {
case MetricType.Counter:
nrMetrics.push(new NRMetric(metric.name, NRMetricType.Count, metric.value, metric.metadata));
break;
case MetricType.Gauge:
nrMetrics.push(new NRMetric(metric.name, NRMetricType.Gauge, metric.value, metric.metadata));
break;
default:
console.log(`NewRelicSink does not have support for '${metric.type}' metrics yet`);
}
}

// Send the metric to the New Relic Metrics API
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-License-Key': this.licenseKey
},
body: JSON.stringify([{ metrics: nrMetrics }])
});

// Check the response status and throw an error if it's not successful
if (!response.ok) {
throw new Error(`Failed to send metrics to New Relic Metrics API: ${response.status} ${response.statusText}`);
}
}
}
45 changes: 45 additions & 0 deletions test/sinks/newrelic.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
import { NewRelicSink } from '../../src/sinks/newrelic';
import { Metric, MetricType } from '../../src/metrics';

describe('Recorder', function() {
it('Sends metrics to New Relic - Response OK', async function() {
enableFetchMocks();
jest.useFakeTimers();
jest.setSystemTime(new Date('2023-01-01')); // 1672531200000

fetchMock.mockResponseOnce(JSON.stringify({requestId: 'f0e7bfff-001a-b000-0000-01682bcf4565'}), {status: 202, headers: { 'Content-Type': 'application/json; charset=UTF-8' }});

const sink = new NewRelicSink('FAKE_LICENSE_KEY', 'https://newrelic.fake');
const metrics = [
new Metric('test_metric_one', MetricType.Counter, 1, { test: 'one' }),
new Metric('test_metric_two', MetricType.Counter, 5, { test: 'two' }),
new Metric('test_metric_three', MetricType.Gauge, 2876176, { test: 'three' }),
];

const expectedFetchCall = {
body: JSON.stringify([{metrics: [
{ name: 'test_metric_one', type: 'count', value: 1, attributes: {test: 'one'}, timestamp: 1672531200000, 'interval.ms': 1 },
{ name: 'test_metric_two', type: 'count', value: 5, attributes: {test: 'two'}, timestamp: 1672531200000, 'interval.ms': 1 },
{ name: 'test_metric_three', type: 'gauge', value: 2876176, attributes: {test: 'three'}, timestamp: 1672531200000, 'interval.ms': 1 }
]}]),
headers: {
'Content-Type': 'application/json',
'X-License-Key': 'FAKE_LICENSE_KEY'
},
method: 'POST'
};

await sink.send(metrics);
expect(fetchMock.mock.calls.length).toEqual(1);
expect(fetchMock.mock.calls[0][0]).toEqual('https://newrelic.fake');
expect(fetchMock.mock.calls[0][1]).toEqual(expectedFetchCall);
});

it('Sends metrics to New Relic - Response KO', async function() {
enableFetchMocks();
fetchMock.mockResponseOnce('', { status: 400 });
const sink = new NewRelicSink('', 'https://newrelic.fake');
expect(async () => {await sink.send([]);}).rejects.toThrowError('Failed to send metrics to New Relic Metrics API: 400 Bad Request');
});
});

0 comments on commit 77ad552

Please sign in to comment.