Skip to content

Commit f5ce197

Browse files
authored
feat: otel and signoz implementation (#1297)
* feat: otel and signoz implementation Signed-off-by: Tipu_Singh <[email protected]> * fix:resolve suggested comments Signed-off-by: Tipu_Singh <[email protected]> * fix:resolve suggested for logging Signed-off-by: Tipu_Singh <[email protected]> * feat: added flag for local setup Signed-off-by: Tipu_Singh <[email protected]> * refactor: updated env sample and demo Signed-off-by: Tipu_Singh <[email protected]> --------- Signed-off-by: Tipu_Singh <[email protected]>
1 parent d37a87e commit f5ce197

File tree

9 files changed

+4687
-3758
lines changed

9 files changed

+4687
-3758
lines changed

.env.demo

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,14 @@ APP=api
145145
#Schema-file-server
146146
APP_PORT=4000
147147
JWT_TOKEN_SECRET=
148-
ISSUER=Credebl
148+
ISSUER=Credebl
149+
150+
#Signoz and OTel
151+
IS_ENABLE_OTEL=false
152+
OTEL_SERVICE_NAME='CREDEBL-PLATFORM-SERVICE'
153+
OTEL_SERVICE_VERSION='1.0.0'
154+
OTEL_TRACES_OTLP_ENDPOINT='http://localhost:4318/v1/traces'
155+
OTEL_LOGS_OTLP_ENDPOINT='http://localhost:4318/v1/logs'
156+
OTEL_HEADERS_KEY=88ca6b1XXXXXXXXXXXXXXXXXXXXXXXXXXX
157+
OTEL_LOGGER_NAME='credebl-platform-logger'
158+
HOSTNAME='localhost'

.env.sample

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,13 @@ ELK_PASSWORD=xxxxxx // ELK user password
166166

167167
ORGANIZATION=credebl
168168
CONTEXT=platform
169-
APP=api
169+
APP=api
170+
171+
IS_ENABLE_OTEL=false # Flag to enable/disable OpenTelemetry (true = enabled, false = disabled)
172+
OTEL_SERVICE_NAME='CREDEBL-PLATFORM-SERVICE' # Logical name of the service shown in observability tools (e.g., SigNoz)
173+
OTEL_SERVICE_VERSION='1.0.0' # Version of the service; helps in tracking changes over time
174+
OTEL_TRACES_OTLP_ENDPOINT='http://localhost:4318/v1/traces' # Endpoint where traces are exported (OTLP over HTTP)
175+
OTEL_LOGS_OTLP_ENDPOINT='http://localhost:4318/v1/logs' # Endpoint where logs are exported (OTLP over HTTP)
176+
OTEL_HEADERS_KEY=88ca6b1XXXXXXXXXXXXXXXXXXXXXXXXXXX # API key or token used for authenticating with the OTel collector (e.g., SigNoz)
177+
OTEL_LOGGER_NAME='credebl-platform-logger' # Name of the logger used for OpenTelemetry log records
178+
HOSTNAME='localhost' # Hostname or unique identifier for the service instance

apps/api-gateway/src/main.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { otelSDK } from './tracer';
12
import * as dotenv from 'dotenv';
23
import * as express from 'express';
34

@@ -7,7 +8,7 @@ import { Logger, VERSION_NEUTRAL, VersioningType } from '@nestjs/common';
78
import { AppModule } from './app.module';
89
import { HttpAdapterHost, NestFactory, Reflector } from '@nestjs/core';
910
import { AllExceptionsFilter } from '@credebl/common/exception-handler';
10-
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
11+
import { type MicroserviceOptions, Transport } from '@nestjs/microservices';
1112
import { getNatsOptions } from '@credebl/common/nats.config';
1213

1314
import helmet from 'helmet';
@@ -18,6 +19,19 @@ import { UpdatableValidationPipe } from '@credebl/common/custom-overrideable-val
1819
dotenv.config();
1920

2021
async function bootstrap(): Promise<void> {
22+
try {
23+
if (otelSDK) {
24+
await otelSDK.start();
25+
// eslint-disable-next-line no-console
26+
console.log('OpenTelemetry SDK started successfully');
27+
} else {
28+
// eslint-disable-next-line no-console
29+
console.log('OpenTelemetry SDK disabled for this environment');
30+
}
31+
} catch (error) {
32+
// eslint-disable-next-line no-console
33+
console.error('Failed to start OpenTelemetry SDK:', error);
34+
}
2135
const app = await NestFactory.create(AppModule);
2236

2337
app.useLogger(app.get(NestjsLoggerServiceAdapter));
@@ -32,7 +46,7 @@ async function bootstrap(): Promise<void> {
3246
app.use(express.json({ limit: '100mb' }));
3347
app.use(express.urlencoded({ limit: '100mb', extended: true }));
3448

35-
app.use(function (req, res, next) {
49+
app.use((req, res, next) => {
3650
let err = null;
3751
try {
3852
decodeURIComponent(req.path);

apps/api-gateway/src/tracer.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-nocheck TODO: Facing issues with types, need to fix later
3+
// tracer.ts
4+
import * as dotenv from 'dotenv';
5+
dotenv.config();
6+
7+
import { NodeSDK } from '@opentelemetry/sdk-node';
8+
import * as process from 'process';
9+
10+
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
11+
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
12+
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
13+
14+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
15+
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
16+
17+
import { resourceFromAttributes } from '@opentelemetry/resources';
18+
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
19+
20+
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
21+
import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api';
22+
import type { Logger } from '@opentelemetry/api-logs';
23+
24+
let otelSDK: NodeSDK | null = null;
25+
let otelLogger: Logger | null = null;
26+
let otelLoggerProviderInstance: LoggerProvider | null = null;
27+
if ('true' === process.env.IS_ENABLE_OTEL) {
28+
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
29+
30+
const resource = resourceFromAttributes({
31+
[SemanticResourceAttributes.SERVICE_NAME]: process.env.OTEL_SERVICE_NAME,
32+
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.OTEL_SERVICE_VERSION,
33+
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: process.env.HOSTNAME
34+
});
35+
36+
const traceExporter = new OTLPTraceExporter({
37+
url: process.env.OTEL_TRACES_OTLP_ENDPOINT,
38+
headers: {
39+
Authorization: `Api-Key ${process.env.OTEL_HEADERS_KEY}`
40+
}
41+
});
42+
43+
const logExporter = new OTLPLogExporter({
44+
url: process.env.OTEL_LOGS_OTLP_ENDPOINT,
45+
headers: {
46+
Authorization: `Api-Key ${process.env.OTEL_HEADERS_KEY}`
47+
}
48+
});
49+
50+
const logProvider = new LoggerProvider({ resource });
51+
logProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
52+
otelLogger = logProvider.getLogger(process.env.OTEL_LOGGER_NAME);
53+
otelLoggerProviderInstance = logProvider;
54+
55+
otelSDK = new NodeSDK({
56+
traceExporter,
57+
resource,
58+
instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation(), new NestInstrumentation()]
59+
});
60+
61+
process.on('SIGTERM', () => {
62+
Promise.all([otelSDK!.shutdown(), logProvider.shutdown()])
63+
// eslint-disable-next-line no-console
64+
.then(() => console.log('SDK and Logger shut down successfully'))
65+
// eslint-disable-next-line no-console
66+
.catch((err) => console.log('Error during shutdown', err))
67+
.finally(() => process.exit(0));
68+
});
69+
}
70+
71+
export { otelSDK, otelLogger, otelLoggerProviderInstance };

libs/logger/src/logger.service.ts

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { Inject, Injectable, Scope } from '@nestjs/common';
22
import { INQUIRER } from '@nestjs/core';
3-
import Logger, {
4-
LoggerBaseKey
5-
} from '@credebl/logger/logger.interface';
3+
import Logger, { LoggerBaseKey } from '@credebl/logger/logger.interface';
64
import { LogData, LogLevel } from '@credebl/logger/log';
75
import { ConfigService } from '@nestjs/config';
8-
import ContextStorageService, {
9-
ContextStorageServiceKey
10-
} from '@credebl/context/contextStorageService.interface';
6+
import ContextStorageService, { ContextStorageServiceKey } from '@credebl/context/contextStorageService.interface';
117
import { MICRO_SERVICE_NAME } from '@credebl/common/common.constant';
8+
import { otelLogger } from '../../../apps/api-gateway/src/tracer';
129

1310
@Injectable({ scope: Scope.TRANSIENT })
1411
export default class LoggerService implements Logger {
@@ -25,45 +22,92 @@ export default class LoggerService implements Logger {
2522
private readonly contextStorageService: ContextStorageService,
2623
@Inject(MICRO_SERVICE_NAME) private readonly microserviceName: string
2724
) {
28-
// Set the source class from the parent class
2925
this.sourceClass = parentClass?.constructor?.name;
30-
// Set the organization, context and app from the environment variables
3126
this.organization = configService.get<string>('ORGANIZATION');
3227
this.context = configService.get<string>('CONTEXT');
3328
this.app = configService.get<string>('APP');
3429
}
3530

36-
public log(
37-
level: LogLevel,
38-
message: string | Error,
39-
data?: LogData,
40-
profile?: string,
41-
): void {
42-
return this.logger.log(level, message, this.getLogData(data), profile);
31+
public log(level: LogLevel, message: string | Error, data?: LogData, profile?: string): void {
32+
this.emitToOtel(level, message, data);
33+
this.logger.log(level, message, this.getLogData(data), profile);
4334
}
4435

45-
public debug(message: string, data?: LogData, profile?: string) : void {
46-
return this.logger.debug(message, this.getLogData(data), profile);
36+
public debug(message: string, data?: LogData, profile?: string): void {
37+
this.emitToOtel('DEBUG', message, data);
38+
this.logger.debug(message, this.getLogData(data), profile);
4739
}
4840

49-
public info(message: string, data?: LogData, profile?: string) : void {
50-
return this.logger.info(message, this.getLogData(data), profile);
41+
public info(message: string, data?: LogData, profile?: string): void {
42+
this.emitToOtel('INFO', message, data);
43+
this.logger.info(message, this.getLogData(data), profile);
5144
}
5245

53-
public warn(message: string | Error, data?: LogData, profile?: string) : void {
54-
return this.logger.warn(message, this.getLogData(data), profile);
46+
public warn(message: string | Error, data?: LogData, profile?: string): void {
47+
this.emitToOtel('WARN', message, data);
48+
this.logger.warn(message, this.getLogData(data), profile);
5549
}
5650

57-
public error(message: string | Error, data?: LogData, profile?: string) : void {
58-
return this.logger.error(message, this.getLogData(data), profile);
51+
public error(message: string | Error, data?: LogData, profile?: string): void {
52+
this.emitToOtel('ERROR', message, data);
53+
this.logger.error(message, this.getLogData(data), profile);
5954
}
6055

61-
public fatal(message: string | Error, data?: LogData, profile?: string) : void {
62-
return this.logger.fatal(message, this.getLogData(data), profile);
56+
public fatal(message: string | Error, data?: LogData, profile?: string): void {
57+
this.emitToOtel('FATAL', message, data);
58+
this.logger.fatal(message, this.getLogData(data), profile);
6359
}
6460

65-
public emergency(message: string | Error, data?: LogData, profile?: string) : void {
66-
return this.logger.emergency(message, this.getLogData(data), profile);
61+
public emergency(message: string | Error, data?: LogData, profile?: string): void {
62+
this.emitToOtel('EMERGENCY', message, data);
63+
this.logger.emergency(message, this.getLogData(data), profile);
64+
}
65+
66+
public startProfile(id: string): void {
67+
this.logger.startProfile(id);
68+
}
69+
70+
private emitToOtel(severityText: string, message: string | Error, data?: LogData): void {
71+
try {
72+
if (!otelLogger) {
73+
return;
74+
}
75+
const correlationId = data?.correlationId || this.contextStorageService.getContextId();
76+
77+
const attributes = {
78+
app: data?.app || this.app,
79+
organization: data?.organization || this.organization,
80+
context: data?.context || this.context,
81+
sourceClass: data?.sourceClass || this.sourceClass,
82+
correlationId,
83+
microservice: this.microserviceName,
84+
...(data ?? {})
85+
};
86+
87+
if (data?.error) {
88+
attributes.error =
89+
'string' === typeof data.error
90+
? data.error
91+
: data.error instanceof Error
92+
? {
93+
name: data.error.name,
94+
message: data.error.message,
95+
stack: data.error.stack
96+
}
97+
: 'object' === typeof data.error
98+
? JSON.parse(JSON.stringify(data.error))
99+
: String(data.error);
100+
}
101+
102+
otelLogger.emit({
103+
body: `${correlationId} ${'string' === typeof message ? message : message.message}`,
104+
severityText,
105+
attributes
106+
});
107+
} catch (err) {
108+
// eslint-disable-next-line no-console
109+
console.error('Failed to emit log to OpenTelemetry:', err);
110+
}
67111
}
68112

69113
private getLogData(data?: LogData): LogData {
@@ -73,12 +117,7 @@ export default class LoggerService implements Logger {
73117
context: data?.context || this.context,
74118
app: data?.app || this.app,
75119
sourceClass: data?.sourceClass || this.sourceClass,
76-
correlationId:
77-
data?.correlationId || this.contextStorageService.getContextId()
120+
correlationId: data?.correlationId || this.contextStorageService.getContextId()
78121
};
79122
}
80-
81-
public startProfile(id: string) : void {
82-
this.logger.startProfile(id);
83-
}
84123
}

libs/logger/src/logging.interceptor.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,39 @@ import Logger, { LoggerKey } from './logger.interface';
77
import { ClsService } from 'nestjs-cls';
88
import { v4 } from 'uuid';
99

10-
const isNullUndefinedOrEmpty = (obj: any): boolean => obj === null || obj === undefined || (typeof obj === 'object' && Object.keys(obj).length === 0);
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
const isNullUndefinedOrEmpty = (obj: any): boolean =>
12+
null === obj || obj === undefined || ('object' === typeof obj && 0 === Object.keys(obj).length);
13+
1114
@Injectable()
1215
export class LoggingInterceptor implements NestInterceptor {
1316
constructor(
1417
private readonly clsService: ClsService,
1518
@Inject(ContextStorageServiceKey)
16-
private readonly contextStorageService: ContextStorageService,
17-
@Inject(LoggerKey) private readonly _logger: Logger,
19+
private readonly contextStorageService: ContextStorageService,
20+
@Inject(LoggerKey) private readonly _logger: Logger
1821
) {}
1922
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2023
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
21-
return this.clsService.run(() => {
22-
23-
this._logger.info('In LoggingInterceptor configuration');
24-
const rpcContext = context.switchToRpc().getContext();
25-
const headers = rpcContext.getHeaders();
26-
27-
if (!isNullUndefinedOrEmpty(headers)) {
28-
this.contextStorageService.set('x-correlation-id', headers._description);
29-
this.contextStorageService.setContextId(headers._description);
30-
} else {
31-
const newContextId = v4();
32-
this.contextStorageService.set('x-correlation-id', newContextId);
33-
this.contextStorageService.setContextId(newContextId);
34-
}
35-
36-
return next.handle().pipe(
37-
catchError((err) => {
38-
this._logger.error(err);
39-
return throwError(() => err);
40-
})
41-
);
42-
43-
});
24+
return this.clsService.run(() => {
25+
this._logger.info('In LoggingInterceptor configuration');
26+
const rpcContext = context.switchToRpc().getContext();
27+
const headers = rpcContext.getHeaders();
4428

29+
if (!isNullUndefinedOrEmpty(headers) && headers._description) {
30+
this.contextStorageService.set('x-correlation-id', headers._description);
31+
this.contextStorageService.setContextId(headers._description);
32+
} else {
33+
const newContextId = v4();
34+
this.contextStorageService.set('x-correlation-id', newContextId);
35+
this.contextStorageService.setContextId(newContextId);
36+
}
37+
return next.handle().pipe(
38+
catchError((err) => {
39+
this._logger.error(err);
40+
return throwError(() => err);
41+
})
42+
);
43+
});
4544
}
46-
4745
}

0 commit comments

Comments
 (0)