Skip to content

Commit

Permalink
feat: update metric spec to the latest otel lib
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Anything related to metrics sdk has changed. OTEL now
has a stable spec and all the projects are moving towards new
interfaces.

Co-authored-by: Ivan Santos <pragmaticivan@gmail.com>
Co-authored-by: Adrian Busse <hello@adrianmxb.com>
  • Loading branch information
pragmaticivan and adrianmxb committed Dec 11, 2021
1 parent 48f724e commit 6ed63ae
Show file tree
Hide file tree
Showing 11 changed files with 1,405 additions and 1,130 deletions.
2,389 changes: 1,332 additions & 1,057 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions package.json
Expand Up @@ -37,9 +37,9 @@
},
"homepage": "https://github.com/pragmaticivan/nestjs-otel#readme",
"dependencies": {
"@opentelemetry/host-metrics": "^0.25.0",
"@opentelemetry/host-metrics": "^0.26.0",
"@opentelemetry/api": "^1.0.3",
"@opentelemetry/api-metrics": "^0.26.0",
"@opentelemetry/api-metrics": "^0.27.0",
"@opentelemetry/metrics": "^0.24.0",
"opentelemetry-node-metrics": "^1.0.3",
"response-time": "^2.3.2"
Expand All @@ -52,26 +52,26 @@
"@nestjs/platform-express": "^8.2.3",
"@nestjs/platform-fastify": "^8.2.3",
"@nestjs/testing": "^8.2.3",
"@opentelemetry/exporter-prometheus": "^0.26.0",
"@opentelemetry/sdk-node": "^0.26.0",
"@opentelemetry/exporter-prometheus": "^0.27.0",
"@opentelemetry/sdk-node": "^0.27.0",
"@types/jest": "^27.0.3",
"@types/node": "^16.11.10",
"@types/node": "^16.11.12",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "5.4.0",
"@typescript-eslint/parser": "5.4.0",
"eslint": "^8.3.0",
"@typescript-eslint/eslint-plugin": "5.6.0",
"@typescript-eslint/parser": "5.6.0",
"eslint": "^8.4.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^16.0.0",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-plugin-import": "^2.25.3",
"husky": "^7.0.4",
"jest": "^27.2.5",
"jest": "^27.4.4",
"lint-staged": "^12.1.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.4.0",
"supertest": "^6.1.6",
"ts-jest": "^27.0.7",
"typescript": "4.5.2",
"ts-jest": "^27.1.1",
"typescript": "4.5.3",
"ansi-regex": ""
},
"peerDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions src/interfaces/opentelemetry-options.interface.ts
@@ -1,12 +1,12 @@
import { ModuleMetadata, Type, Abstract } from '@nestjs/common';
import { Labels } from '@opentelemetry/api-metrics';
import { Attributes } from '@opentelemetry/api-metrics';
import { RouteInfo } from '@nestjs/common/interfaces';

export type OpenTelemetryModuleOptions = {
/**
* OpenTelemetry Metrics Setup
*/
metrics?: OpenTelemetryMetrics
metrics?: OpenTelemetryMetrics;
};

export interface OpenTelemetryOptionsFactory {
Expand Down Expand Up @@ -52,7 +52,7 @@ export type OpenTelemetryMetrics = {
timeBuckets?: number[],
requestSizeBuckets?: number[],
responseSizeBuckets?: number[],
defaultLabels?: Labels,
defaultAttributes?: Attributes,
ignoreRoutes?: (string | RouteInfo)[],
ignoreUndefinedRoutes?: boolean,
},
Expand Down
2 changes: 1 addition & 1 deletion src/metrics/decorators/common.ts
@@ -1,5 +1,5 @@
import { Counter, MetricOptions } from '@opentelemetry/api-metrics';
import { getOrCreateCounter, getOrCreateValueRecorder, MetricType } from '../metric-data';
import { getOrCreateCounter, MetricType } from '../metric-data';

/**
* Create and increment a counter when a new instance is created
Expand Down
2 changes: 1 addition & 1 deletion src/metrics/decorators/counter.ts
@@ -1,6 +1,6 @@
import { createParamDecorator } from '@nestjs/common';
import { MetricOptions } from '@opentelemetry/api-metrics';
import { getOrCreateCounter, getOrCreateValueRecorder, MetricType } from '../metric-data';
import { getOrCreateCounter, MetricType } from '../metric-data';

export const OtelCounter = createParamDecorator((name: string, options?: MetricOptions) => {
if (!name || name.length === 0) {
Expand Down
4 changes: 2 additions & 2 deletions src/metrics/decorators/value-recorder.ts
@@ -1,10 +1,10 @@
import { createParamDecorator } from '@nestjs/common';
import { MetricOptions } from '@opentelemetry/api-metrics';
import { getOrCreateValueRecorder, MetricType } from '../metric-data';
import { getOrCreateHistogram, MetricType } from '../metric-data';

export const OtelValueRecorder = createParamDecorator((name: string, options?: MetricOptions) => {
if (!name || name.length === 0) {
throw new Error('OtelValueRecorder need a name argument');
}
return getOrCreateValueRecorder(name, MetricType.ValueRecorder, options);
return getOrCreateHistogram(name, MetricType.Histogram, options);
});
20 changes: 10 additions & 10 deletions src/metrics/metric-data.ts
@@ -1,34 +1,34 @@
import {
Counter, MetricOptions, metrics, UpDownCounter, ValueRecorder,
Counter, MetricOptions, metrics, UpDownCounter, Histogram,
} from '@opentelemetry/api-metrics';
import { OTEL_METER_NAME } from '../opentelemetry.constants';

export type GenericMetric = Counter | UpDownCounter | ValueRecorder;
export type GenericMetric = Counter | UpDownCounter | Histogram;

export enum MetricType {
'Counter' = 'Counter',
'UpDownCounter' = 'UpDownCounter',
'ValueRecorder' = 'ValueRecorder',
'Histogram' = 'Histogram',
}

export const meterData: Map<string, GenericMetric> = new Map();

export function getOrCreateValueRecorder(
export function getOrCreateHistogram(
name: string,
type: MetricType,
options: MetricOptions,
): ValueRecorder {
): Histogram {
if (meterData.has(name)) {
return meterData.get(name) as ValueRecorder;
return meterData.get(name) as Histogram;
}

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

switch (type) {
case MetricType.ValueRecorder:
const valueRecorder = meter.createValueRecorder(name, options);
meterData.set(name, valueRecorder);
return valueRecorder;
case MetricType.Histogram:
const histogram = meter.createHistogram(name, options);
meterData.set(name, histogram);
return histogram;
default:
throw new Error(`Unknown type: ${type}`);
}
Expand Down
14 changes: 7 additions & 7 deletions src/metrics/metric.service.ts
@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import {
Counter, UpDownCounter, ValueRecorder, MetricOptions,
Counter, UpDownCounter, Histogram, MetricOptions,
} from '@opentelemetry/api-metrics';
import {
getOrCreateCounter, getOrCreateValueRecorder, MetricType,
getOrCreateCounter, getOrCreateHistogram, MetricType,
} from './metric-data';

@Injectable()
Expand All @@ -16,16 +16,16 @@ export class MetricService {
return this.getOrCreateCounter(name, MetricType.UpDownCounter, options);
}

getValueRecorder(name: string, options?: MetricOptions) {
return this.getOrCreateValueRecorder(name, MetricType.ValueRecorder, options);
getHistogram(name: string, options?: MetricOptions) {
return this.getOrCreateHistogram(name, MetricType.Histogram, options);
}

private getOrCreateValueRecorder(
private getOrCreateHistogram(
name: string,
type: MetricType,
options: MetricOptions,
): ValueRecorder {
return getOrCreateValueRecorder(name, type, options);
): Histogram {
return getOrCreateHistogram(name, type, options);
}

private getOrCreateCounter(
Expand Down
38 changes: 19 additions & 19 deletions src/middleware/api-metrics.middleware.ts
@@ -1,7 +1,7 @@
import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
import * as responseTime from 'response-time';
import * as urlParser from 'url';
import { Counter, Labels, ValueRecorder } from '@opentelemetry/api-metrics';
import { Counter, Attributes, Histogram } from '@opentelemetry/api-metrics';
import { OpenTelemetryModuleOptions } from '../interfaces';
import { MetricService } from '../metrics/metric.service';
import { OPENTELEMETRY_MODULE_OPTIONS } from '../opentelemetry.constants';
Expand Down Expand Up @@ -37,19 +37,19 @@ export class ApiMetricsMiddleware implements NestMiddleware {

private serverAbortsTotal: Counter;

private requestDuration: ValueRecorder;
private requestDuration: Histogram;

private requestSizeValueRecorder: ValueRecorder;
private requestSizeHistogram: Histogram;

private responseSizeValueRecorder: ValueRecorder;
private responseSizeHistogram: Histogram;

private defaultLabels: Labels;
private defaultAttributes: Attributes;

private readonly ignoreUndefinedRoutes: boolean;

constructor(
@Inject(OPENTELEMETRY_MODULE_OPTIONS) private readonly options: OpenTelemetryModuleOptions = {},
@Inject(MetricService) private readonly metricService: MetricService,
@Inject(OPENTELEMETRY_MODULE_OPTIONS) private readonly options: OpenTelemetryModuleOptions = {},
) {
this.requestTotal = this.metricService.getCounter('http_request_total', {
description: 'Total number of HTTP requests',
Expand Down Expand Up @@ -80,25 +80,25 @@ export class ApiMetricsMiddleware implements NestMiddleware {
});

const {
timeBuckets = [], requestSizeBuckets = [], responseSizeBuckets = [], defaultLabels = {},
timeBuckets = [], requestSizeBuckets = [], responseSizeBuckets = [], defaultAttributes = {},
ignoreUndefinedRoutes = false,
} = options?.metrics?.apiMetrics;

this.defaultLabels = defaultLabels;
this.defaultAttributes = defaultAttributes;
this.ignoreUndefinedRoutes = ignoreUndefinedRoutes;

this.requestDuration = this.metricService.getValueRecorder('http_request_duration_seconds', {
this.requestDuration = this.metricService.getHistogram('http_request_duration_seconds', {
boundaries: timeBuckets.length > 0 ? timeBuckets : this.defaultLongRunningRequestBuckets,
description: 'HTTP latency value recorder in seconds',
});

this.requestSizeValueRecorder = this.metricService.getValueRecorder('http_request_size_bytes', {
this.requestSizeHistogram = this.metricService.getHistogram('http_request_size_bytes', {
boundaries:
requestSizeBuckets.length > 0 ? requestSizeBuckets : this.defaultRequestSizeBuckets,
description: 'Current total of incoming bytes',
});

this.responseSizeValueRecorder = this.metricService.getValueRecorder('http_response_size_bytes', {
this.responseSizeHistogram = this.metricService.getHistogram('http_response_size_bytes', {
boundaries:
responseSizeBuckets.length > 0 ? responseSizeBuckets : this.defaultResponseSizeBuckets,
description: 'Current total of outgoing bytes',
Expand All @@ -119,21 +119,21 @@ export class ApiMetricsMiddleware implements NestMiddleware {
path = urlParser.parse(url).pathname;
}

this.requestTotal.bind({ method, path }).add(1);
this.requestTotal.add(1, { method, path });

const requestLength = parseInt(req.headers['content-length'], 10) || 0;
const responseLength: number = parseInt(res.getHeader('Content-Length'), 10) || 0;

const status = res.statusCode || 500;
const labels: Labels = {
method, status, path, ...this.defaultLabels,
const attributes: Attributes = {
method, status, path, ...this.defaultAttributes,
};

this.requestSizeValueRecorder.bind(labels).record(requestLength);
this.responseSizeValueRecorder.bind(labels).record(responseLength);
this.requestSizeHistogram.record(requestLength, attributes);
this.responseSizeHistogram.record(responseLength, attributes);

this.responseTotal.bind(labels).add(1);
this.requestDuration.bind(labels).record(time / 1000);
this.responseTotal.add(1, attributes);
this.requestDuration.record(time / 1000, attributes);

const codeClass = this.getStatusCodeClass(status);

Expand All @@ -143,7 +143,7 @@ export class ApiMetricsMiddleware implements NestMiddleware {
this.responseSuccessTotal.add(1);
break;
case 'redirect':
// TODO: Review what should be appropriate for redirects.
// TODO: Review what should be appropriate for redirects.
this.responseSuccessTotal.add(1);
break;
case 'client_error':
Expand Down
16 changes: 8 additions & 8 deletions tests/e2e/metrics/metric.service.spec.ts
Expand Up @@ -165,8 +165,8 @@ describe('MetricService', () => {
});
});

describe('getValueRecorder', () => {
it('creates a new valueRecorder on meterData on the first time method is called', async () => {
describe('getHistogram', () => {
it('creates a new histogram on meterData on the first time method is called', async () => {
const moduleRef = await Test.createTestingModule({
imports: [OpenTelemetryModule.forRoot({
metrics: {
Expand All @@ -184,15 +184,15 @@ describe('MetricService', () => {
// Starts empty
expect(meterData.size).toBe(0);

const counter = metricService.getValueRecorder('test1');
counter.clear();
const counter = metricService.getHistogram('test1');
// counter.clear();

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

it('reuses an existing valueRecorder on meterData when method is called twice', async () => {
it('reuses an existing histogram on meterData when method is called twice', async () => {
const moduleRef = await Test.createTestingModule({
imports: [OpenTelemetryModule.forRoot({
metrics: {
Expand All @@ -208,10 +208,10 @@ describe('MetricService', () => {

metricService = moduleRef.get<MetricService>(MetricService);

const counter = metricService.getValueRecorder('test1', { description: 'test1 description' });
counter.clear();
const counter = metricService.getHistogram('test1', { description: 'test1 description' });
// counter.clear();

const existingCounter = metricService.getValueRecorder('test1');
const existingCounter = metricService.getHistogram('test1');
expect(meterData.has('test1')).toBeTruthy();

// TODO: The metric class does not expose current description
Expand Down
20 changes: 10 additions & 10 deletions tests/e2e/middleware/api-metrics.middleware.spec.ts
Expand Up @@ -15,7 +15,7 @@ describe('Api Metrics Middleware', () => {
let app: INestApplication;
const metricService = {
getCounter: jest.fn(),
getValueRecorder: jest.fn(),
getHistogram: jest.fn(),
};

let exporter: PrometheusExporter;
Expand Down Expand Up @@ -76,7 +76,7 @@ describe('Api Metrics Middleware', () => {
expect(metricService.getCounter).toHaveBeenCalledWith('http_response_total', { description: 'Total number of HTTP responses' });
expect(metricService.getCounter).toHaveBeenCalledWith('http_response_success_total', { description: 'Total number of all successful responses' });

expect(metricService.getValueRecorder).toHaveBeenCalledTimes(3);
expect(metricService.getHistogram).toHaveBeenCalledTimes(3);
});

it('registers custom boundaries', async () => {
Expand All @@ -99,9 +99,9 @@ describe('Api Metrics Middleware', () => {
app = testingModule.createNestApplication();
await app.init();

expect(metricService.getValueRecorder).toHaveBeenCalledWith('http_request_duration_seconds', { description: 'HTTP latency value recorder in seconds', boundaries });
expect(metricService.getValueRecorder).toHaveBeenCalledWith('http_request_size_bytes', { description: 'Current total of incoming bytes', boundaries });
expect(metricService.getValueRecorder).toHaveBeenCalledWith('http_response_size_bytes', { description: 'Current total of outgoing bytes', boundaries });
expect(metricService.getHistogram).toHaveBeenCalledWith('http_request_duration_seconds', { description: 'HTTP latency value recorder in seconds', boundaries });
expect(metricService.getHistogram).toHaveBeenCalledWith('http_request_size_bytes', { description: 'Current total of incoming bytes', boundaries });
expect(metricService.getHistogram).toHaveBeenCalledWith('http_response_size_bytes', { description: 'Current total of outgoing bytes', boundaries });
});

it('uses custom buckets when provided', async () => {
Expand All @@ -121,7 +121,7 @@ describe('Api Metrics Middleware', () => {
app = testingModule.createNestApplication();
await app.init();

expect(metricService.getValueRecorder).toBeCalledWith('http_request_duration_seconds', { description: 'HTTP latency value recorder in seconds', boundaries: [1, 2] });
expect(metricService.getHistogram).toBeCalledWith('http_request_duration_seconds', { description: 'HTTP latency value recorder in seconds', boundaries: [1, 2] });
});

it('registers successful request records', async () => {
Expand Down Expand Up @@ -236,13 +236,13 @@ describe('Api Metrics Middleware', () => {
expect(/http_server_error_total 1/.test(text)).toBeTruthy();
});

it('registers requests with custom labels', async () => {
it('registers requests with custom attributes', async () => {
const testingModule = await Test.createTestingModule({
imports: [OpenTelemetryModule.forRoot({
metrics: {
apiMetrics: {
enable: true,
defaultLabels: {
defaultAttributes: {
custom: 'label',
},
},
Expand Down Expand Up @@ -386,13 +386,13 @@ describe('Api Metrics Middleware', () => {
expect(/http_server_error_total 1/.test(text)).toBeTruthy();
});

it('registers requests with custom labels when using Fastify', async () => {
it('registers requests with custom attributes when using Fastify', async () => {
const testingModule = await Test.createTestingModule({
imports: [OpenTelemetryModule.forRoot({
metrics: {
apiMetrics: {
enable: true,
defaultLabels: {
defaultAttributes: {
custom: 'label',
},
},
Expand Down

0 comments on commit 6ed63ae

Please sign in to comment.