diff --git a/README.md b/README.md index 7ae5b51..6ec65a5 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,19 @@ opentelemetry-node-metrics `nodeSDKConfiguration` property accepts OpenTelemetry NodeSDK configuration. ```ts -const OpenTelemetryModuleConfig = OpenTelemetryModule.register({ - withPrometheusExporter: { - enable: true, // Enables prometheus exporter - withHostMetrics: true, // Includes Host Metrics - withDefaultMetrics: true, // Includes Default Metrics - withHttpMiddleware: { // Includes api metrics - enable: true, +const OpenTelemetryModuleConfig = OpenTelemetryModule.forRoot({ + metrics: { + hostMetrics: true, // Includes Host Metrics + defaultMetrics: true, // Includes Default Metrics + apiMetrics: { + enable: true, // Includes api metrics timeBuckets: [], }, }, nodeSDKConfiguration: { + metricExporter: new PrometheusExporter({ + port: 8081, + }), spanProcessor: new BatchSpanProcessor(new JaegerExporter()), contextManager: new AsyncLocalStorageContextManager(), textMapPropagator: new CompositePropagator({ @@ -155,7 +157,7 @@ Impl |Metric |Description| Labels | Metric Type ## Prometheus Metrics -When `withPrometheusExporter` is enabled it will start a new process on port `8081` and metrics will be available at `http://localhost:8081/metrics`. +When `nodeSDKConfiguration.metricExporter` is defined with a `PrometheusExporter`it will start a new process on port `8081` (default port) and metrics will be available at `http://localhost:8081/metrics`. ## Using with a logger diff --git a/src/interfaces/opentelemetry-options.interface.ts b/src/interfaces/opentelemetry-options.interface.ts index 95f4996..0587265 100644 --- a/src/interfaces/opentelemetry-options.interface.ts +++ b/src/interfaces/opentelemetry-options.interface.ts @@ -3,24 +3,21 @@ import { NodeSDKConfiguration } from '@opentelemetry/sdk-node'; export type OpenTelemetryModuleOptions = { nodeSDKConfiguration?: Partial /** - * Prometheus Exporter Setup + * OpenTelemetry Metrics Setup */ - withPrometheusExporter?: PrometheusExporterOptions; + metrics?: OpenTelemetryMetrics }; -export type PrometheusExporterOptions = { - enable?: boolean, - withHttpMiddleware?: { +export type OpenTelemetryMetrics = { + defaultMetrics?: boolean, + hostMetrics?: boolean, + apiMetrics?: { enable?: boolean, timeBuckets: number[] }, - withHostMetrics?: boolean, - withDefaultMetrics?: boolean, defaultLabels?: { [key: string]: string | number }, - metricPath?: string - port?: number, }; export interface OpenTelemetryOptionsFactory { diff --git a/src/metrics/metric.service.ts b/src/metrics/metric.service.ts index 25f4cc4..c977650 100644 --- a/src/metrics/metric.service.ts +++ b/src/metrics/metric.service.ts @@ -20,7 +20,7 @@ export class MetricService { private meterData: Map = new Map(); constructor(@Inject(OPENTELEMETRY_METER_PROVIDER) private readonly meterProvider: MeterProvider) { - this.meter = this.meterProvider.getMeter('prometheus-metrics'); + this.meter = this.meterProvider.getMeter('metrics'); } getCounter(name: string, options?: MetricOptions) { diff --git a/src/middleware/api-metrics.middleware.ts b/src/middleware/api-metrics.middleware.ts index 78f3aa4..cd65c68 100644 --- a/src/middleware/api-metrics.middleware.ts +++ b/src/middleware/api-metrics.middleware.ts @@ -55,7 +55,7 @@ export class ApiMetricsMiddleware implements NestMiddleware { description: 'Total number of server error requests', }); - const { timeBuckets = [] } = this.options?.withPrometheusExporter?.withHttpMiddleware; + const { timeBuckets = [] } = this.options?.metrics?.apiMetrics; this.requestDuration = this.metricService.getValueRecorder('http_request_duration_seconds', { boundaries: timeBuckets.length > 0 ? timeBuckets : this.defaultLongRunningRequestBuckets, description: 'HTTP latency value recorder in seconds', @@ -70,7 +70,8 @@ export class ApiMetricsMiddleware implements NestMiddleware { if (path === '/favicon.ico') { return; } - const metricPath = this.options?.withPrometheusExporter?.metricPath ? this.options.withPrometheusExporter.metricPath : '/metrics'; + // TODO Support `ignore routes array instead` + const metricPath = '/metrics'; if (path === metricPath) { return; } diff --git a/src/opentelemetry-core.module.ts b/src/opentelemetry-core.module.ts index 30dbf49..1a522c4 100644 --- a/src/opentelemetry-core.module.ts +++ b/src/opentelemetry-core.module.ts @@ -4,18 +4,18 @@ import { OnApplicationShutdown, Provider, } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; -import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; import { HostMetrics } from '@opentelemetry/host-metrics'; import { MeterProvider } from '@opentelemetry/metrics'; -import { NodeSDK } from '@opentelemetry/sdk-node'; import * as nodeMetrics from 'opentelemetry-node-metrics'; +import { metrics } from '@opentelemetry/api-metrics'; +import { NodeSDK } from '@opentelemetry/sdk-node'; import { OpenTelemetryModuleOptions } from './interfaces'; import { MetricService } from './metrics/metric.service'; import { ApiMetricsMiddleware } from './middleware'; import { OPENTELEMETRY_METER_PROVIDER, OPENTELEMETRY_MODULE_OPTIONS, - OPENTELEMETRY_PROMETHEUS_EXPORTER, OPENTELEMETRY_SDK, + OPENTELEMETRY_SDK, } from './opentelemetry.constants'; import { TraceService } from './tracing/trace.service'; @@ -29,8 +29,8 @@ export class OpenTelemetryCoreModule implements OnApplicationShutdown, OnApplica private readonly moduleRef: ModuleRef, ) {} - static async register( - options: OpenTelemetryModuleOptions = { withPrometheusExporter: {} }, + static async forRoot( + options: OpenTelemetryModuleOptions = { metrics: {} }, ): Promise { const openTelemetryModuleOptions = { provide: OPENTELEMETRY_MODULE_OPTIONS, @@ -40,28 +40,16 @@ export class OpenTelemetryCoreModule implements OnApplicationShutdown, OnApplica const providers = [ openTelemetryModuleOptions, await this.createNodeSDKProvider(), + this.createMeterProvider(), TraceService, + MetricService, ]; const exports: Provider[] = [ TraceService, + MetricService, ]; - const { - enable = false, - } = options?.withPrometheusExporter; - - if (enable) { - providers.push(this.createPrometheusExporterProvider()); - providers.push(this.createMeterProvider()); - providers.push(MetricService); - exports.push(MetricService); - } else { - // Conditional Injection for factories/static methods is not available yet. - // More info here: https://github.com/nestjs/nest/issues/7306 - providers.push(this.noopPrometheusExporterProvider()); - } - return { module: OpenTelemetryCoreModule, providers, @@ -71,18 +59,23 @@ export class OpenTelemetryCoreModule implements OnApplicationShutdown, OnApplica configure(consumer: MiddlewareConsumer) { const { - withHttpMiddleware = { enable: false }, - } = this.options?.withPrometheusExporter; - if (withHttpMiddleware.enable === true) { + apiMetrics = { enable: false }, + } = this.options?.metrics; + if (apiMetrics.enable === true) { consumer.apply(ApiMetricsMiddleware).forRoutes('*'); } } async onApplicationBootstrap() { const nodeOtelSDK = this.moduleRef.get(OPENTELEMETRY_SDK); - + const meterProvider = this.moduleRef.get(OPENTELEMETRY_METER_PROVIDER); try { + this.logger.log('NestJS OpenTelemetry starting'); await nodeOtelSDK.start(); + // Start method sets a custom meter provider + // when exporter is defined. Overwrites that here. + // Possible improvements can be found here: https://github.com/open-telemetry/opentelemetry-js/issues/2307 + metrics.setGlobalMeterProvider(meterProvider); } catch (e) { this.logger.error(e?.message); } @@ -101,60 +94,41 @@ export class OpenTelemetryCoreModule implements OnApplicationShutdown, OnApplica private static async createNodeSDKProvider(): Promise { return { provide: OPENTELEMETRY_SDK, - useFactory: async (options: OpenTelemetryModuleOptions, exporter: PrometheusExporter) => { - const { enable = false } = options?.withPrometheusExporter; - if (enable) { - // eslint-disable-next-line no-param-reassign - options.nodeSDKConfiguration.metricExporter = exporter; - } - return new NodeSDK(options.nodeSDKConfiguration); + useFactory: (options: OpenTelemetryModuleOptions, meterProvider: MeterProvider) => { + const sdk = new NodeSDK( + { ...options.nodeSDKConfiguration }, + ); + return sdk; }, - inject: [OPENTELEMETRY_MODULE_OPTIONS, OPENTELEMETRY_PROMETHEUS_EXPORTER], - }; - } - - private static createPrometheusExporterProvider(): Provider { - return { - provide: OPENTELEMETRY_PROMETHEUS_EXPORTER, - useFactory: (options: OpenTelemetryModuleOptions) => { - const { port = 8081 } = options?.withPrometheusExporter; - return new PrometheusExporter({ port }); - }, - inject: [OPENTELEMETRY_MODULE_OPTIONS], - }; - } - - private static noopPrometheusExporterProvider(): Provider { - return { - provide: OPENTELEMETRY_PROMETHEUS_EXPORTER, - useFactory: () => null, + inject: [OPENTELEMETRY_MODULE_OPTIONS, OPENTELEMETRY_METER_PROVIDER], }; } private static createMeterProvider(): Provider { return { provide: OPENTELEMETRY_METER_PROVIDER, - useFactory: (exporter: PrometheusExporter, options: OpenTelemetryModuleOptions) => { + useFactory: (options: OpenTelemetryModuleOptions) => { const { - withDefaultMetrics = false, withHostMetrics = false, - } = options?.withPrometheusExporter; + defaultMetrics = false, hostMetrics = false, + } = options?.metrics; const meterProvider = new MeterProvider({ - exporter, interval: 1000, + exporter: options?.nodeSDKConfiguration?.metricExporter, }); - if (withDefaultMetrics) { + if (defaultMetrics) { nodeMetrics(meterProvider); } - if (withHostMetrics) { - const hostMetrics = new HostMetrics({ meterProvider, name: 'host-metrics' }); - hostMetrics.start(); + if (hostMetrics) { + const host = new HostMetrics({ meterProvider, name: 'host-metrics' }); + host.start(); } + return meterProvider; }, - inject: [OPENTELEMETRY_PROMETHEUS_EXPORTER, OPENTELEMETRY_MODULE_OPTIONS], + inject: [OPENTELEMETRY_MODULE_OPTIONS], }; } } diff --git a/src/opentelemetry.constants.ts b/src/opentelemetry.constants.ts index 15627ea..696e62c 100644 --- a/src/opentelemetry.constants.ts +++ b/src/opentelemetry.constants.ts @@ -5,5 +5,4 @@ export enum Constants { export const OPENTELEMETRY_SDK = 'OPEN_TELEMETRY_SDK'; export const OPENTELEMETRY_MODULE_OPTIONS = 'OpenTelemetryModuleOptions'; export const OPENTELEMETRY_MODULE_ID = 'OpenTelemetryModuleId'; -export const OPENTELEMETRY_PROMETHEUS_EXPORTER = 'OpenTelemetryPrometheusExporter'; export const OPENTELEMETRY_METER_PROVIDER = 'OpenTelemetryMeterProvider'; diff --git a/src/opentelemetry.module.ts b/src/opentelemetry.module.ts index 2d9ec2e..a3904d1 100644 --- a/src/opentelemetry.module.ts +++ b/src/opentelemetry.module.ts @@ -4,12 +4,12 @@ import { OpenTelemetryModuleOptions } from './interfaces'; @Module({}) export class OpenTelemetryModule { - static async register( + static async forRoot( options?: OpenTelemetryModuleOptions, ): Promise { return { module: OpenTelemetryModule, - imports: [await OpenTelemetryCoreModule.register(options)], + imports: [await OpenTelemetryCoreModule.forRoot(options)], }; } } diff --git a/tests/e2e/module.spec.ts b/tests/e2e/module.spec.ts index b6acda2..801f873 100644 --- a/tests/e2e/module.spec.ts +++ b/tests/e2e/module.spec.ts @@ -6,7 +6,7 @@ import { createOpenTelemetryModule } from '../utils'; describe('OpenTelemetryModule', () => { let app: INestApplication; - describe('#register', () => { + describe('#forRoot', () => { beforeEach(async () => { ({ app } = await createOpenTelemetryModule()); }); diff --git a/tests/utils.ts b/tests/utils.ts index 06b7fa6..04e97c0 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -17,7 +17,7 @@ export async function createOpenTelemetryModule( options?: OpenTelemetryModuleOptions, ): Promise { const testingModule = await Test.createTestingModule({ - imports: [OpenTelemetryModule.register(options)], + imports: [OpenTelemetryModule.forRoot(options)], }).compile(); const app = testingModule.createNestApplication();