Skip to content

Commit

Permalink
Merge pull request #5014 from novuhq/add-otel
Browse files Browse the repository at this point in the history
feat(pkg): Add Open-Telemetry and Prometheus Montioring to Novu
  • Loading branch information
Cliftonz authored Jan 16, 2024
2 parents 71c037f + a541b5a commit cd9d5e2
Show file tree
Hide file tree
Showing 17 changed files with 1,725 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,9 @@
"Stdev",
"openapi",
"headerapikey",
"isend",
"Otel",
"opentelemetry",
"INITDB",
"isend",
"Idand"
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { RavenInterceptor, RavenModule } from 'nest-raven';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import * as packageJson from '../package.json';
import { TracingModule } from '@novu/application-generic';

import { SharedModule } from './app/shared/shared.module';
import { UserModule } from './app/user/user.module';
Expand Down Expand Up @@ -83,6 +85,7 @@ const baseModules: Array<Type | DynamicModule | Promise<DynamicModule> | Forward
TenantModule,
WorkflowOverridesModule,
RateLimitingModule,
TracingModule.register(packageJson.name),
];

const enterpriseModules = enterpriseImports();
Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/app/integrations/integrations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { ChannelTypeEnum, IJwtPayload, MemberRoleEnum } from '@novu/shared';
import { CalculateLimitNovuIntegration, CalculateLimitNovuIntegrationCommand } from '@novu/application-generic';
import {
CalculateLimitNovuIntegration,
CalculateLimitNovuIntegrationCommand,
OtelSpan,
} from '@novu/application-generic';
import { ApiExcludeEndpoint, ApiOperation, ApiTags } from '@nestjs/swagger';

import { UserAuthGuard } from '../auth/framework/user.auth.guard';
Expand Down Expand Up @@ -253,6 +257,7 @@ export class IntegrationsController {

@Get('/:channelType/limit')
@ApiExcludeEndpoint()
@OtelSpan()
async getProviderLimit(
@UserSession() user: IJwtPayload,
@Param('channelType') channelType: ChannelTypeEnum
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/app/layouts/layouts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
ApiOkResponse,
} from '../shared/framework/response.decorator';
import { IJwtPayload } from '@novu/shared';
import { GetLayoutCommand, GetLayoutUseCase } from '@novu/application-generic';
import { GetLayoutCommand, GetLayoutUseCase, OtelSpan } from '@novu/application-generic';

import {
CreateLayoutRequestDto,
Expand Down Expand Up @@ -71,6 +71,7 @@ export class LayoutsController {
@ExternalApiAccessible()
@ApiResponse(CreateLayoutResponseDto, 201)
@ApiOperation({ summary: 'Layout creation', description: 'Create a layout' })
@OtelSpan()
async createLayout(
@UserSession() user: IJwtPayload,
@Body() body: CreateLayoutRequestDto
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/config/env-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ const validators: { [K in keyof any]: ValidatorSpec<any[K]> } = {
WORKER_DEFAULT_LOCK_DURATION: num({
default: undefined,
}),
ENABLE_OTEL: str({
default: 'false',
choices: ['false', 'true'],
}),
};

if (process.env.STORAGE_SERVICE === 'AZURE') {
Expand Down
3 changes: 2 additions & 1 deletion apps/webhook/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { AppService } from './app.service';
import { SharedModule } from './shared/shared.module';
import { HealthModule } from './health/health.module';
import { WebhooksModule } from './webhooks/webhooks.module';
import { createNestLoggingModuleOptions, LoggerModule } from '@novu/application-generic';
import { createNestLoggingModuleOptions, LoggerModule, TracingModule } from '@novu/application-generic';
const packageJson = require('../package.json');

const modules = [
SharedModule,
HealthModule,
WebhooksModule,
TracingModule.register(packageJson.name),
LoggerModule.forRoot(
createNestLoggingModuleOptions({
serviceName: packageJson.name,
Expand Down
4 changes: 2 additions & 2 deletions apps/webhook/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import './config';
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import * as Sentry from '@sentry/node';
import { version } from '../package.json';
import { getErrorInterceptor, Logger } from '@novu/application-generic';
import * as packageJson from '../package.json';

import { AppModule } from './app.module';

if (process.env.SENTRY_DSN) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
release: `v${version}`,
release: `v${packageJson.version}`,
});
}

Expand Down
3 changes: 2 additions & 1 deletion apps/ws/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { RavenInterceptor, RavenModule } from 'nest-raven';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { createNestLoggingModuleOptions, LoggerModule } from '@novu/application-generic';
import { createNestLoggingModuleOptions, LoggerModule, TracingModule } from '@novu/application-generic';

import { AppController } from './app.controller';
import { AppService } from './app.service';
Expand All @@ -14,6 +14,7 @@ const packageJson = require('../package.json');
const modules = [
SharedModule,
HealthModule,
TracingModule.register(packageJson.name),
SocketModule,
LoggerModule.forRoot(
createNestLoggingModuleOptions({
Expand Down
2 changes: 1 addition & 1 deletion apps/ws/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import helmet from 'helmet';
import { NestFactory } from '@nestjs/core';
import * as Sentry from '@sentry/node';
import { BullMqService, getErrorInterceptor, Logger } from '@novu/application-generic';
import * as packageJson from '../package.json';

import { AppModule } from './app.module';
import { CONTEXT_PATH } from './config';
Expand All @@ -19,7 +20,6 @@ if (process.env.SENTRY_DSN) {
release: `v${version}`,
});
}

export async function bootstrap() {
BullMqService.haveProInstalled();
const app = await NestFactory.create(AppModule, { bufferLogs: true });
Expand Down
16 changes: 16 additions & 0 deletions packages/application-generic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@
"@novu/zulip": "^0.22.0",
"@novu/nexmo": "^0.22.0",
"@novu/rocket-chat": "^0.22.0",
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/auto-instrumentations-node": "^0.40.2",
"@opentelemetry/context-async-hooks": "^1.19.0",
"@opentelemetry/core": "^1.19.0",
"@opentelemetry/exporter-collector": "^0.25.0",
"@opentelemetry/exporter-jaeger": "^1.19.0",
"@opentelemetry/exporter-prometheus": "^0.46.0",
"@opentelemetry/instrumentation": "^0.46.0",
"@opentelemetry/propagator-b3": "^1.19.0",
"@opentelemetry/propagator-jaeger": "^1.19.0",
"@opentelemetry/resources": "^1.19.0",
"@opentelemetry/sdk-node": "^0.46.0",
"@opentelemetry/sdk-trace-base": "^1.19.0",
"@opentelemetry/sdk-trace-node": "^1.19.0",
"@opentelemetry/semantic-conventions": "^1.19.0",
"@sentry/node": "^7.12.1",
"@segment/analytics-node": "^1.1.4",
"axios": "^1.6.2",
Expand All @@ -127,6 +142,7 @@
"launchdarkly-node-server-sdk": "^7.0.1",
"lodash": "^4.17.15",
"mixpanel": "^0.17.0",
"nestjs-otel": "^5.1.5",
"nestjs-pino": "^3.4.0",
"node-fetch": "^3.2.10",
"pino-http": "^8.3.3",
Expand Down
1 change: 1 addition & 0 deletions packages/application-generic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export * from './utils/digest';
export * from './utils/object';
export * from './decorators/external-api.decorator';
export * from './decorators/user-session.decorator';
export * from './tracing';
export * from './dtos';
2 changes: 2 additions & 0 deletions packages/application-generic/src/tracing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './tracing.module';
export * from './otel-wrapper';
100 changes: 100 additions & 0 deletions packages/application-generic/src/tracing/otel-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Span } from 'nestjs-otel';
import { TraceService as setTraceService } from 'nestjs-otel';
import { MetricService as setMetricService } from 'nestjs-otel';
import { OtelInstanceCounter as setOtelInstanceCounter } from 'nestjs-otel';
import { OtelUpDownCounter as setOtelUpDownCounter } from 'nestjs-otel';
import { OtelHistogram as setOtelHistogram } from 'nestjs-otel';
import { OtelObservableGauge as setOtelObservableGauge } from 'nestjs-otel';
import { OtelObservableCounter as setOtelObservableCounter } from 'nestjs-otel';
import { OtelObservableUpDownCounter as setOtelObservableUpDownCounter } from 'nestjs-otel';
import { OtelCounter as setOtelCounter } from 'nestjs-otel';
import { MetricOptions, SpanOptions } from '@opentelemetry/api';
import { Injectable } from '@nestjs/common';
import { PipeTransform, Type } from '@nestjs/common';

export type OtelDataOrPipe =
| string
| PipeTransform<any, any>
| Type<PipeTransform<any, any>>;

// eslint-disable-next-line @typescript-eslint/naming-convention
export function OtelSpan(name?: string, options?: SpanOptions) {
return Span(name, options);
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export function OtelInstanceCounter(options?: MetricOptions) {
return setOtelInstanceCounter(options);
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export function OtelUpDownCounter(...dataOrPipes: OtelDataOrPipe[]) {
return setOtelUpDownCounter(...dataOrPipes);
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export function OtelHistogram(...dataOrPipes: OtelDataOrPipe[]) {
return setOtelHistogram(...dataOrPipes);
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export function OtelObservableGauge(...dataOrPipes: OtelDataOrPipe[]) {
return setOtelObservableGauge(...dataOrPipes);
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export function OtelObservableCounter(...dataOrPipes: OtelDataOrPipe[]) {
return setOtelObservableCounter(...dataOrPipes);
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export function OtelObservableUpDownCounter(...dataOrPipes: OtelDataOrPipe[]) {
return setOtelObservableUpDownCounter(...dataOrPipes);
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export function OtelCounter(...dataOrPipes: OtelDataOrPipe[]) {
return setOtelCounter(...dataOrPipes);
}

@Injectable()
export class TraceService extends setTraceService {
getTracer() {
return super.getTracer();
}

getSpan() {
return super.getSpan();
}

startSpan(name: string) {
return super.startSpan(name);
}
}

@Injectable()
export class MetricService extends setMetricService {
getCounter(name, options) {
return super.getCounter(name, options);
}

getUpDownCounter(name, options) {
return super.getUpDownCounter(name, options);
}

getHistogram(name, options) {
return super.getHistogram(name, options);
}

getObservableCounter(name, options) {
return super.getObservableCounter(name, options);
}

getObservableGauge(name, options) {
return super.getObservableGauge(name, options);
}

getObservableUpDownCounter(name, options) {
return super.getObservableUpDownCounter(name, options);
}
}
34 changes: 34 additions & 0 deletions packages/application-generic/src/tracing/tracing.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { DynamicModule, Module } from '@nestjs/common';
import { TracingService } from './tracing.service';
import { OpenTelemetryModule } from 'nestjs-otel';

// eslint-disable-next-line @typescript-eslint/naming-convention
const OtelModule = OpenTelemetryModule.forRoot({
metrics: {
hostMetrics: true,
apiMetrics: {
enable: true,
ignoreRoutes: ['/favicon.ico', '/v1/health-check'],
//Records metrics for all URLs, even undefined ones
ignoreUndefinedRoutes: true,
},
},
});

@Module({})
export class TracingModule {
static register(serviceName: string): DynamicModule {
return {
module: TracingModule,
imports: [OtelModule],
providers: [
TracingService,
{ provide: 'TRACING_SERVICE_NAME', useValue: serviceName },
{
provide: 'TRACING_ENABLE_OTEL',
useValue: process.env.ENABLE_OTEL === 'true',
},
],
};
}
}
34 changes: 34 additions & 0 deletions packages/application-generic/src/tracing/tracing.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
Inject,
Injectable,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { initializeOtelSdk } from './tracing';
import { NodeSDK } from '@opentelemetry/sdk-node';

@Injectable()
export class TracingService implements OnModuleInit, OnModuleDestroy {
private otelSDKInstance: NodeSDK;

constructor(
@Inject('TRACING_SERVICE_NAME') private readonly serviceName: string,
@Inject('TRACING_ENABLE_OTEL') private readonly otelEnabled: boolean
) {}

async onModuleDestroy() {
if (this.otelSDKInstance) {
await this.otelSDKInstance.shutdown();
}
}
onModuleInit() {
if (!this.hasValidParameters()) return;

this.otelSDKInstance = initializeOtelSdk(this.serviceName);
this.otelSDKInstance.start();
}

private hasValidParameters() {
return this.serviceName && this.otelEnabled;
}
}
37 changes: 37 additions & 0 deletions packages/application-generic/src/tracing/tracing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
CompositePropagator,
W3CBaggagePropagator,
W3CTraceContextPropagator,
} from '@opentelemetry/core';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { JaegerPropagator } from '@opentelemetry/propagator-jaeger';
import { B3InjectEncoding, B3Propagator } from '@opentelemetry/propagator-b3';
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';

// eslint-disable-next-line @typescript-eslint/naming-convention
export function initializeOtelSdk(serviceName: string) {
return new NodeSDK({
metricReader: new PrometheusExporter({
port: 9464,
}),
spanProcessor: new BatchSpanProcessor(new JaegerExporter()),
contextManager: new AsyncLocalStorageContextManager(),
serviceName: serviceName,
textMapPropagator: new CompositePropagator({
propagators: [
new JaegerPropagator(),
new W3CTraceContextPropagator(),
new W3CBaggagePropagator(),
new B3Propagator(),
new B3Propagator({
injectEncoding: B3InjectEncoding.MULTI_HEADER,
}),
],
}),
instrumentations: [getNodeAutoInstrumentations()],
});
}
Loading

0 comments on commit cd9d5e2

Please sign in to comment.