azify-logger 1.0.40 → 1.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/index.js +1 -0
- package/init.js +1 -0
- package/middleware-express.js +1 -0
- package/middleware-fastify.js +134 -86
- package/middleware-restify.js +172 -48
- package/otel-env.js +42 -0
- package/package.json +5 -2
- package/queue/redisQueue.js +2 -3
- package/register-otel.js +15 -31
- package/scripts/redis-worker.js +2 -3
- package/server.js +24 -26
- package/streams/httpQueue.js +5 -7
- package/trace-export.js +85 -0
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ await fastify.listen({ port: 3000 })
|
|
|
73
73
|
| `APP_NAME` | - | Nome da aplicação |
|
|
74
74
|
| `AZIFY_LOGGER_URL` | `http://localhost:3001/log` | URL do logger |
|
|
75
75
|
| `AZIFY_LOGGER_REDIS_URL` | `redis://localhost:6381` | URL do Redis (fila de logs) |
|
|
76
|
-
| `AZIFY_LOGGER_REDIS_PASSWORD` | — | **Obrigatório
|
|
76
|
+
| `AZIFY_LOGGER_REDIS_PASSWORD` | — | **Obrigatório em todos os ambientes.** Sem senha, a app continua (usa HTTP direto) e a mensagem de aviso é exibida. |
|
|
77
77
|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318/v1/traces` | Endpoint OTLP para traces (opcional) |
|
|
78
78
|
| `NODE_ENV` | `development` | Ambiente |
|
|
79
79
|
|
package/index.js
CHANGED
package/init.js
CHANGED
package/middleware-express.js
CHANGED
package/middleware-fastify.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
require('./otel-env')
|
|
1
2
|
const { startRequestContext, runWithRequestContext, getRequestContext } = require('./store')
|
|
3
|
+
const { sendSpanToOtel, setEndpointOverride } = require('./trace-export')
|
|
2
4
|
const os = require('os')
|
|
3
5
|
|
|
4
|
-
let trace, otelContext
|
|
6
|
+
let trace, otelContext, tracer
|
|
5
7
|
try {
|
|
6
8
|
const otelApi = require('@opentelemetry/api')
|
|
7
9
|
trace = otelApi.trace
|
|
8
10
|
otelContext = otelApi.context
|
|
11
|
+
tracer = trace.getTracer('azify-logger', '1.0.0')
|
|
9
12
|
} catch (_) {
|
|
10
13
|
trace = { getSpan: () => null }
|
|
11
14
|
otelContext = { active: () => ({}) }
|
|
15
|
+
tracer = null
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
function fastUUID() {
|
|
@@ -75,8 +79,17 @@ function pickHeaders(source) {
|
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
function createFastifyLoggingPlugin(options = {}) {
|
|
82
|
+
const svcName = options.serviceName || process.env.APP_NAME
|
|
83
|
+
if (svcName) {
|
|
84
|
+
process.env.OTEL_SERVICE_NAME = svcName
|
|
85
|
+
process.env.SERVICE_NAME = svcName
|
|
86
|
+
}
|
|
87
|
+
const otelEndpoint = options.otelEndpoint || options.otelExporterEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|
|
88
|
+
if (otelEndpoint) {
|
|
89
|
+
setEndpointOverride(otelEndpoint)
|
|
90
|
+
}
|
|
78
91
|
const config = {
|
|
79
|
-
serviceName:
|
|
92
|
+
serviceName: svcName,
|
|
80
93
|
loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
|
|
81
94
|
environment: options.environment || process.env.NODE_ENV,
|
|
82
95
|
captureResponseBody: options.captureResponseBody !== false && process.env.AZIFY_LOGGER_CAPTURE_RESPONSE_BODY !== 'false',
|
|
@@ -101,49 +114,83 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
101
114
|
} catch (_) {}
|
|
102
115
|
}
|
|
103
116
|
|
|
117
|
+
function toTraceId(sc) {
|
|
118
|
+
if (!sc?.traceId || !sc?.spanId) return null
|
|
119
|
+
const traceHex = (sc.traceId || '').replace(/-/g, '').padStart(32, '0').slice(0, 32)
|
|
120
|
+
return {
|
|
121
|
+
traceId: traceHex.length === 32 ? `${traceHex.substring(0, 8)}-${traceHex.substring(8, 12)}-${traceHex.substring(12, 16)}-${traceHex.substring(16, 20)}-${traceHex.substring(20, 32)}` : sc.traceId,
|
|
122
|
+
traceIdHex: traceHex,
|
|
123
|
+
spanId: sc.spanId,
|
|
124
|
+
parentSpanId: null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
return async function azifyFastifyPlugin(fastify, opts) {
|
|
105
129
|
fastify.addHook('onRequest', async (request, reply) => {
|
|
106
130
|
let reqCtx = null
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
let libSpan = null
|
|
132
|
+
let libTraceId = null
|
|
133
|
+
let libSpanId = null
|
|
134
|
+
|
|
135
|
+
let libTraceIdHex = null
|
|
136
|
+
if (tracer) {
|
|
109
137
|
try {
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
traceId: traceHex.length === 32 ? `${traceHex.substring(0, 8)}-${traceHex.substring(8, 12)}-${traceHex.substring(12, 16)}-${traceHex.substring(16, 20)}-${traceHex.substring(20, 32)}` : spanContext.traceId,
|
|
118
|
-
spanId: spanContext.spanId,
|
|
119
|
-
parentSpanId: null
|
|
120
|
-
}
|
|
138
|
+
const parentCtx = otelContext.active()
|
|
139
|
+
const existingSpan = trace.getSpan(parentCtx) || (request.opentelemetry && typeof request.opentelemetry === 'function' ? request.opentelemetry()?.span : null)
|
|
140
|
+
libSpan = tracer.startSpan('azify-logger.request', {
|
|
141
|
+
attributes: {
|
|
142
|
+
'http.method': request.method,
|
|
143
|
+
'http.url': request.url
|
|
121
144
|
}
|
|
145
|
+
}, existingSpan ? trace.setSpan(parentCtx, existingSpan) : parentCtx)
|
|
146
|
+
const tid = toTraceId(libSpan.spanContext())
|
|
147
|
+
if (tid) {
|
|
148
|
+
libTraceId = tid.traceId
|
|
149
|
+
libTraceIdHex = tid.traceIdHex
|
|
150
|
+
libSpanId = tid.spanId
|
|
122
151
|
}
|
|
123
152
|
} catch (_) {}
|
|
124
|
-
return null
|
|
125
153
|
}
|
|
126
|
-
|
|
154
|
+
if (!libTraceId) {
|
|
155
|
+
if (request.opentelemetry && typeof request.opentelemetry === 'function') {
|
|
156
|
+
try {
|
|
157
|
+
const o = request.opentelemetry()
|
|
158
|
+
const tid = o?.span ? toTraceId(o.span.spanContext()) : null
|
|
159
|
+
if (tid) {
|
|
160
|
+
libTraceId = tid.traceId
|
|
161
|
+
libTraceIdHex = tid.traceIdHex
|
|
162
|
+
libSpanId = tid.spanId
|
|
163
|
+
}
|
|
164
|
+
} catch (_) {}
|
|
165
|
+
}
|
|
166
|
+
if (!libTraceId) {
|
|
167
|
+
try {
|
|
168
|
+
const span = trace.getSpan(otelContext.active())
|
|
169
|
+
const tid = span ? toTraceId(span.spanContext()) : null
|
|
170
|
+
if (tid) {
|
|
171
|
+
libTraceId = tid.traceId
|
|
172
|
+
libTraceIdHex = tid.traceIdHex
|
|
173
|
+
libSpanId = tid.spanId
|
|
174
|
+
}
|
|
175
|
+
} catch (_) {}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
127
179
|
function ensureRequestContext() {
|
|
128
180
|
if (reqCtx) return reqCtx
|
|
129
|
-
|
|
130
|
-
const otelCtx = getOtelTraceContext()
|
|
131
|
-
const traceHex = otelCtx ? otelCtx.traceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(request.headers['x-trace-id'])
|
|
132
|
-
|
|
181
|
+
const traceHex = libTraceId ? libTraceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(request.headers['x-trace-id'])
|
|
133
182
|
reqCtx = startRequestContext({
|
|
134
183
|
requestId: request.requestId || fastUUID(),
|
|
135
184
|
traceHex: traceHex || undefined,
|
|
136
|
-
parentSpanId:
|
|
185
|
+
parentSpanId: request.headers['x-parent-span-id'] || null
|
|
137
186
|
})
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
reqCtx.
|
|
141
|
-
reqCtx.spanId = otelCtx.spanId
|
|
187
|
+
if (libTraceId && libSpanId) {
|
|
188
|
+
reqCtx.traceId = libTraceId
|
|
189
|
+
reqCtx.spanId = libSpanId
|
|
142
190
|
}
|
|
143
|
-
|
|
144
191
|
return reqCtx
|
|
145
192
|
}
|
|
146
|
-
|
|
193
|
+
|
|
147
194
|
const ctx = ensureRequestContext()
|
|
148
195
|
|
|
149
196
|
runWithRequestContext(ctx, () => {})
|
|
@@ -161,12 +208,10 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
161
208
|
let headersCached = false
|
|
162
209
|
|
|
163
210
|
const logWithContext = (level, message, meta) => {
|
|
164
|
-
const otelCtx = getOtelTraceContext()
|
|
165
211
|
const ctx = getRequestContext() || ensureRequestContext()
|
|
166
|
-
|
|
167
|
-
meta.
|
|
168
|
-
meta.
|
|
169
|
-
meta.parentSpanId = otelCtx?.parentSpanId || meta.parentSpanId || ctx.parentSpanId
|
|
212
|
+
meta.traceId = libTraceId || meta.traceId || ctx.traceId
|
|
213
|
+
meta.spanId = libSpanId || meta.spanId || ctx.spanId
|
|
214
|
+
meta.parentSpanId = ctx.parentSpanId || null
|
|
170
215
|
sendLog(level, message, meta)
|
|
171
216
|
}
|
|
172
217
|
|
|
@@ -178,16 +223,9 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
178
223
|
|
|
179
224
|
const ctx = ensureRequestContext()
|
|
180
225
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
spanId = otelCtx.spanId
|
|
185
|
-
parentSpanId = otelCtx.parentSpanId
|
|
186
|
-
} else {
|
|
187
|
-
traceId = ctx.traceId
|
|
188
|
-
spanId = ctx.spanId
|
|
189
|
-
parentSpanId = ctx.parentSpanId || request.headers['x-parent-span-id'] || null
|
|
190
|
-
}
|
|
226
|
+
traceId = libTraceId || ctx.traceId
|
|
227
|
+
spanId = libSpanId || ctx.spanId
|
|
228
|
+
parentSpanId = ctx.parentSpanId || request.headers['x-parent-span-id'] || null
|
|
191
229
|
|
|
192
230
|
requestId = ctx.requestId || request.requestId || fastUUID()
|
|
193
231
|
clientIp = request.ip || request.socket?.remoteAddress || 'unknown'
|
|
@@ -225,6 +263,7 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
225
263
|
logSent: false,
|
|
226
264
|
requestId: null,
|
|
227
265
|
traceId: null,
|
|
266
|
+
traceIdHex: libTraceIdHex,
|
|
228
267
|
spanId: null,
|
|
229
268
|
parentSpanId: null,
|
|
230
269
|
clientIp: null,
|
|
@@ -232,7 +271,10 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
232
271
|
cachedHeaders: null,
|
|
233
272
|
idsCreated: false,
|
|
234
273
|
headersCached: false,
|
|
235
|
-
reqCtx: null
|
|
274
|
+
reqCtx: null,
|
|
275
|
+
libSpan,
|
|
276
|
+
libTraceId,
|
|
277
|
+
libSpanId
|
|
236
278
|
}
|
|
237
279
|
|
|
238
280
|
if (config.logRequest) {
|
|
@@ -253,13 +295,13 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
253
295
|
requestObj.body = safeSerializeBody(request.body)
|
|
254
296
|
}
|
|
255
297
|
|
|
256
|
-
const otelCtx = getOtelTraceContext()
|
|
257
298
|
const ctx = getRequestContext() || ensureRequestContext()
|
|
258
299
|
|
|
259
300
|
const meta = {
|
|
260
|
-
traceId:
|
|
261
|
-
|
|
262
|
-
|
|
301
|
+
traceId: libTraceId || ctx.traceId,
|
|
302
|
+
traceIdHex: libTraceIdHex || (ctx.traceId ? ctx.traceId.replace(/-/g, '').slice(0, 32) : null),
|
|
303
|
+
spanId: libSpanId || ctx.spanId,
|
|
304
|
+
parentSpanId: ctx.parentSpanId || null,
|
|
263
305
|
requestId,
|
|
264
306
|
request: requestObj,
|
|
265
307
|
timestamp: Date.now(),
|
|
@@ -279,6 +321,13 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
279
321
|
|
|
280
322
|
logger.logSent = true
|
|
281
323
|
|
|
324
|
+
if (logger.libSpan) {
|
|
325
|
+
try {
|
|
326
|
+
logger.libSpan.setAttribute('http.status_code', reply.statusCode || 200)
|
|
327
|
+
logger.libSpan.end()
|
|
328
|
+
} catch (_) {}
|
|
329
|
+
}
|
|
330
|
+
|
|
282
331
|
if (config.captureResponseBody && payload != null && !logger.responseChunkCaptured) {
|
|
283
332
|
logger.responseChunk = payload
|
|
284
333
|
logger.responseChunkCaptured = true
|
|
@@ -288,58 +337,41 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
288
337
|
const method = logger.method
|
|
289
338
|
const url = logger.url
|
|
290
339
|
const path = logger.path
|
|
340
|
+
const capturedTraceId = logger.libTraceId || null
|
|
341
|
+
const capturedTraceIdHex = logger.traceIdHex || (capturedTraceId ? capturedTraceId.replace(/-/g, '').slice(0, 32) : null)
|
|
342
|
+
const capturedSpanId = logger.libSpanId || null
|
|
343
|
+
const capturedParentSpanId = null
|
|
291
344
|
|
|
292
345
|
let reqCtx = null
|
|
293
346
|
let requestId, traceId, spanId, parentSpanId, clientIp, query, cachedHeaders
|
|
294
347
|
let idsCreated = false
|
|
295
348
|
let headersCached = false
|
|
296
349
|
|
|
297
|
-
function getOtelTraceContext() {
|
|
298
|
-
try {
|
|
299
|
-
const activeContext = otelContext.active()
|
|
300
|
-
const span = trace.getSpan(activeContext)
|
|
301
|
-
if (span) {
|
|
302
|
-
const spanContext = span.spanContext()
|
|
303
|
-
if (spanContext && spanContext.traceId && spanContext.spanId) {
|
|
304
|
-
const traceHex = spanContext.traceId.replace(/-/g, '')
|
|
305
|
-
return {
|
|
306
|
-
traceId: traceHex.length === 32 ? `${traceHex.substring(0, 8)}-${traceHex.substring(8, 12)}-${traceHex.substring(12, 16)}-${traceHex.substring(16, 20)}-${traceHex.substring(20, 32)}` : spanContext.traceId,
|
|
307
|
-
spanId: spanContext.spanId,
|
|
308
|
-
parentSpanId: null
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
} catch (_) {}
|
|
313
|
-
return null
|
|
314
|
-
}
|
|
315
|
-
|
|
316
350
|
function ensureRequestContext() {
|
|
317
351
|
if (reqCtx) return reqCtx
|
|
318
352
|
|
|
319
|
-
const
|
|
320
|
-
const traceHex = otelCtx ? otelCtx.traceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(request.headers['x-trace-id'])
|
|
353
|
+
const traceHex = capturedTraceId ? capturedTraceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(request.headers['x-trace-id'])
|
|
321
354
|
|
|
322
355
|
reqCtx = startRequestContext({
|
|
323
356
|
requestId: request.requestId || fastUUID(),
|
|
324
357
|
traceHex: traceHex || undefined,
|
|
325
|
-
parentSpanId:
|
|
358
|
+
parentSpanId: capturedParentSpanId || request.headers['x-parent-span-id'] || null
|
|
326
359
|
})
|
|
327
360
|
|
|
328
|
-
if (
|
|
329
|
-
reqCtx.traceId =
|
|
330
|
-
reqCtx.spanId =
|
|
361
|
+
if (capturedTraceId && capturedSpanId) {
|
|
362
|
+
reqCtx.traceId = capturedTraceId
|
|
363
|
+
reqCtx.spanId = capturedSpanId
|
|
331
364
|
}
|
|
332
365
|
|
|
333
366
|
return reqCtx
|
|
334
367
|
}
|
|
335
368
|
|
|
336
369
|
const logWithContext = (level, message, meta) => {
|
|
337
|
-
const otelCtx = getOtelTraceContext()
|
|
338
370
|
const ctx = getRequestContext() || ensureRequestContext()
|
|
339
371
|
|
|
340
|
-
meta.traceId =
|
|
341
|
-
meta.spanId =
|
|
342
|
-
meta.parentSpanId =
|
|
372
|
+
meta.traceId = capturedTraceId || meta.traceId || ctx.traceId
|
|
373
|
+
meta.spanId = capturedSpanId || meta.spanId || ctx.spanId
|
|
374
|
+
meta.parentSpanId = capturedParentSpanId || meta.parentSpanId || ctx.parentSpanId
|
|
343
375
|
sendLog(level, message, meta)
|
|
344
376
|
}
|
|
345
377
|
|
|
@@ -351,11 +383,10 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
351
383
|
|
|
352
384
|
const ctx = ensureRequestContext()
|
|
353
385
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
parentSpanId = otelCtx.parentSpanId
|
|
386
|
+
if (capturedTraceId && capturedSpanId) {
|
|
387
|
+
traceId = capturedTraceId
|
|
388
|
+
spanId = capturedSpanId
|
|
389
|
+
parentSpanId = capturedParentSpanId
|
|
359
390
|
} else {
|
|
360
391
|
traceId = ctx.traceId
|
|
361
392
|
spanId = ctx.spanId
|
|
@@ -408,13 +439,13 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
408
439
|
if (query) requestObj.query = query
|
|
409
440
|
if (config.captureHeaders && cachedHeaders) requestObj.headers = cachedHeaders
|
|
410
441
|
|
|
411
|
-
const otelCtx = getOtelTraceContext()
|
|
412
442
|
const ctx = getRequestContext() || ensureRequestContext()
|
|
413
443
|
|
|
414
444
|
const meta = {
|
|
415
|
-
traceId:
|
|
416
|
-
|
|
417
|
-
|
|
445
|
+
traceId: capturedTraceId || ctx.traceId,
|
|
446
|
+
traceIdHex: capturedTraceIdHex || (ctx.traceId ? ctx.traceId.replace(/-/g, '').slice(0, 32) : null),
|
|
447
|
+
spanId: capturedSpanId || ctx.spanId,
|
|
448
|
+
parentSpanId: capturedParentSpanId != null ? capturedParentSpanId : (ctx.parentSpanId || null),
|
|
418
449
|
requestId,
|
|
419
450
|
request: requestObj,
|
|
420
451
|
response,
|
|
@@ -426,6 +457,23 @@ function createFastifyLoggingPlugin(options = {}) {
|
|
|
426
457
|
|
|
427
458
|
const chunkToProcess = (logger.responseChunk !== null && logger.responseChunkCaptured) ? logger.responseChunk : null
|
|
428
459
|
emitResponseLog(meta, chunkToProcess)
|
|
460
|
+
const traceIdForSpan = meta.traceId
|
|
461
|
+
const spanIdForSpan = meta.spanId
|
|
462
|
+
if (traceIdForSpan && spanIdForSpan && config.serviceName) {
|
|
463
|
+
const startNs = BigInt(logger.startTime) * BigInt(1e6)
|
|
464
|
+
const endNs = BigInt(meta.timestamp || Date.now()) * BigInt(1e6)
|
|
465
|
+
sendSpanToOtel({
|
|
466
|
+
traceId: traceIdForSpan,
|
|
467
|
+
spanId: spanIdForSpan,
|
|
468
|
+
serviceName: config.serviceName,
|
|
469
|
+
name: `request handler - ${logger.path || url}`,
|
|
470
|
+
startTimeNs: startNs,
|
|
471
|
+
endTimeNs: endNs,
|
|
472
|
+
statusCode: reply.statusCode || 200,
|
|
473
|
+
method,
|
|
474
|
+
url
|
|
475
|
+
})
|
|
476
|
+
}
|
|
429
477
|
})
|
|
430
478
|
|
|
431
479
|
return payload
|
package/middleware-restify.js
CHANGED
|
@@ -1,15 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
require('./otel-env')
|
|
2
|
+
const { startRequestContext, runWithRequestContext, getRequestContext, als } = require('./store')
|
|
2
3
|
const { createHttpLoggerTransport } = require('./streams/httpQueue')
|
|
3
4
|
const os = require('os')
|
|
4
5
|
|
|
5
|
-
let trace, otelContext
|
|
6
|
+
let trace, otelContext, otelRootContext
|
|
6
7
|
try {
|
|
7
8
|
const otelApi = require('@opentelemetry/api')
|
|
8
9
|
trace = otelApi.trace
|
|
9
10
|
otelContext = otelApi.context
|
|
11
|
+
otelRootContext = otelApi.ROOT_CONTEXT
|
|
10
12
|
} catch (_) {
|
|
11
13
|
trace = { getSpan: () => null }
|
|
12
|
-
otelContext = { active: () => ({}) }
|
|
14
|
+
otelContext = { active: () => ({}), with: (_ctx, fn) => fn() }
|
|
15
|
+
otelRootContext = null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let emergencySendLog = null
|
|
19
|
+
let unhandledRejectionInstalled = false
|
|
20
|
+
function installUnhandledRejectionHandler () {
|
|
21
|
+
if (unhandledRejectionInstalled) return
|
|
22
|
+
unhandledRejectionInstalled = true
|
|
23
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
24
|
+
try {
|
|
25
|
+
if (emergencySendLog) {
|
|
26
|
+
const err = reason instanceof Error ? reason : new Error(String(reason))
|
|
27
|
+
const meta = {
|
|
28
|
+
error: { message: err.message, name: err.name, stack: err.stack }
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const ctx = getRequestContext()
|
|
32
|
+
if (ctx) {
|
|
33
|
+
meta.requestId = ctx.requestId
|
|
34
|
+
meta.traceId = ctx.traceId
|
|
35
|
+
meta.spanId = ctx.spanId
|
|
36
|
+
meta.parentSpanId = ctx.parentSpanId || null
|
|
37
|
+
}
|
|
38
|
+
} catch (_) {}
|
|
39
|
+
const message = err.message || '[azify-logger] Unhandled rejection'
|
|
40
|
+
emergencySendLog('error', message, meta)
|
|
41
|
+
}
|
|
42
|
+
} catch (_) {}
|
|
43
|
+
})
|
|
13
44
|
}
|
|
14
45
|
|
|
15
46
|
function fastUUID() {
|
|
@@ -47,9 +78,9 @@ function pickHeaders (source) {
|
|
|
47
78
|
for (const key in source) {
|
|
48
79
|
const lower = key.toLowerCase()
|
|
49
80
|
if (!HEADER_WHITELIST.has(lower)) continue
|
|
50
|
-
|
|
81
|
+
|
|
51
82
|
if (!Object.prototype.hasOwnProperty.call(source, key)) continue
|
|
52
|
-
|
|
83
|
+
|
|
53
84
|
const value = source[key]
|
|
54
85
|
if (Array.isArray(value)) {
|
|
55
86
|
result[key] = value.map(String)
|
|
@@ -64,7 +95,8 @@ function createRestifyLoggingMiddleware (options = {}) {
|
|
|
64
95
|
const config = {
|
|
65
96
|
serviceName: options.serviceName || process.env.APP_NAME,
|
|
66
97
|
loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
|
|
67
|
-
environment: options.environment || process.env.NODE_ENV
|
|
98
|
+
environment: options.environment || process.env.NODE_ENV,
|
|
99
|
+
logRequest: options.logRequest !== false && process.env.AZIFY_LOGGER_LOG_REQUEST !== 'false'
|
|
68
100
|
}
|
|
69
101
|
|
|
70
102
|
const transport = createHttpLoggerTransport(config.loggerUrl, {})
|
|
@@ -73,12 +105,12 @@ function createRestifyLoggingMiddleware (options = {}) {
|
|
|
73
105
|
|
|
74
106
|
function sendLog (level, message, meta = {}) {
|
|
75
107
|
if (!transport || typeof transport.enqueue !== 'function') return
|
|
76
|
-
|
|
108
|
+
|
|
77
109
|
if (serviceObj) meta.service = serviceObj
|
|
78
110
|
if (config.environment) meta.environment = config.environment
|
|
79
111
|
meta.timestamp = Date.now()
|
|
80
112
|
meta.hostname = hostname
|
|
81
|
-
|
|
113
|
+
|
|
82
114
|
transport.enqueue({
|
|
83
115
|
level,
|
|
84
116
|
message,
|
|
@@ -86,6 +118,11 @@ function createRestifyLoggingMiddleware (options = {}) {
|
|
|
86
118
|
}, { 'content-type': 'application/json' })
|
|
87
119
|
}
|
|
88
120
|
|
|
121
|
+
if (emergencySendLog === null) {
|
|
122
|
+
emergencySendLog = sendLog
|
|
123
|
+
installUnhandledRejectionHandler()
|
|
124
|
+
}
|
|
125
|
+
|
|
89
126
|
return function azifyLoggingMiddleware (req, res, next) {
|
|
90
127
|
const startTime = Date.now()
|
|
91
128
|
|
|
@@ -112,31 +149,27 @@ function createRestifyLoggingMiddleware (options = {}) {
|
|
|
112
149
|
} catch (_) {}
|
|
113
150
|
return null
|
|
114
151
|
}
|
|
115
|
-
|
|
152
|
+
|
|
116
153
|
function ensureRequestContext() {
|
|
117
154
|
const otelCtx = getOtelTraceContext()
|
|
118
155
|
const traceHex = otelCtx ? otelCtx.traceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(req.headers['x-trace-id'])
|
|
119
|
-
|
|
156
|
+
|
|
120
157
|
const reqCtx = startRequestContext({
|
|
121
158
|
requestId: req.requestId || fastUUID(),
|
|
122
159
|
traceHex: traceHex || undefined,
|
|
123
160
|
parentSpanId: otelCtx?.parentSpanId || req.headers['x-parent-span-id'] || null
|
|
124
161
|
})
|
|
125
|
-
|
|
162
|
+
|
|
126
163
|
if (otelCtx) {
|
|
127
164
|
reqCtx.traceId = otelCtx.traceId
|
|
128
165
|
reqCtx.spanId = otelCtx.spanId
|
|
129
166
|
}
|
|
130
|
-
|
|
167
|
+
|
|
131
168
|
return reqCtx
|
|
132
169
|
}
|
|
133
|
-
|
|
170
|
+
|
|
134
171
|
const reqCtx = ensureRequestContext()
|
|
135
172
|
const requestId = reqCtx.requestId
|
|
136
|
-
|
|
137
|
-
runWithRequestContext(reqCtx, () => {
|
|
138
|
-
next()
|
|
139
|
-
})
|
|
140
173
|
|
|
141
174
|
let normalizedPath = req.url
|
|
142
175
|
if (normalizedPath.includes('?')) {
|
|
@@ -155,6 +188,26 @@ function createRestifyLoggingMiddleware (options = {}) {
|
|
|
155
188
|
ip: req.connection.remoteAddress || req.socket.remoteAddress
|
|
156
189
|
}
|
|
157
190
|
|
|
191
|
+
runWithRequestContext(reqCtx, () => {
|
|
192
|
+
if (config.logRequest) {
|
|
193
|
+
process.nextTick(() => {
|
|
194
|
+
try {
|
|
195
|
+
const meta = {
|
|
196
|
+
traceId: reqCtx.traceId,
|
|
197
|
+
spanId: reqCtx.spanId,
|
|
198
|
+
parentSpanId: reqCtx.parentSpanId || null,
|
|
199
|
+
requestId,
|
|
200
|
+
request: requestSnapshot,
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
hostname
|
|
203
|
+
}
|
|
204
|
+
sendLog('info', `[REQUEST] ${req.method} ${req.url}`, meta)
|
|
205
|
+
} catch (_) {}
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
next()
|
|
209
|
+
})
|
|
210
|
+
|
|
158
211
|
let responseStatus
|
|
159
212
|
const originalStatus = res.status
|
|
160
213
|
if (typeof originalStatus === 'function') {
|
|
@@ -181,56 +234,127 @@ function createRestifyLoggingMiddleware (options = {}) {
|
|
|
181
234
|
|
|
182
235
|
let logSent = false
|
|
183
236
|
|
|
237
|
+
function runOutsideOtelContext (fn) {
|
|
238
|
+
if (otelRootContext != null && typeof otelContext.with === 'function') {
|
|
239
|
+
return otelContext.with(otelRootContext, fn)
|
|
240
|
+
}
|
|
241
|
+
return fn()
|
|
242
|
+
}
|
|
243
|
+
|
|
184
244
|
function emitLog (level, message, extraMeta = {}) {
|
|
185
245
|
if (logSent) return
|
|
186
246
|
logSent = true
|
|
187
|
-
|
|
188
|
-
process.nextTick(() => {
|
|
189
|
-
const duration = Date.now() - startTime
|
|
190
|
-
const statusCode = responseStatus || res.statusCode || 200
|
|
191
|
-
const responseHeaders = typeof res.getHeaders === 'function' ? res.getHeaders() : {}
|
|
192
|
-
|
|
193
|
-
const otelCtx = getOtelTraceContext()
|
|
194
|
-
const ctx = getRequestContext() || reqCtx
|
|
195
247
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
248
|
+
setImmediate(() => {
|
|
249
|
+
runOutsideOtelContext(() => {
|
|
250
|
+
try {
|
|
251
|
+
const duration = Date.now() - startTime
|
|
252
|
+
const statusCode = responseStatus ?? 200
|
|
253
|
+
|
|
254
|
+
const meta = {
|
|
255
|
+
traceId: reqCtx.traceId,
|
|
256
|
+
spanId: reqCtx.spanId,
|
|
257
|
+
parentSpanId: reqCtx.parentSpanId || null,
|
|
258
|
+
requestId,
|
|
259
|
+
request: requestSnapshot,
|
|
260
|
+
response: {
|
|
261
|
+
statusCode,
|
|
262
|
+
headers: {},
|
|
263
|
+
durationMs: duration
|
|
264
|
+
},
|
|
265
|
+
...extraMeta
|
|
266
|
+
}
|
|
209
267
|
|
|
210
|
-
|
|
268
|
+
sendLog(level, message, meta)
|
|
269
|
+
} catch (err) {
|
|
270
|
+
const errPayload = {
|
|
271
|
+
traceId: reqCtx.traceId,
|
|
272
|
+
spanId: reqCtx.spanId,
|
|
273
|
+
parentSpanId: reqCtx.parentSpanId || null,
|
|
274
|
+
requestId,
|
|
275
|
+
request: requestSnapshot,
|
|
276
|
+
error: {
|
|
277
|
+
message: err && err.message ? err.message : String(err),
|
|
278
|
+
name: err && err.name ? err.name : 'Error',
|
|
279
|
+
stack: err && err.stack ? err.stack : undefined
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
setImmediate(() => {
|
|
283
|
+
try {
|
|
284
|
+
sendLog('error', (requestSnapshot && (requestSnapshot.method + ' ' + requestSnapshot.url)) || 'request', errPayload)
|
|
285
|
+
} catch (_) {}
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
})
|
|
211
289
|
})
|
|
212
290
|
}
|
|
213
291
|
|
|
214
292
|
function finalize (level = 'info', errorMeta) {
|
|
215
293
|
if (logSent) return
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
294
|
+
try {
|
|
295
|
+
const message = `${requestSnapshot.method} ${requestSnapshot.url}`
|
|
296
|
+
const meta = errorMeta ? { error: errorMeta } : {}
|
|
297
|
+
emitLog(level, message, meta)
|
|
298
|
+
} catch (err) {
|
|
299
|
+
try {
|
|
300
|
+
emitLog('error', `${requestSnapshot.method} ${requestSnapshot.url}`, { error: err && err.message ? err.message : String(err) })
|
|
301
|
+
} catch (_) {}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function runOutsideRequestContext (fn) {
|
|
306
|
+
als.run(undefined, fn)
|
|
219
307
|
}
|
|
220
308
|
|
|
221
309
|
res.on('finish', () => {
|
|
222
|
-
|
|
310
|
+
runOutsideOtelContext(() => {
|
|
311
|
+
runOutsideRequestContext(() => {
|
|
312
|
+
try {
|
|
313
|
+
finalize('info')
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const errPayload = {
|
|
316
|
+
traceId: reqCtx.traceId,
|
|
317
|
+
spanId: reqCtx.spanId,
|
|
318
|
+
parentSpanId: reqCtx.parentSpanId || null,
|
|
319
|
+
requestId,
|
|
320
|
+
request: requestSnapshot,
|
|
321
|
+
error: {
|
|
322
|
+
message: err && err.message ? err.message : String(err),
|
|
323
|
+
name: err && err.name ? err.name : 'Error',
|
|
324
|
+
stack: err && err.stack ? err.stack : undefined
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
setImmediate(() => {
|
|
328
|
+
try {
|
|
329
|
+
sendLog('error', (requestSnapshot && (requestSnapshot.method + ' ' + requestSnapshot.url)) || 'request', errPayload)
|
|
330
|
+
} catch (_) {}
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
})
|
|
223
335
|
})
|
|
224
336
|
|
|
225
337
|
res.on('close', () => {
|
|
226
|
-
|
|
338
|
+
runOutsideOtelContext(() => {
|
|
339
|
+
runOutsideRequestContext(() => {
|
|
340
|
+
try {
|
|
341
|
+
finalize('warn', { message: 'response closed before finish' })
|
|
342
|
+
} catch (_) {}
|
|
343
|
+
})
|
|
344
|
+
})
|
|
227
345
|
})
|
|
228
346
|
|
|
229
347
|
res.on('error', (err) => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
348
|
+
runOutsideOtelContext(() => {
|
|
349
|
+
runOutsideRequestContext(() => {
|
|
350
|
+
try {
|
|
351
|
+
finalize('error', {
|
|
352
|
+
message: err && err.message ? err.message : String(err),
|
|
353
|
+
name: err && err.name ? err.name : 'Error',
|
|
354
|
+
stack: err && err.stack ? err.stack : undefined
|
|
355
|
+
})
|
|
356
|
+
} catch (_) {}
|
|
357
|
+
})
|
|
234
358
|
})
|
|
235
359
|
})
|
|
236
360
|
|
package/otel-env.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
if (process.env.OTEL_TRACES_SAMPLER === undefined) {
|
|
2
|
+
process.env.OTEL_TRACES_SAMPLER = 'always_on'
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const Module = require('module')
|
|
7
|
+
const { pathToFileURL } = require('url')
|
|
8
|
+
const parentURL = pathToFileURL(require.resolve('./otel-env.js')).href
|
|
9
|
+
Module.register('import-in-the-middle/hook.mjs', parentURL, {
|
|
10
|
+
data: { include: ['@opentelemetry/sdk-node'] }
|
|
11
|
+
})
|
|
12
|
+
const { Hook } = require('import-in-the-middle')
|
|
13
|
+
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base')
|
|
14
|
+
new Hook(['@opentelemetry/sdk-node'], (exported) => {
|
|
15
|
+
if (!exported?.NodeSDK) return
|
|
16
|
+
const Original = exported.NodeSDK
|
|
17
|
+
const { AlwaysOnSampler } = exported.tracing || require('@opentelemetry/sdk-trace-base')
|
|
18
|
+
class Patched extends Original {
|
|
19
|
+
constructor (config = {}) {
|
|
20
|
+
if (!config.sampler) {
|
|
21
|
+
config = { ...config, sampler: new AlwaysOnSampler() }
|
|
22
|
+
}
|
|
23
|
+
if (config.traceExporter && !config.spanProcessor && !config.spanProcessors) {
|
|
24
|
+
config = { ...config, spanProcessor: new SimpleSpanProcessor(config.traceExporter) }
|
|
25
|
+
}
|
|
26
|
+
const inst = (config.instrumentations || []).flat()
|
|
27
|
+
const hasFastifyOtel = inst.some(i => i && /fastifyotel|@fastify\/otel/i.test(String(i.instrumentationName || i?.constructor?.name || '')))
|
|
28
|
+
if (hasFastifyOtel) {
|
|
29
|
+
config = {
|
|
30
|
+
...config,
|
|
31
|
+
instrumentations: inst.filter(i => {
|
|
32
|
+
const n = String(i?.instrumentationName || i?.constructor?.name || '')
|
|
33
|
+
return !n.includes('@opentelemetry/instrumentation-fastify')
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
super(config)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exported.NodeSDK = Patched
|
|
41
|
+
})
|
|
42
|
+
} catch (_) {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azify-logger",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.41",
|
|
4
4
|
"description": "Azify Logger Client - Centralized logging for OpenSearch",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -57,8 +57,9 @@
|
|
|
57
57
|
"cors": "^2.8.5",
|
|
58
58
|
"dotenv": "^17.2.3",
|
|
59
59
|
"express": "^4.18.2",
|
|
60
|
-
"fastify-plugin": "^5.0.0",
|
|
61
60
|
"express-session": "^1.17.3",
|
|
61
|
+
"fastify-plugin": "^5.0.0",
|
|
62
|
+
"import-in-the-middle": "^3.0.0",
|
|
62
63
|
"ioredis": "^5.8.2",
|
|
63
64
|
"js-yaml": "^4.1.0",
|
|
64
65
|
"passport": "^0.6.0",
|
|
@@ -81,6 +82,8 @@
|
|
|
81
82
|
"azify-logger-worker": "scripts/redis-worker.js"
|
|
82
83
|
},
|
|
83
84
|
"files": [
|
|
85
|
+
"trace-export.js",
|
|
86
|
+
"otel-env.js",
|
|
84
87
|
"index.js",
|
|
85
88
|
"index.d.ts",
|
|
86
89
|
"init.js",
|
package/queue/redisQueue.js
CHANGED
|
@@ -30,9 +30,8 @@ function createRedisProducer(config = {}) {
|
|
|
30
30
|
const maxLen = Number.isFinite(config.maxLen) ? config.maxLen : DEFAULT_MAXLEN
|
|
31
31
|
const password = config.password ?? process.env.AZIFY_LOGGER_REDIS_PASSWORD
|
|
32
32
|
const pass = password != null && String(password).trim() !== '' ? String(password).trim() : null
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new Error('Redis requires a password in production. Set AZIFY_LOGGER_REDIS_PASSWORD (or options.redis.password).')
|
|
33
|
+
if (!pass) {
|
|
34
|
+
throw new Error('Redis requires a password. Set AZIFY_LOGGER_REDIS_PASSWORD (or options.redis.password).')
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
const redisOptions = { ...defaultRedisOptions, ...(config.redisOptions || {}) }
|
package/register-otel.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
try {
|
|
2
2
|
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
3
|
+
const { AlwaysOnSampler } = require('@opentelemetry/core')
|
|
3
4
|
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http')
|
|
4
5
|
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express')
|
|
5
6
|
const { RestifyInstrumentation } = require('@opentelemetry/instrumentation-restify')
|
|
6
7
|
const { Resource } = require('@opentelemetry/resources')
|
|
7
8
|
const { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions')
|
|
8
9
|
|
|
9
|
-
const serviceName = process.env.OTEL_SERVICE_NAME || process.env.APP_NAME || 'app'
|
|
10
|
+
const serviceName = process.env.OTEL_SERVICE_NAME || process.env.SERVICE_NAME || process.env.APP_NAME || 'app'
|
|
10
11
|
const serviceVersion = process.env.OTEL_SERVICE_VERSION || '1.0.0'
|
|
11
12
|
|
|
12
13
|
process.env.OTEL_SERVICE_NAME = serviceName
|
|
@@ -18,43 +19,25 @@ try {
|
|
|
18
19
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = otlpEndpoint
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
let
|
|
22
|
-
let collectorPath = null
|
|
22
|
+
let ignoreOutgoingUrls = []
|
|
23
23
|
try {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const u = process.env.AZIFY_LOGGER_URL
|
|
25
|
+
if (u) {
|
|
26
|
+
const t = new URL(u)
|
|
27
|
+
const base = t.origin + (t.pathname || '/').replace(/\/$/, '') || t.origin + '/'
|
|
28
|
+
ignoreOutgoingUrls = [base, base + '/']
|
|
29
|
+
}
|
|
27
30
|
} catch (_) {}
|
|
28
31
|
|
|
29
|
-
const isLoggerRequest = (host, path) => {
|
|
30
|
-
if (!collectorHost || typeof host !== 'string') return false
|
|
31
|
-
if (host !== collectorHost) return false
|
|
32
|
-
return typeof path === 'string' && path.startsWith(collectorPath)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
32
|
const httpInstrumentation = new HttpInstrumentation({
|
|
36
33
|
enabled: true,
|
|
37
34
|
requireParentforOutgoingSpans: false,
|
|
38
35
|
requireParentforIncomingSpans: false,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
ignoreOutgoingRequestHook (res) {
|
|
46
|
-
try {
|
|
47
|
-
if (!collectorHost) return false
|
|
48
|
-
const host = res?.host
|
|
49
|
-
const path = res?.path
|
|
50
|
-
return isLoggerRequest(host, path)
|
|
51
|
-
} catch (_) {
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
requestHook (span, { options }) {
|
|
56
|
-
if (!options?.headers) return
|
|
57
|
-
const requestId = options.headers['x-request-id'] || options.headers['X-Request-ID']
|
|
36
|
+
ignoreIncomingPaths: ['/log', '/send', '/v1/traces'],
|
|
37
|
+
ignoreOutgoingUrls: ignoreOutgoingUrls.length ? ignoreOutgoingUrls : undefined,
|
|
38
|
+
requestHook (span, request) {
|
|
39
|
+
if (!request?.getHeader) return
|
|
40
|
+
const requestId = request.getHeader('x-request-id') || request.getHeader('X-Request-ID')
|
|
58
41
|
if (requestId) {
|
|
59
42
|
span.setAttribute('azify.request_id', requestId)
|
|
60
43
|
}
|
|
@@ -173,6 +156,7 @@ try {
|
|
|
173
156
|
[SEMRESATTRS_SERVICE_VERSION]: serviceVersion
|
|
174
157
|
}),
|
|
175
158
|
spanProcessor,
|
|
159
|
+
sampler: new AlwaysOnSampler(),
|
|
176
160
|
instrumentations: [
|
|
177
161
|
httpInstrumentation,
|
|
178
162
|
expressInstrumentation,
|
package/scripts/redis-worker.js
CHANGED
|
@@ -27,9 +27,8 @@ const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: MAX_SOCKETS })
|
|
|
27
27
|
|
|
28
28
|
const REDIS_PASSWORD = process.env.AZIFY_LOGGER_REDIS_PASSWORD
|
|
29
29
|
const pass = REDIS_PASSWORD != null && String(REDIS_PASSWORD).trim() !== '' ? String(REDIS_PASSWORD).trim() : null
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
process.stderr.write('[azify-logger] ❌ Redis requires a password in production. Set AZIFY_LOGGER_REDIS_PASSWORD (e.g. in env/app.env or your environment).\n')
|
|
30
|
+
if (!pass) {
|
|
31
|
+
process.stderr.write('[azify-logger] ⚠️ Redis requires a password. No password set. Set AZIFY_LOGGER_REDIS_PASSWORD to use Redis. Exiting.\n')
|
|
33
32
|
process.exit(1)
|
|
34
33
|
}
|
|
35
34
|
|
package/server.js
CHANGED
|
@@ -499,7 +499,26 @@ async function setupGrafanaForApp(appName) {
|
|
|
499
499
|
|
|
500
500
|
try {
|
|
501
501
|
const tempoDatasourceUid = `tempo-${appName.toLowerCase()}`
|
|
502
|
-
const tempoUrl = runningInDocker ? 'http://
|
|
502
|
+
const tempoUrl = runningInDocker ? 'http://tempo:3200' : 'http://localhost:3200'
|
|
503
|
+
const serviceFilter = `{ resource.service.name="${appName}" }`
|
|
504
|
+
const tempoJsonData = {
|
|
505
|
+
httpMethod: 'GET',
|
|
506
|
+
defaultQuery: serviceFilter,
|
|
507
|
+
tracesToLogs: {
|
|
508
|
+
datasourceUid: datasourceUid,
|
|
509
|
+
tags: ['job', 'service', 'pod'],
|
|
510
|
+
mappedTags: [{ key: 'service.name', value: 'service' }],
|
|
511
|
+
mapTagNamesEnabled: false,
|
|
512
|
+
spanStartTimeShift: '-1m',
|
|
513
|
+
spanEndTimeShift: '1m',
|
|
514
|
+
filterByTraceID: true,
|
|
515
|
+
filterBySpanID: false,
|
|
516
|
+
customQuery: true,
|
|
517
|
+
query: `traceId:"\${__trace.traceId}"`
|
|
518
|
+
},
|
|
519
|
+
nodeGraph: { enabled: false },
|
|
520
|
+
search: { hide: true }
|
|
521
|
+
}
|
|
503
522
|
const tempoDatasourceConfig = {
|
|
504
523
|
name: `Tempo-${appName}`,
|
|
505
524
|
type: 'tempo',
|
|
@@ -507,28 +526,7 @@ async function setupGrafanaForApp(appName) {
|
|
|
507
526
|
url: tempoUrl,
|
|
508
527
|
uid: tempoDatasourceUid,
|
|
509
528
|
isDefault: false,
|
|
510
|
-
jsonData:
|
|
511
|
-
httpMethod: 'GET',
|
|
512
|
-
tracesToLogs: {
|
|
513
|
-
datasourceUid: datasourceUid,
|
|
514
|
-
tags: ['job', 'service', 'pod'],
|
|
515
|
-
mappedTags: [{ key: 'service.name', value: 'service' }],
|
|
516
|
-
mapTagNamesEnabled: false,
|
|
517
|
-
spanStartTimeShift: '1h',
|
|
518
|
-
spanEndTimeShift: '1h',
|
|
519
|
-
filterByTraceID: false,
|
|
520
|
-
filterBySpanID: false
|
|
521
|
-
},
|
|
522
|
-
serviceMap: {
|
|
523
|
-
datasourceUid: datasourceUid
|
|
524
|
-
},
|
|
525
|
-
nodeGraph: {
|
|
526
|
-
enabled: true
|
|
527
|
-
},
|
|
528
|
-
search: {
|
|
529
|
-
hide: false
|
|
530
|
-
}
|
|
531
|
-
},
|
|
529
|
+
jsonData: tempoJsonData,
|
|
532
530
|
editable: true,
|
|
533
531
|
version: 1
|
|
534
532
|
}
|
|
@@ -1699,7 +1697,7 @@ async function handleLog(req, res) {
|
|
|
1699
1697
|
parentSpanId: null
|
|
1700
1698
|
}
|
|
1701
1699
|
}
|
|
1702
|
-
|
|
1700
|
+
const traceIdHex = (meta && meta.traceIdHex) || (traceContext.traceId ? String(traceContext.traceId).replace(/-/g, '').padStart(32, '0').slice(0, 32) : null)
|
|
1703
1701
|
const logEntry = {
|
|
1704
1702
|
'@timestamp': (meta && meta.timestamp) || new Date().toISOString(),
|
|
1705
1703
|
level,
|
|
@@ -1711,14 +1709,14 @@ async function handleLog(req, res) {
|
|
|
1711
1709
|
appName: (meta && meta.appName) || (meta && meta.service && meta.service.name) || undefined,
|
|
1712
1710
|
environment: (meta && meta.environment) || process.env.NODE_ENV,
|
|
1713
1711
|
hostname: (meta && meta.hostname) || os.hostname(),
|
|
1714
|
-
traceId: traceContext.traceId,
|
|
1712
|
+
traceId: traceIdHex || traceContext.traceId,
|
|
1715
1713
|
spanId: traceContext.spanId,
|
|
1716
1714
|
parentSpanId: traceContext.parentSpanId
|
|
1717
1715
|
}
|
|
1718
1716
|
|
|
1719
1717
|
if (meta) {
|
|
1720
1718
|
Object.keys(meta).forEach(key => {
|
|
1721
|
-
if (!['timestamp', 'service', 'environment', 'hostname', 'traceId', 'spanId', 'parentSpanId'].includes(key)) {
|
|
1719
|
+
if (!['timestamp', 'service', 'environment', 'hostname', 'traceId', 'traceIdHex', 'spanId', 'parentSpanId'].includes(key)) {
|
|
1722
1720
|
let value = meta[key]
|
|
1723
1721
|
|
|
1724
1722
|
if (typeof value === 'string') {
|
package/streams/httpQueue.js
CHANGED
|
@@ -94,9 +94,8 @@ function resolveRedisConfig (overrides = {}) {
|
|
|
94
94
|
const spoolDir = overrides.redisSpoolDir || (overrides.redis && overrides.redis.spoolDir) || process.env.AZIFY_LOGGER_REDIS_SPOOL_DIR
|
|
95
95
|
const password = overrides.redisPassword ?? (overrides.redis && overrides.redis.password) ?? process.env.AZIFY_LOGGER_REDIS_PASSWORD
|
|
96
96
|
const pass = password != null && String(password).trim() !== '' ? String(password).trim() : undefined
|
|
97
|
-
const isProd = process.env.NODE_ENV === 'production'
|
|
98
97
|
|
|
99
|
-
if (!pass
|
|
98
|
+
if (!pass) {
|
|
100
99
|
return null
|
|
101
100
|
}
|
|
102
101
|
|
|
@@ -128,14 +127,13 @@ function createHttpLoggerTransport (loggerUrl, overrides) {
|
|
|
128
127
|
}
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
const isProd = process.env.NODE_ENV === 'production'
|
|
132
130
|
const redisUrl = overrides.redisUrl || (overrides.redis && overrides.redis.url) || process.env.AZIFY_LOGGER_REDIS_URL || DEFAULT_REDIS_URL
|
|
133
131
|
const password = overrides.redisPassword ?? (overrides.redis && overrides.redis.password) ?? process.env.AZIFY_LOGGER_REDIS_PASSWORD
|
|
134
132
|
const pass = password != null && String(password).trim() !== '' ? String(password).trim() : undefined
|
|
135
|
-
if (
|
|
136
|
-
if (typeof global.
|
|
137
|
-
global.
|
|
138
|
-
process.stderr.write('[azify-logger] ⚠️ Redis requires a password
|
|
133
|
+
if (redisUrl && !pass) {
|
|
134
|
+
if (typeof global.__azifyLoggerRedisPasswordWarned === 'undefined') {
|
|
135
|
+
global.__azifyLoggerRedisPasswordWarned = true
|
|
136
|
+
process.stderr.write('[azify-logger] ⚠️ Redis requires a password. No password set. Using direct HTTP (no Redis). Set AZIFY_LOGGER_REDIS_PASSWORD to use Redis.\n')
|
|
139
137
|
}
|
|
140
138
|
}
|
|
141
139
|
|
package/trace-export.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const http = require('http')
|
|
2
|
+
const https = require('https')
|
|
3
|
+
|
|
4
|
+
let _endpoint = null
|
|
5
|
+
let _endpointOverride = null
|
|
6
|
+
function setEndpointOverride(url) {
|
|
7
|
+
_endpointOverride = url || null
|
|
8
|
+
_endpoint = null
|
|
9
|
+
}
|
|
10
|
+
function getEndpoint() {
|
|
11
|
+
if (_endpoint != null) return _endpoint
|
|
12
|
+
const url = _endpointOverride || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|
|
13
|
+
if (!url) return null
|
|
14
|
+
try {
|
|
15
|
+
_endpoint = new URL(url.includes('/v1/') ? url : String(url).replace(/\/$/, '') + '/v1/traces')
|
|
16
|
+
return _endpoint
|
|
17
|
+
} catch (_) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeHex(val, len) {
|
|
23
|
+
if (!val || typeof val !== 'string') return null
|
|
24
|
+
const hex = val.replace(/[^0-9a-fA-F]/g, '').padStart(len, '0').slice(0, len)
|
|
25
|
+
return hex || null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sendSpanToOtel(opts) {
|
|
29
|
+
const endpoint = getEndpoint()
|
|
30
|
+
if (!endpoint) return
|
|
31
|
+
const { traceId, spanId, serviceName, name, startTimeNs, endTimeNs, statusCode, method, url } = opts
|
|
32
|
+
const traceIdHex = normalizeHex(traceId, 32)
|
|
33
|
+
const spanIdHex = normalizeHex(spanId, 16)
|
|
34
|
+
if (!traceIdHex || !spanIdHex || !serviceName) return
|
|
35
|
+
const svc = String(serviceName).trim() || 'unknown'
|
|
36
|
+
const spanName = name || 'request'
|
|
37
|
+
const start = startTimeNs || BigInt(Date.now()) * BigInt(1e6)
|
|
38
|
+
const end = endTimeNs || BigInt(Date.now()) * BigInt(1e6)
|
|
39
|
+
const attrs = [
|
|
40
|
+
{ key: 'http.method', value: { stringValue: String(method || 'GET') } },
|
|
41
|
+
{ key: 'http.url', value: { stringValue: String(url || '') } }
|
|
42
|
+
]
|
|
43
|
+
if (statusCode != null) {
|
|
44
|
+
attrs.push({ key: 'http.status_code', value: { stringValue: String(statusCode) } })
|
|
45
|
+
}
|
|
46
|
+
const resourceSpans = [{
|
|
47
|
+
resource: {
|
|
48
|
+
attributes: [
|
|
49
|
+
{ key: 'service.name', value: { stringValue: svc } },
|
|
50
|
+
{ key: 'service.version', value: { stringValue: '1.0.0' } }
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
scopeSpans: [{
|
|
54
|
+
spans: [{
|
|
55
|
+
traceId: traceIdHex,
|
|
56
|
+
spanId: spanIdHex,
|
|
57
|
+
name: spanName,
|
|
58
|
+
kind: 1,
|
|
59
|
+
startTimeUnixNano: String(start),
|
|
60
|
+
endTimeUnixNano: String(end),
|
|
61
|
+
attributes: attrs
|
|
62
|
+
}]
|
|
63
|
+
}]
|
|
64
|
+
}]
|
|
65
|
+
const payload = JSON.stringify({ resourceSpans })
|
|
66
|
+
const urlObj = endpoint
|
|
67
|
+
const protocol = urlObj.protocol === 'https:' ? https : http
|
|
68
|
+
const options = {
|
|
69
|
+
hostname: urlObj.hostname,
|
|
70
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
71
|
+
path: urlObj.pathname,
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
'Content-Length': Buffer.byteLength(payload)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const req = protocol.request(options, () => {})
|
|
79
|
+
req.on('error', () => {})
|
|
80
|
+
req.setTimeout(2000, () => { req.destroy() })
|
|
81
|
+
req.write(payload)
|
|
82
|
+
req.end()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { sendSpanToOtel, getEndpoint, setEndpointOverride }
|