Skip to content

Commit

Permalink
feat: open telemetry support
Browse files Browse the repository at this point in the history
  • Loading branch information
liorzblrn committed Oct 31, 2023
1 parent a8a4547 commit e3cab27
Show file tree
Hide file tree
Showing 11 changed files with 620 additions and 13 deletions.
469 changes: 460 additions & 9 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions services/workflows-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,20 @@
"@nestjs/platform-express": "^9.3.12",
"@nestjs/serve-static": "3.0.1",
"@nestjs/testing": "^9.3.12",
"@opentelemetry/api": "^1.6.0",
"@opentelemetry/context-async-hooks": "^1.17.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.44.0",
"@opentelemetry/instrumentation": "^0.44.0",
"@opentelemetry/instrumentation-dns": "^0.32.3",
"@opentelemetry/instrumentation-http": "^0.44.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.33.2",
"@opentelemetry/instrumentation-net": "^0.32.2",
"@opentelemetry/resources": "^1.17.1",
"@opentelemetry/sdk-trace-base": "^1.17.1",
"@opentelemetry/sdk-trace-node": "^1.17.1",
"@opentelemetry/semantic-conventions": "^1.17.1",
"@prisma/client": "^4.13.0",
"@prisma/instrumentation": "^5.5.2",
"@sentry/cli": "^2.17.5",
"@sentry/integrations": "^7.52.1",
"@sentry/node": "^7.52.1",
Expand Down Expand Up @@ -85,6 +98,7 @@
"nest-access-control": "2.2.0",
"nestjs-cls": "^3.5.0",
"nestjs-prisma": "0.20.0",
"npm": "^10.2.1",
"passport": "0.6.0",
"passport-http": "0.3.0",
"passport-jwt": "4.0.1",
Expand Down
2 changes: 2 additions & 0 deletions services/workflows-service/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ datasource db {

generator client {
provider = "prisma-client-js"
binaryTargets = ["native"]
previewFeatures = ["tracing"]
}

model User {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { AppLoggerService } from '@/common/app-logger/app-logger.service';
import { WinstonLogger } from '@/common/utils/winston-logger/winston-logger';
import { Global, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Global()
@Module({
providers: [{ useClass: WinstonLogger, provide: 'LOGGER' }, AppLoggerService],
exports: [AppLoggerService],
providers: [
{ useClass: WinstonLogger, provide: 'LOGGER' },
AppLoggerService,
{
provide: 'LOGGER_SETTINGS',
useFactory: (config: ConfigService) => {
return {
logLevel: config.getOrThrow<string>('LOG_LEVEL'),
envName: config.getOrThrow<string>('ENVIRONMENT_NAME'),
};
},
inject: [ConfigService],
},
],
imports: [ConfigModule],
})
export class AppLoggerModule {}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { IAppLogger, LogPayload } from '@/common/abstract-logger/abstract-logger';
import { Inject } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createLogger, format, transports, Logger as TWinstonLogger } from 'winston';

export type LoggerSettings = {
envName: string;
logLevel: string;
};

export class WinstonLogger implements IAppLogger {
private logger: TWinstonLogger;

constructor() {
const isProduction = process.env.NODE_ENV === 'production';
constructor(@Inject('LOGGER_SETTINGS') private readonly loggerSettings: LoggerSettings) {
const isProduction = loggerSettings.envName === 'production';

const productionFormat = format.combine(format.timestamp(), format.json());

Expand All @@ -23,6 +30,7 @@ export class WinstonLogger implements IAppLogger {
);

this.logger = createLogger({
level: loggerSettings.logLevel || 'info',
format: isProduction ? productionFormat : developmentFormat,
transports: [new transports.Console()],
});
Expand Down
1 change: 1 addition & 0 deletions services/workflows-service/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const env = createEnv({
*/
clientPrefix: 'PUBLIC_',
server: {
LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']).optional().default('info'), // TODO: remove 'test', 'local'
NODE_ENV: z.enum(['development', 'production', 'test', 'local']), // TODO: remove 'test', 'local'
ENVIRONMENT_NAME: z.enum(['development', 'production', 'staging', 'test', 'local']),
ENV_FILE_NAME: z.string().optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ async function __getTables(prisma: PrismaClient): Promise<string[]> {

const __removeAllTableContent = async (prisma: PrismaClient, tableNames: Array<string>) => {
for (const table of tableNames) {
await prisma.$executeRawUnsafe(`DELETE FROM ${TEST_DATABASE_SCHEMA_NAME}."${table}" CASCADE;`);
await prisma.$executeRawUnsafe(
`TRUNCATE TABLE ${TEST_DATABASE_SCHEMA_NAME}."${table}" CASCADE;`,
);
}
};
31 changes: 31 additions & 0 deletions services/workflows-service/src/tracing/http-instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { IncomingMessage } from 'http';

export function createFilter(filters: RegExp[] | string) {
let f: RegExp[];
if (typeof filters === 'string') {
f = filters.split(',').map(r => new RegExp(r));
} else {
f = filters;
}

return (url: string) => {
return f.some(reg => {
return url.match(reg);
});
};
}

export class IgnoreByUrlHttpInstrumentation extends HttpInstrumentation {
constructor(excludeUrls: RegExp[] | string = []) {
const urlsFilter = createFilter(excludeUrls);
super({
ignoreOutgoingRequestHook: () => true,
ignoreIncomingRequestHook: (request: IncomingMessage) => {
if (!request.url) return false;

return urlsFilter(request.url);
},
});
}
}
20 changes: 20 additions & 0 deletions services/workflows-service/src/tracing/trace-id.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { getTraceId } from './tracing-utils';
import * as opentelemetry from '@opentelemetry/api';

@Injectable()
export class TraceIdMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
if (req.url.includes('health')) {
// TODO: add live/rediness
return next();
}

const span = opentelemetry.trace.getActiveSpan();
if (span && span.spanContext()) {
const traceId = getTraceId(span);
res.setHeader('x-trace-id', traceId);
}
return next();
}
}
55 changes: 55 additions & 0 deletions services/workflows-service/src/tracing/tracer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as api from '@opentelemetry/api';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import {
ConsoleSpanExporter,
SimpleSpanProcessor,
NodeTracerProvider,
} from '@opentelemetry/sdk-trace-node';
import { PrismaInstrumentation } from '@prisma/instrumentation';
import { Resource } from '@opentelemetry/resources';
import { IgnoreByUrlHttpInstrumentation } from './http-instrumentation';
import { DnsInstrumentation } from '@opentelemetry/instrumentation-dns';
import { NetInstrumentation } from '@opentelemetry/instrumentation-net';
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';

const contextManager = new AsyncHooksContextManager();
contextManager.enable();
api.context.setGlobalContextManager(contextManager);

const resource = Resource.default().merge(
new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'workflow-service',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]:
process.env.ENVIRONMENT_NAME || process.env.NODE_ENV,
}),
);

// Configure the trace provider
const provider = new NodeTracerProvider({
resource,
});

// Configure how spans are processed and exported. In this case we're sending spans
// as we receive them to an OTLP-compatible collector (e.g. Jaeger).
// provider.addSpanProcessor(new SimpleSpanProcessor(new OTLPTraceExporter()));
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));

// Register your auto-instrumentors
registerInstrumentations({
tracerProvider: provider,
instrumentations: [
new DnsInstrumentation(),
new NetInstrumentation(),
new NestInstrumentation(),
new IgnoreByUrlHttpInstrumentation([/favicon.ico/, /__vite_ping/, /health/]),
new PrismaInstrumentation({
enabled: true,
}),
],
});

// Register the provider globally
provider.register();
8 changes: 8 additions & 0 deletions services/workflows-service/src/tracing/tracing-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function getTraceId(span: any): string | undefined {
if (!span) {
// TODO: Should log a warning?
return;
}

return span.spanContext().traceId;
}

0 comments on commit e3cab27

Please sign in to comment.