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 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 em produção.** Em `NODE_ENV=production` o Redis exige senha; em dev/staging é opcional. |
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
@@ -1,3 +1,4 @@
1
+ require('./otel-env')
1
2
  const { createHttpLoggerTransport } = require('./streams/httpQueue')
2
3
  const nativeConsole = {
3
4
  log: console.log.bind(console),
package/init.js CHANGED
@@ -1,6 +1,7 @@
1
1
  if (process.env.AZIFY_LOGGER_DISABLE === '1') {
2
2
  module.exports = {}
3
3
  } else {
4
+ require('./otel-env')
4
5
  const Module = require('module')
5
6
  const originalRequire = Module.prototype.require
6
7
 
@@ -1,3 +1,4 @@
1
+ require('./otel-env')
1
2
  const { startRequestContext, runWithRequestContext, getRequestContext } = require('./store')
2
3
  const { createHttpLoggerTransport } = require('./streams/httpQueue')
3
4
  const { Worker } = require('worker_threads')
@@ -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: options.serviceName || process.env.APP_NAME,
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
- function getOtelTraceContext() {
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 activeContext = otelContext.active()
111
- const span = trace.getSpan(activeContext)
112
- if (span) {
113
- const spanContext = span.spanContext()
114
- if (spanContext && spanContext.traceId && spanContext.spanId) {
115
- const traceHex = spanContext.traceId.replace(/-/g, '')
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: otelCtx?.parentSpanId || request.headers['x-parent-span-id'] || null
185
+ parentSpanId: request.headers['x-parent-span-id'] || null
137
186
  })
138
-
139
- if (otelCtx) {
140
- reqCtx.traceId = otelCtx.traceId
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.traceId = otelCtx?.traceId || meta.traceId || ctx.traceId
168
- meta.spanId = otelCtx?.spanId || meta.spanId || ctx.spanId
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
- const otelCtx = getOtelTraceContext()
182
- if (otelCtx) {
183
- traceId = otelCtx.traceId
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: otelCtx?.traceId || ctx.traceId,
261
- spanId: otelCtx?.spanId || ctx.spanId,
262
- parentSpanId: otelCtx?.parentSpanId || ctx.parentSpanId || null,
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 otelCtx = getOtelTraceContext()
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: otelCtx?.parentSpanId || request.headers['x-parent-span-id'] || null
358
+ parentSpanId: capturedParentSpanId || request.headers['x-parent-span-id'] || null
326
359
  })
327
360
 
328
- if (otelCtx) {
329
- reqCtx.traceId = otelCtx.traceId
330
- reqCtx.spanId = otelCtx.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 = otelCtx?.traceId || meta.traceId || ctx.traceId
341
- meta.spanId = otelCtx?.spanId || meta.spanId || ctx.spanId
342
- meta.parentSpanId = otelCtx?.parentSpanId || meta.parentSpanId || ctx.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
- const otelCtx = getOtelTraceContext()
355
- if (otelCtx) {
356
- traceId = otelCtx.traceId
357
- spanId = otelCtx.spanId
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: otelCtx?.traceId || ctx.traceId,
416
- spanId: otelCtx?.spanId || ctx.spanId,
417
- parentSpanId: otelCtx?.parentSpanId || ctx.parentSpanId || null,
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
@@ -1,15 +1,46 @@
1
- const { startRequestContext, runWithRequestContext, getRequestContext } = require('./store')
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
- const meta = {
197
- traceId: otelCtx?.traceId || ctx.traceId,
198
- spanId: otelCtx?.spanId || ctx.spanId,
199
- parentSpanId: otelCtx?.parentSpanId || ctx.parentSpanId || null,
200
- requestId,
201
- request: requestSnapshot,
202
- response: {
203
- statusCode,
204
- headers: pickHeaders(responseHeaders),
205
- durationMs: duration
206
- },
207
- ...extraMeta
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
- sendLog(level, message, meta)
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
- const message = `${req.method} ${req.url}`
217
- const meta = errorMeta ? { error: errorMeta } : {}
218
- emitLog(level, message, meta)
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
- finalize('info')
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
- finalize('warn', { message: 'response closed before finish' })
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
- finalize('error', {
231
- message: err && err.message ? err.message : String(err),
232
- name: err && err.name ? err.name : 'Error',
233
- stack: err && err.stack ? err.stack : undefined
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.40",
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",
@@ -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
- const isProd = process.env.NODE_ENV === 'production'
34
- if (!pass && isProd) {
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 collectorHost = null
22
- let collectorPath = null
22
+ let ignoreOutgoingUrls = []
23
23
  try {
24
- const target = new URL(process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log')
25
- collectorHost = target.host
26
- collectorPath = target.pathname || '/'
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
- ignoreIncomingRequestHook (req) {
40
- if (!collectorHost) return false
41
- const host = req.headers?.host
42
- const url = req.url
43
- return typeof host === 'string' && isLoggerRequest(host, url)
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,
@@ -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
- const isProd = process.env.NODE_ENV === 'production'
31
- if (!pass && isProd) {
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://azify-tempo:3200' : 'http://localhost:3200'
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') {
@@ -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 && isProd) {
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 (isProd && redisUrl && !pass) {
136
- if (typeof global.__azifyLoggerProdPasswordWarned === 'undefined') {
137
- global.__azifyLoggerProdPasswordWarned = true
138
- process.stderr.write('[azify-logger] ⚠️ Redis requires a password in production. Using direct HTTP (no Redis). Set AZIFY_LOGGER_REDIS_PASSWORD to use Redis.\n')
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
 
@@ -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 }