Skip to content

Commit 1ddea33

Browse files
Merge pull request #260 from Luchistack/Develop-comprehensive-unit-and-integration-test-coverage-for-the-pull-payment-cron-scheduler
Add OpenTelemetry integration and tracing support
2 parents 36feb76 + 62616e7 commit 1ddea33

6 files changed

Lines changed: 149 additions & 16 deletions

File tree

‎.env.example‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
PORT=3000
2+
OTEL_DISABLED=false
3+
OTEL_SERVICE_NAME=substream-protocol-backend
4+
OTEL_SERVICE_VERSION=1.0.0
5+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
6+
OTEL_EXPORTER_OTLP_HEADERS=
7+
OTEL_IGNORE_PATHS=/health,/metrics
8+
OTEL_DIAG_LEVEL=error
29

310
# Creator auth configuration
411
CREATOR_AUTH_SECRET=replace-with-a-long-random-secret

‎index.js‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
const express = require('express');
2-
const cors = require('cors');
31
const dotenv = require('dotenv');
4-
const promClient = require('prom-client');
2+
const { initTracing } = require('./src/utils/opentelemetry');
53

64
dotenv.config();
7-
require('dotenv').config();
8-
dotenv.config();
5+
initTracing({ serviceName: 'substream-protocol-backend', serviceVersion: '1.0.0' });
6+
7+
const express = require('express');
8+
const cors = require('cors');
9+
const promClient = require('prom-client');
910

1011
// Initialize Prometheus metrics
1112
const register = new promClient.Registry();
@@ -179,6 +180,9 @@ async function createApp(dependencies = {}) {
179180
app.use(express.json({ limit: '10mb' }));
180181
app.use(express.urlencoded({ extended: true }));
181182

183+
// Request tracing middleware must be registered early for trace propagation
184+
app.use(requestTracingMiddleware);
185+
182186
// Add request start time for accurate monitoring
183187
app.use(addRequestStartTime);
184188

‎package.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,12 @@
9595
"uuid": "^14.0.0",
9696
"web3.storage": "^4.5.4",
9797
"winston": "^3.19.0",
98-
"xlsx": "^0.18.5"
98+
"xlsx": "^0.18.5",
99+
"@opentelemetry/api": "^1.18.0",
100+
"@opentelemetry/sdk-node": "^0.66.0",
101+
"@opentelemetry/auto-instrumentations-node": "^0.66.0",
102+
"@opentelemetry/exporter-trace-otlp-http": "^0.66.0",
103+
"@opentelemetry/sdk-trace-base": "^0.66.0"
99104
},
100105
"devDependencies": {
101106
"@nestjs/cli": "^11.0.21",

‎src/utils/logger.js‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
const winston = require('winston');
1212
const { v4: uuidv4 } = require('uuid');
13+
const { trace, context } = require('@opentelemetry/api');
1314

1415
// Custom format for structured logging
1516
const { combine, timestamp, json, errors, splat } = winston.format;
@@ -75,32 +76,41 @@ function createLoggerContext(traceId, additionalContext = {}) {
7576
* Middleware to add trace ID to all requests
7677
*/
7778
function requestTracingMiddleware(req, res, next) {
78-
// Get or create trace ID
79-
const traceId = req.headers['x-trace-id'] || uuidv4();
80-
81-
// Add trace ID to response headers
79+
const activeSpan = trace.getSpan(context.active());
80+
const spanContext = activeSpan?.spanContext();
81+
const traceId = req.headers['x-trace-id'] || spanContext?.traceId || uuidv4();
82+
const spanId = spanContext?.spanId;
83+
84+
// Add trace headers for downstream correlation
8285
res.setHeader('x-trace-id', traceId);
83-
84-
// Attach logger to request with context
86+
if (spanId) {
87+
res.setHeader('x-span-id', spanId);
88+
}
89+
8590
req.logger = logger.child({
8691
traceId,
92+
spanId,
8793
method: req.method,
8894
path: req.path,
8995
ip: req.ip,
9096
userAgent: req.headers['user-agent'],
9197
});
92-
98+
99+
// Preserve trace fields for downstream error handling
100+
req.logger.fields = { traceId, spanId };
101+
93102
// Track request timing
94103
const startTime = Date.now();
95-
104+
96105
res.on('finish', () => {
97106
const duration = Date.now() - startTime;
98107
const context = {
99108
traceId,
109+
spanId,
100110
statusCode: res.statusCode,
101111
duration: `${duration}ms`,
102112
};
103-
113+
104114
if (res.statusCode >= 500) {
105115
logger.error('Request failed', context);
106116
} else if (res.statusCode >= 400) {
@@ -109,7 +119,7 @@ function requestTracingMiddleware(req, res, next) {
109119
logger.info('Request completed', context);
110120
}
111121
});
112-
122+
113123
next();
114124
}
115125

‎src/utils/opentelemetry.js‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const { NodeSDK } = require('@opentelemetry/sdk-node');
2+
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
3+
const { Resource } = require('@opentelemetry/resources');
4+
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
5+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
6+
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-base');
7+
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
8+
9+
function parseOtlpHeaders(headerValue) {
10+
if (!headerValue || typeof headerValue !== 'string') {
11+
return undefined;
12+
}
13+
14+
return Object.fromEntries(
15+
headerValue
16+
.split(',')
17+
.map((pair) => pair.trim())
18+
.filter(Boolean)
19+
.map((pair) => pair.split('=').map((value) => value.trim()))
20+
.filter((tuple) => tuple.length === 2)
21+
);
22+
}
23+
24+
function createTraceExporter() {
25+
const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
26+
const headers = parseOtlpHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS);
27+
28+
if (endpoint) {
29+
return new OTLPTraceExporter({ url: endpoint, headers });
30+
}
31+
32+
return new ConsoleSpanExporter();
33+
}
34+
35+
function getDiagLevel() {
36+
const level = (process.env.OTEL_DIAG_LEVEL || 'error').toUpperCase();
37+
return DiagLogLevel[level] || DiagLogLevel.ERROR;
38+
}
39+
40+
function initTracing(options = {}) {
41+
if (process.env.OTEL_DISABLED === 'true' || global.__substreamOtelInitialized) {
42+
return;
43+
}
44+
45+
diag.setLogger(new DiagConsoleLogger(), getDiagLevel());
46+
47+
const serviceName = process.env.OTEL_SERVICE_NAME || options.serviceName || 'substream-protocol-backend';
48+
const serviceVersion = process.env.OTEL_SERVICE_VERSION || options.serviceVersion || '1.0.0';
49+
50+
const sdk = new NodeSDK({
51+
resource: new Resource({
52+
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
53+
[SemanticResourceAttributes.SERVICE_VERSION]: serviceVersion,
54+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
55+
}),
56+
traceExporter: createTraceExporter(),
57+
instrumentations: [
58+
getNodeAutoInstrumentations({
59+
'@opentelemetry/instrumentation-http': {
60+
ignoreIncomingPaths: process.env.OTEL_IGNORE_PATHS ? process.env.OTEL_IGNORE_PATHS.split(',') : ['/health', '/metrics'],
61+
},
62+
'@opentelemetry/instrumentation-express': {
63+
requestHook: (span, info) => {
64+
if (span && info) {
65+
span.setAttribute('http.route', info.route || info.request?.url || '/');
66+
}
67+
},
68+
},
69+
}),
70+
],
71+
});
72+
73+
sdk.start()
74+
.then(() => {
75+
console.log('[Tracing] OpenTelemetry initialized', {
76+
serviceName,
77+
serviceVersion,
78+
});
79+
})
80+
.catch((error) => {
81+
console.error('[Tracing] Failed to initialize OpenTelemetry', error);
82+
});
83+
84+
const shutdown = async () => {
85+
try {
86+
await sdk.shutdown();
87+
console.log('[Tracing] OpenTelemetry shutdown complete');
88+
} catch (error) {
89+
console.error('[Tracing] OpenTelemetry shutdown failed', error);
90+
}
91+
};
92+
93+
process.on('SIGTERM', shutdown);
94+
process.on('SIGINT', shutdown);
95+
96+
global.__substreamOtelInitialized = true;
97+
}
98+
99+
module.exports = {
100+
initTracing,
101+
};

‎worker.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
#!/usr/bin/env node
22

3+
const dotenv = require('dotenv');
4+
const { initTracing } = require('./src/utils/opentelemetry');
5+
6+
dotenv.config();
7+
initTracing({ serviceName: 'substream-protocol-backend-worker', serviceVersion: '1.0.0' });
8+
39
const { loadConfig } = require('./src/config');
410
const { BackgroundWorkerService } = require('./src/services/backgroundWorkerService');
511
const { SorobanIndexerWorker } = require('./src/services/sorobanIndexerWorker');

0 commit comments

Comments
 (0)