Skip to content

Commit

Permalink
feat: metric prefixes
Browse files Browse the repository at this point in the history
  • Loading branch information
mertalev committed Mar 25, 2024
1 parent 282c478 commit 44ab6ed
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 24 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const OpenTelemetryModuleConfig = OpenTelemetryModule.forRoot({
},
ignoreRoutes: ['/favicon.ico'], // You can ignore specific routes (See https://docs.nestjs.com/middleware#excluding-routes for options)
ignoreUndefinedRoutes: false, //Records metrics for all URLs, even undefined ones
prefix: 'my_prefix', // Add a custom prefix to all API metrics
},
},
});
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/opentelemetry-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ export type OpenTelemetryMetrics = {
defaultAttributes?: MetricAttributes,
ignoreRoutes?: (string | RouteInfo)[],
ignoreUndefinedRoutes?: boolean,
prefix?: string,
},
};
54 changes: 30 additions & 24 deletions src/metrics/metric-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,87 +28,93 @@ export function getOrCreateHistogram(
name: string,
options: MetricOptions = {},
): Histogram {
if (meterData.has(name)) {
return meterData.get(name) as Histogram;
const nameWithPrefix = options.prefix ? `${options.prefix}.${name}` : name;
if (meterData.has(nameWithPrefix)) {
return meterData.get(nameWithPrefix) as Histogram;
}

const meter = metrics.getMeterProvider().getMeter(OTEL_METER_NAME);
const histogram = meter.createHistogram(name, options);
meterData.set(name, histogram);
const histogram = meter.createHistogram(nameWithPrefix, options);
meterData.set(nameWithPrefix, histogram);
return histogram;
}

export function getOrCreateCounter(
name: string,
options: MetricOptions = {},
): Counter {
if (meterData.has(name)) {
return meterData.get(name) as Counter;
const nameWithPrefix = options.prefix ? `${options.prefix}.${name}` : name;
if (meterData.has(nameWithPrefix)) {
return meterData.get(nameWithPrefix) as Counter;
}

const meter = metrics.getMeterProvider().getMeter(OTEL_METER_NAME);

const counter = meter.createCounter(name, options);
meterData.set(name, counter);
const counter = meter.createCounter(nameWithPrefix, options);
meterData.set(nameWithPrefix, counter);
return counter;
}

export function getOrCreateUpDownCounter(
name: string,
options: MetricOptions = {},
): UpDownCounter {
if (meterData.has(name)) {
return meterData.get(name) as UpDownCounter;
const nameWithPrefix = options.prefix ? `${options.prefix}.${name}` : name;
if (meterData.has(nameWithPrefix)) {
return meterData.get(nameWithPrefix) as UpDownCounter;
}

const meter = metrics.getMeterProvider().getMeter(OTEL_METER_NAME);

const upDownCounter = meter.createUpDownCounter(name, options);
meterData.set(name, upDownCounter);
const upDownCounter = meter.createUpDownCounter(nameWithPrefix, options);
meterData.set(nameWithPrefix, upDownCounter);
return upDownCounter;
}

export function getOrCreateObservableGauge(
name: string,
options: MetricOptions = {},
): ObservableGauge {
if (meterData.has(name)) {
return meterData.get(name) as ObservableGauge;
const nameWithPrefix = options.prefix ? `${options.prefix}.${name}` : name;
if (meterData.has(nameWithPrefix)) {
return meterData.get(nameWithPrefix) as ObservableGauge;
}

const meter = metrics.getMeterProvider().getMeter(OTEL_METER_NAME);

const observableGauge = meter.createObservableGauge(name, options);
meterData.set(name, observableGauge);
const observableGauge = meter.createObservableGauge(nameWithPrefix, options);
meterData.set(nameWithPrefix, observableGauge);
return observableGauge;
}

export function getOrCreateObservableCounter(
name: string,
options: MetricOptions = {},
): ObservableCounter {
if (meterData.has(name)) {
return meterData.get(name) as ObservableCounter;
const nameWithPrefix = options.prefix ? `${options.prefix}.${name}` : name;
if (meterData.has(nameWithPrefix)) {
return meterData.get(nameWithPrefix) as ObservableCounter;
}

const meter = metrics.getMeterProvider().getMeter(OTEL_METER_NAME);

const observableCounter = meter.createObservableCounter(name, options);
meterData.set(name, observableCounter);
const observableCounter = meter.createObservableCounter(nameWithPrefix, options);
meterData.set(nameWithPrefix, observableCounter);
return observableCounter;
}

export function getOrCreateObservableUpDownCounter(
name: string,
options: MetricOptions = {},
): ObservableUpDownCounter {
if (meterData.has(name)) {
return meterData.get(name) as ObservableUpDownCounter;
const nameWithPrefix = options.prefix ? `${options.prefix}.${name}` : name;
if (meterData.has(nameWithPrefix)) {
return meterData.get(nameWithPrefix) as ObservableUpDownCounter;
}

const meter = metrics.getMeterProvider().getMeter(OTEL_METER_NAME);

const observableCounter = meter.createObservableCounter(name, options);
meterData.set(name, observableCounter);
const observableCounter = meter.createObservableCounter(nameWithPrefix, options);
meterData.set(nameWithPrefix, observableCounter);
return observableCounter;
}
10 changes: 10 additions & 0 deletions src/middleware/api-metrics.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class ApiMetricsMiddleware implements NestMiddleware {
const {
defaultAttributes = {},
ignoreUndefinedRoutes = false,
prefix,
} = options?.metrics?.apiMetrics ?? {};

this.defaultMetricAttributes = defaultAttributes;
Expand All @@ -48,45 +49,54 @@ export class ApiMetricsMiddleware implements NestMiddleware {
this.httpServerRequestCount = this.metricService.getCounter('http.server.request.count', {
description: 'Total number of HTTP requests',
unit: 'requests',
prefix,
});

this.httpServerResponseCount = this.metricService.getCounter('http.server.response.count', {
description: 'Total number of HTTP responses',
unit: 'responses',
prefix,
});

this.httpServerAbortCount = this.metricService.getCounter('http.server.abort.count', {
description: 'Total number of data transfers aborted',
unit: 'requests',
prefix,
});

this.httpServerDuration = this.metricService.getHistogram('http.server.duration', {
description: 'The duration of the inbound HTTP request',
unit: 'ms',
prefix,
});

this.httpServerRequestSize = this.metricService.getHistogram('http.server.request.size', {
description: 'Size of incoming bytes',
unit: 'By',
prefix,
});

this.httpServerResponseSize = this.metricService.getHistogram('http.server.response.size', {
description: 'Size of outgoing bytes',
unit: 'By',
prefix,
});

// Helpers
this.httpServerResponseSuccessCount = this.metricService.getCounter('http.server.response.success.count', {
description: 'Total number of all successful responses',
unit: 'responses',
prefix,
});

this.httpServerResponseErrorCount = this.metricService.getCounter('http.server.response.error.count', {
description: 'Total number of all response errors',
prefix,
});

this.httpClientRequestErrorCount = this.metricService.getCounter('http.client.request.error.count', {
description: 'Total number of client error requests',
prefix,
});
}

Expand Down
77 changes: 77 additions & 0 deletions tests/e2e/metrics/metric.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,32 @@ describe('MetricService', () => {
// eslint-disable-next-line no-underscore-dangle
expect(existingCounter._descriptor.description).toBe('test1 description');
});

it('uses prefix when provided', async () => {
const moduleRef = await Test.createTestingModule({
imports: [OpenTelemetryModule.forRoot({
metrics: {
apiMetrics: {
enable: false,
},
},
})],
}).compile();

app = moduleRef.createNestApplication();
await app.init();

metricService = moduleRef.get<MetricService>(MetricService);
// Starts empty
expect(meterData.size).toBe(0);

const counter = metricService.getCounter('test1', { prefix: 'test_prefix' });
counter.add(1);

// Has new key record
const data = meterData;
expect(data.has('test_prefix.test1')).toBeTruthy();
});
});

describe('getUpDownCounter', () => {
Expand Down Expand Up @@ -169,6 +195,32 @@ describe('MetricService', () => {
// eslint-disable-next-line no-underscore-dangle
expect(existingCounter._descriptor.description).toBe('test1 description');
});

it('uses prefix when provided', async () => {
const moduleRef = await Test.createTestingModule({
imports: [OpenTelemetryModule.forRoot({
metrics: {
apiMetrics: {
enable: false,
},
},
})],
}).compile();

app = moduleRef.createNestApplication();
await app.init();

metricService = moduleRef.get<MetricService>(MetricService);
// Starts empty
expect(meterData.size).toBe(0);

const counter = metricService.getUpDownCounter('test1', { prefix: 'test_prefix' });
counter.add(1);

// Has new key record
const data = meterData;
expect(data.has('test_prefix.test1')).toBeTruthy();
});
});

describe('getHistogram', () => {
Expand Down Expand Up @@ -223,5 +275,30 @@ describe('MetricService', () => {
// eslint-disable-next-line no-underscore-dangle
expect(existingCounter._descriptor.description).toBe('test1 description');
});

it('uses prefix when provided', async () => {
const moduleRef = await Test.createTestingModule({
imports: [OpenTelemetryModule.forRoot({
metrics: {
apiMetrics: {
enable: false,
},
},
})],
}).compile();

app = moduleRef.createNestApplication();
await app.init();

metricService = moduleRef.get<MetricService>(MetricService);
// Starts empty
expect(meterData.size).toBe(0);

metricService.getHistogram('test1', { prefix: 'test_prefix' });

// Has new key record
const data = meterData;
expect(data.has('test_prefix.test1')).toBeTruthy();
});
});
});
54 changes: 54 additions & 0 deletions tests/e2e/middleware/api-metrics.middleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,60 @@ describe('Api Metrics Middleware', () => {
expect(metricService.getHistogram).toHaveBeenCalledWith('http.server.duration', { description: 'The duration of the inbound HTTP request', unit: 'ms' });
});

it('uses prefix when provided', async () => {
const testingModule = await Test.createTestingModule({
imports: [OpenTelemetryModule.forRoot({
metrics: {
apiMetrics: {
enable: true,
prefix: 'test_prefix',
},
},
})],
}).overrideProvider(MetricService)
.useValue(metricService)
.compile();

app = testingModule.createNestApplication();
await app.init();

expect(metricService.getCounter).toHaveBeenCalledWith('http.server.request.count', expect.objectContaining({
prefix: 'test_prefix',
}));

expect(metricService.getCounter).toHaveBeenCalledWith('http.server.response.count', expect.objectContaining({
prefix: 'test_prefix',
}));

expect(metricService.getCounter).toHaveBeenCalledWith('http.server.abort.count', expect.objectContaining({
prefix: 'test_prefix',
}));

expect(metricService.getHistogram).toHaveBeenCalledWith('http.server.duration', expect.objectContaining({
prefix: 'test_prefix',
}));

expect(metricService.getHistogram).toHaveBeenCalledWith('http.server.request.size', expect.objectContaining({
prefix: 'test_prefix',
}));

expect(metricService.getHistogram).toHaveBeenCalledWith('http.server.response.size', expect.objectContaining({
prefix: 'test_prefix',
}));

expect(metricService.getCounter).toHaveBeenCalledWith('http.server.response.success.count', expect.objectContaining({
prefix: 'test_prefix',
}));

expect(metricService.getCounter).toHaveBeenCalledWith('http.server.response.error.count', expect.objectContaining({
prefix: 'test_prefix',
}));

expect(metricService.getCounter).toHaveBeenCalledWith('http.client.request.error.count', expect.objectContaining({
prefix: 'test_prefix',
}));
});

describe('metric: http.server.request.count', () => {
it('succesfully request records', async () => {
const testingModule = await Test.createTestingModule({
Expand Down

0 comments on commit 44ab6ed

Please sign in to comment.