azify-logger 1.0.41 → 1.0.43

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.
@@ -15,6 +15,8 @@ declare function createExpressLoggingMiddleware(options?: {
15
15
  serviceName?: string;
16
16
  loggerUrl?: string;
17
17
  environment?: string;
18
+ otelEndpoint?: string;
19
+ otelExporterEndpoint?: string;
18
20
  }): ExpressMiddleware;
19
21
 
20
22
  export = createExpressLoggingMiddleware;
@@ -1,6 +1,7 @@
1
1
  require('./otel-env')
2
2
  const { startRequestContext, runWithRequestContext, getRequestContext } = require('./store')
3
3
  const { createHttpLoggerTransport } = require('./streams/httpQueue')
4
+ const { sendSpanToOtel, setEndpointOverride } = require('./trace-export')
4
5
  const { Worker } = require('worker_threads')
5
6
  const path = require('path')
6
7
  const os = require('os')
@@ -93,8 +94,17 @@ function pickHeaders(source) {
93
94
  }
94
95
 
95
96
  function createExpressLoggingMiddleware(options = {}) {
97
+ const svcName = options.serviceName || process.env.APP_NAME
98
+ if (svcName) {
99
+ process.env.OTEL_SERVICE_NAME = svcName
100
+ process.env.SERVICE_NAME = svcName
101
+ }
102
+ const otelEndpoint = options.otelEndpoint || options.otelExporterEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
103
+ if (otelEndpoint) {
104
+ setEndpointOverride(otelEndpoint)
105
+ }
96
106
  const config = {
97
- serviceName: options.serviceName || process.env.APP_NAME,
107
+ serviceName: svcName,
98
108
  loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
99
109
  environment: options.environment || process.env.NODE_ENV,
100
110
  captureResponseBody: options.captureResponseBody !== false && process.env.AZIFY_LOGGER_CAPTURE_RESPONSE_BODY !== 'false',
@@ -354,6 +364,21 @@ function createExpressLoggingMiddleware(options = {}) {
354
364
 
355
365
  const chunkToProcess = (responseChunk !== null && responseChunkCaptured) ? responseChunk : null
356
366
  emitResponseLog(meta, chunkToProcess)
367
+ if (meta.traceId && meta.spanId && config.serviceName) {
368
+ const startNs = BigInt(startTime) * BigInt(1e6)
369
+ const endNs = BigInt(meta.timestamp || Date.now()) * BigInt(1e6)
370
+ sendSpanToOtel({
371
+ traceId: meta.traceId,
372
+ spanId: meta.spanId,
373
+ serviceName: config.serviceName,
374
+ name: `request handler - ${path || url}`,
375
+ startTimeNs: startNs,
376
+ endTimeNs: endNs,
377
+ statusCode: res.statusCode || 200,
378
+ method,
379
+ url
380
+ })
381
+ }
357
382
  } catch (err) {
358
383
  try {
359
384
  ensureIds()
@@ -380,6 +405,21 @@ function createExpressLoggingMiddleware(options = {}) {
380
405
  if (serviceObj) meta.service = serviceObj
381
406
  if (config.environment) meta.environment = config.environment
382
407
  emitResponseLog(meta, null)
408
+ if (meta.traceId && meta.spanId && config.serviceName) {
409
+ const startNs = BigInt(startTime) * BigInt(1e6)
410
+ const endNs = BigInt(meta.timestamp || Date.now()) * BigInt(1e6)
411
+ sendSpanToOtel({
412
+ traceId: meta.traceId,
413
+ spanId: meta.spanId,
414
+ serviceName: config.serviceName,
415
+ name: `request handler - ${path || url}`,
416
+ startTimeNs: startNs,
417
+ endTimeNs: endNs,
418
+ statusCode: statusCode || 200,
419
+ method,
420
+ url
421
+ })
422
+ }
383
423
  } catch (_) {
384
424
  }
385
425
  }
@@ -1,6 +1,7 @@
1
1
  require('./otel-env')
2
2
  const { startRequestContext, runWithRequestContext, getRequestContext, als } = require('./store')
3
3
  const { createHttpLoggerTransport } = require('./streams/httpQueue')
4
+ const { sendSpanToOtel, setEndpointOverride } = require('./trace-export')
4
5
  const os = require('os')
5
6
 
6
7
  let trace, otelContext, otelRootContext
@@ -92,8 +93,17 @@ function pickHeaders (source) {
92
93
  }
93
94
 
94
95
  function createRestifyLoggingMiddleware (options = {}) {
96
+ const svcName = options.serviceName || process.env.APP_NAME
97
+ if (svcName) {
98
+ process.env.OTEL_SERVICE_NAME = svcName
99
+ process.env.SERVICE_NAME = svcName
100
+ }
101
+ const otelEndpoint = options.otelEndpoint || options.otelExporterEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
102
+ if (otelEndpoint) {
103
+ setEndpointOverride(otelEndpoint)
104
+ }
95
105
  const config = {
96
- serviceName: options.serviceName || process.env.APP_NAME,
106
+ serviceName: svcName,
97
107
  loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
98
108
  environment: options.environment || process.env.NODE_ENV,
99
109
  logRequest: options.logRequest !== false && process.env.AZIFY_LOGGER_LOG_REQUEST !== 'false'
@@ -266,6 +276,22 @@ function createRestifyLoggingMiddleware (options = {}) {
266
276
  }
267
277
 
268
278
  sendLog(level, message, meta)
279
+ if (meta.traceId && meta.spanId && config.serviceName) {
280
+ const startNs = BigInt(startTime) * BigInt(1e6)
281
+ const endNs = BigInt(Date.now()) * BigInt(1e6)
282
+ const statusCode = meta.response?.statusCode ?? 200
283
+ sendSpanToOtel({
284
+ traceId: meta.traceId,
285
+ spanId: meta.spanId,
286
+ serviceName: config.serviceName,
287
+ name: `request handler - ${requestSnapshot?.baseUrl || requestSnapshot?.url || ''}`,
288
+ startTimeNs: startNs,
289
+ endTimeNs: endNs,
290
+ statusCode,
291
+ method: requestSnapshot?.method,
292
+ url: requestSnapshot?.url
293
+ })
294
+ }
269
295
  } catch (err) {
270
296
  const errPayload = {
271
297
  traceId: reqCtx.traceId,
@@ -282,6 +308,21 @@ function createRestifyLoggingMiddleware (options = {}) {
282
308
  setImmediate(() => {
283
309
  try {
284
310
  sendLog('error', (requestSnapshot && (requestSnapshot.method + ' ' + requestSnapshot.url)) || 'request', errPayload)
311
+ if (errPayload.traceId && errPayload.spanId && config.serviceName) {
312
+ const startNs = BigInt(startTime) * BigInt(1e6)
313
+ const endNs = BigInt(Date.now()) * BigInt(1e6)
314
+ sendSpanToOtel({
315
+ traceId: errPayload.traceId,
316
+ spanId: errPayload.spanId,
317
+ serviceName: config.serviceName,
318
+ name: `request handler - ${requestSnapshot?.baseUrl || requestSnapshot?.url || ''}`,
319
+ startTimeNs: startNs,
320
+ endTimeNs: endNs,
321
+ statusCode: 500,
322
+ method: requestSnapshot?.method,
323
+ url: requestSnapshot?.url
324
+ })
325
+ }
285
326
  } catch (_) {}
286
327
  })
287
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azify-logger",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "description": "Azify Logger Client - Centralized logging for OpenSearch",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -30,9 +30,7 @@ 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
- if (!pass) {
34
- throw new Error('Redis requires a password. Set AZIFY_LOGGER_REDIS_PASSWORD (or options.redis.password).')
35
- }
33
+ const onUnrecoverableError = typeof config.onUnrecoverableError === 'function' ? config.onUnrecoverableError : null
36
34
 
37
35
  const redisOptions = { ...defaultRedisOptions, ...(config.redisOptions || {}) }
38
36
  if (pass) {
@@ -43,13 +41,9 @@ function createRedisProducer(config = {}) {
43
41
  let lastConnectionErrorLog = 0
44
42
  let lastEnqueueErrorLog = 0
45
43
  let connectionErrorCount = 0
44
+ let unrecoverableNotified = false
46
45
  const ERROR_LOG_INTERVAL = 300000
47
-
48
- if (typeof global.__azifyLoggerRedisErrorLogged === 'undefined') {
49
- global.__azifyLoggerRedisErrorLogged = false
50
- global.__azifyLoggerRedisErrorLastLog = 0
51
- }
52
-
46
+
53
47
  function isRedisAuthError(err) {
54
48
  if (!err) return false
55
49
  const code = err.code || ''
@@ -65,8 +59,26 @@ function createRedisProducer(config = {}) {
65
59
  return isRedisAuthError(err)
66
60
  }
67
61
 
62
+ function notifyUnrecoverableOnce() {
63
+ if (unrecoverableNotified) return
64
+ unrecoverableNotified = true
65
+ if (typeof global.__azifyLoggerRedisErrorLogged === 'undefined') {
66
+ global.__azifyLoggerRedisErrorLogged = false
67
+ global.__azifyLoggerRedisErrorLastLog = 0
68
+ }
69
+ global.__azifyLoggerRedisErrorLogged = true
70
+ global.__azifyLoggerRedisErrorLastLog = Date.now()
71
+ process.stderr.write('[azify-logger] Redis unavailable or auth failed. Using direct HTTP (no Redis). Continuing.\n')
72
+ if (onUnrecoverableError) {
73
+ try { onUnrecoverableError() } catch (_) {}
74
+ }
75
+ }
76
+
68
77
  client.on('error', (err) => {
69
- if (isRedisAuthError(err)) return
78
+ if (isRedisAuthError(err) || isRedisUnavailable(err)) {
79
+ notifyUnrecoverableOnce()
80
+ return
81
+ }
70
82
  const now = Date.now()
71
83
  if (!global.__azifyLoggerRedisErrorLogged && now - global.__azifyLoggerRedisErrorLastLog > ERROR_LOG_INTERVAL) {
72
84
  if (isRedisUnavailable(err)) {
@@ -17,9 +17,16 @@ function buildEnv(redisConfig = {}, extraEnv = {}) {
17
17
  if (redisConfig.streamKey) env.AZIFY_LOGGER_REDIS_STREAM = String(redisConfig.streamKey)
18
18
  if (redisConfig.streamKey) env.AZIFY_LOGGER_REDIS_QUEUE_KEY = String(redisConfig.streamKey)
19
19
  if (redisConfig.maxLen != null) env.AZIFY_LOGGER_REDIS_MAX_STREAM_LENGTH = String(redisConfig.maxLen)
20
- if (redisConfig.password != null && String(redisConfig.password).trim() !== '') {
21
- env.AZIFY_LOGGER_REDIS_PASSWORD = String(redisConfig.password).trim()
22
- }
20
+ const configPass = redisConfig.password != null && String(redisConfig.password).trim() !== ''
21
+ ? String(redisConfig.password).trim()
22
+ : null
23
+ const envPass = process.env.AZIFY_LOGGER_REDIS_PASSWORD != null && String(process.env.AZIFY_LOGGER_REDIS_PASSWORD).trim() !== ''
24
+ ? String(process.env.AZIFY_LOGGER_REDIS_PASSWORD).trim()
25
+ : null
26
+ const password = configPass || envPass
27
+ if (password) env.AZIFY_LOGGER_REDIS_PASSWORD = password
28
+ else if (process.env.AZIFY_LOGGER_REDIS_PASSWORD != null && String(process.env.AZIFY_LOGGER_REDIS_PASSWORD).trim() !== '')
29
+ env.AZIFY_LOGGER_REDIS_PASSWORD = String(process.env.AZIFY_LOGGER_REDIS_PASSWORD).trim()
23
30
 
24
31
  return env
25
32
  }
@@ -77,7 +84,7 @@ function spawnWorker(redisConfig, options = {}) {
77
84
  if (code === EXIT_AUTH_FAILURE) {
78
85
  if (typeof global.__azifyLoggerWorkerAuthExitLogged === 'undefined') {
79
86
  global.__azifyLoggerWorkerAuthExitLogged = true
80
- process.stderr.write('[azify-logger] Redis: invalid password. Worker exiting (no restart). Set AZIFY_LOGGER_REDIS_PASSWORD.\n')
87
+ process.stderr.write('[azify-logger] Redis unavailable or auth failed. Worker exited once (no restart). Using direct HTTP.\n')
81
88
  }
82
89
  const fns = authFailureCallbacks.splice(0, authFailureCallbacks.length)
83
90
  fns.forEach((fn) => { try { fn() } catch (_) {} })
package/register-otel.js CHANGED
@@ -1,3 +1,8 @@
1
+ function normalizeServiceNameForTraces(s) {
2
+ const t = String(s || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
3
+ return t && t !== 'unknown' && t !== 'unknown-service' ? t : 'default'
4
+ }
5
+
1
6
  try {
2
7
  const { NodeSDK } = require('@opentelemetry/sdk-node')
3
8
  const { AlwaysOnSampler } = require('@opentelemetry/core')
@@ -7,7 +12,8 @@ try {
7
12
  const { Resource } = require('@opentelemetry/resources')
8
13
  const { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions')
9
14
 
10
- const serviceName = process.env.OTEL_SERVICE_NAME || process.env.SERVICE_NAME || process.env.APP_NAME || 'app'
15
+ const rawName = process.env.APP_NAME || process.env.OTEL_SERVICE_NAME || process.env.SERVICE_NAME || 'app'
16
+ const serviceName = normalizeServiceNameForTraces(rawName)
11
17
  const serviceVersion = process.env.OTEL_SERVICE_VERSION || '1.0.0'
12
18
 
13
19
  process.env.OTEL_SERVICE_NAME = serviceName
@@ -488,8 +488,10 @@ ensureGroup()
488
488
  }
489
489
  return consumeLoop()
490
490
  }
491
- const authFail = isRedisAuthError(err) || /noauth|wrongpass|invalid password|authentication required/i.test(errMsg)
492
- if (authFail) process.exit(2)
491
+ const redisUnavailable = isRedisAuthError(err) || isRedisUnavailable(err) || /noauth|wrongpass|invalid password|authentication required|econnrefused/i.test(errMsg)
492
+ if (redisUnavailable) {
493
+ process.exit(2)
494
+ }
493
495
  console.error('[azify-logger][worker] failed to start:', errMsg)
494
496
  process.exit(1)
495
497
  })
package/server.js CHANGED
@@ -51,24 +51,24 @@ function isPrivateOrLocalhost(ip) {
51
51
  if (IS_LOCAL) {
52
52
  return true
53
53
  }
54
-
54
+
55
55
  ip = ip.replace('::ffff:', '').replace('::1', '127.0.0.1')
56
-
56
+
57
57
  if (ip === '127.0.0.1' || ip === 'localhost' || ip === '::1') {
58
58
  return true
59
59
  }
60
-
60
+
61
61
  const parts = ip.split('.').map(Number)
62
-
62
+
63
63
  if (parts.length !== 4) {
64
64
  return false
65
65
  }
66
-
66
+
67
67
  if (parts[0] === 10) return true
68
68
  if (parts[0] === 127) return true
69
69
  if (parts[0] === 192 && parts[1] === 168) return true
70
70
  if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true
71
-
71
+
72
72
  return false
73
73
  }
74
74
 
@@ -76,13 +76,13 @@ function validateNetworkAccess(req, res, next) {
76
76
  if (req.path === '/health' || req.path === '/' || req.path === '/v1/traces') {
77
77
  return next()
78
78
  }
79
-
79
+
80
80
  const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
81
- req.headers['x-real-ip'] ||
82
- req.ip ||
81
+ req.headers['x-real-ip'] ||
82
+ req.ip ||
83
83
  req.connection.remoteAddress ||
84
84
  req.socket.remoteAddress
85
-
85
+
86
86
  if (
87
87
  !IS_LOCAL &&
88
88
  !isPrivateOrLocalhost(clientIP) &&
@@ -94,7 +94,7 @@ function validateNetworkAccess(req, res, next) {
94
94
  clientIP: clientIP
95
95
  })
96
96
  }
97
-
97
+
98
98
  next()
99
99
  }
100
100
 
@@ -108,7 +108,7 @@ const traceContextMap = new Map()
108
108
  async function ensureIndexTemplate() {
109
109
  const templateName = 'logs-template'
110
110
  const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200'
111
-
111
+
112
112
  try {
113
113
  await axios.put(`${osUrl}/_index_template/${templateName}`, {
114
114
  index_patterns: ['logs-*'],
@@ -120,7 +120,8 @@ async function ensureIndexTemplate() {
120
120
  },
121
121
  mappings: {
122
122
  properties: {
123
- '@timestamp': { type: 'date' },
123
+ '@timestamp': { type: 'date', format: 'epoch_millis' },
124
+ time: { type: 'date' },
124
125
  level: { type: 'text', fields: { keyword: { type: 'keyword' } } },
125
126
  message: { type: 'text' },
126
127
  service: {
@@ -129,7 +130,7 @@ async function ensureIndexTemplate() {
129
130
  version: { type: 'keyword' }
130
131
  }
131
132
  },
132
- appName: {
133
+ appName: {
133
134
  type: 'keyword',
134
135
  fields: {
135
136
  keyword: { type: 'keyword' }
@@ -174,6 +175,99 @@ async function ensureIndexTemplate() {
174
175
 
175
176
  ensureIndexTemplate()
176
177
 
178
+ const { generateOtelCollectorConfig } = require('./otel-collector-config-generator')
179
+ const seenServiceNames = new Set()
180
+
181
+ function addSeenServiceName(name) {
182
+ const s = String(name || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
183
+ if (!s || s === 'unknown' || s === 'unknown-service') return
184
+ const isNew = !seenServiceNames.has(s)
185
+ seenServiceNames.add(s)
186
+ if (isNew) scheduleOtelCollectorConfigWrite()
187
+ }
188
+
189
+ async function fetchGrafanaOrgNames() {
190
+ const orgs = await fetchGrafanaOrgs()
191
+ return orgs.map((o) => (o.name || '').trim()).filter(Boolean)
192
+ }
193
+
194
+ async function fetchGrafanaOrgs() {
195
+ const runningInDocker = fs.existsSync('/.dockerenv')
196
+ const defaultGrafanaUrl = runningInDocker ? 'http://azify-grafana:3000' : 'http://127.0.0.1:3002'
197
+ const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
198
+ const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
199
+ const grafanaAdminPassword = process.env.GRAFANA_ADMIN_PASSWORD || process.env.GF_SECURITY_ADMIN_PASSWORD || 'admin'
200
+ try {
201
+ const resp = await axios.get(`${grafanaUrl}/api/orgs`, {
202
+ auth: { username: grafanaAdminUser, password: grafanaAdminPassword },
203
+ timeout: 3000
204
+ })
205
+ const orgs = (resp.data || []).map((o) => ({ id: o.id, name: (o.name || '').trim() })).filter((o) => o.name)
206
+ return orgs
207
+ } catch (err) {
208
+ return []
209
+ }
210
+ }
211
+
212
+ function normalizeServiceNameForOtel(s) {
213
+ const t = String(s || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
214
+ return t && t !== 'unknown' && t !== 'unknown-service' ? t : null
215
+ }
216
+
217
+ async function getOtelCollectorConfigYaml() {
218
+ const orgNames = await fetchGrafanaOrgNames()
219
+ const raw = [...new Set([...orgNames, ...seenServiceNames])]
220
+ const all = raw.map(normalizeServiceNameForOtel).filter(Boolean)
221
+ return generateOtelCollectorConfig(all)
222
+ }
223
+
224
+ function tryRestartOtelCollector() {
225
+ const container = process.env.OTEL_COLLECTOR_CONTAINER || 'azify-otel-collector'
226
+ try {
227
+ const http = require('http')
228
+ const req = http.request({
229
+ socketPath: '/var/run/docker.sock',
230
+ path: `/containers/${container}/restart`,
231
+ method: 'POST',
232
+ timeout: 10000
233
+ }, () => {})
234
+ req.on('error', () => {})
235
+ req.end()
236
+ } catch (_) {}
237
+ console.log('[otel-config] Restart do collector solicitado')
238
+ }
239
+
240
+ let otelConfigWriteScheduled = null
241
+ async function writeOtelCollectorConfigIfEnabled() {
242
+ const outPath = process.env.OTEL_COLLECTOR_CONFIG_OUTPUT_PATH
243
+ if (!outPath) return
244
+ try {
245
+ const yaml = await getOtelCollectorConfigYaml()
246
+ const all = [...new Set([...(await fetchGrafanaOrgNames()), ...seenServiceNames])]
247
+ const list = all.map(normalizeServiceNameForOtel).filter(Boolean)
248
+ const dir = require('path').dirname(outPath)
249
+ if (!fs.existsSync(dir)) {
250
+ try { fs.mkdirSync(dir, { recursive: true }) } catch (_) {}
251
+ }
252
+ if (fs.existsSync(dir)) {
253
+ fs.writeFileSync(outPath, yaml, 'utf8')
254
+ console.log('[otel-config] Config escrita:', list.length, 'serviços:', list.join(', ') || '(nenhum)')
255
+ tryRestartOtelCollector()
256
+ }
257
+ } catch (err) {
258
+ console.warn('[otel-config] Falha ao escrever config do collector:', err?.message || err)
259
+ }
260
+ }
261
+
262
+ function scheduleOtelCollectorConfigWrite() {
263
+ if (!process.env.OTEL_COLLECTOR_CONFIG_OUTPUT_PATH) return
264
+ if (otelConfigWriteScheduled) return
265
+ otelConfigWriteScheduled = setTimeout(async () => {
266
+ otelConfigWriteScheduled = null
267
+ await writeOtelCollectorConfigIfEnabled()
268
+ }, 5000)
269
+ }
270
+
177
271
  function escapeForSQLite(str) {
178
272
  if (!str) return "''"
179
273
  return str.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r')
@@ -184,7 +278,7 @@ function runSqlite(sql) {
184
278
  const grafanaContainer = process.env.GRAFANA_DOCKER_CONTAINER || 'azify-grafana'
185
279
  const { execSync } = require('child_process')
186
280
  const localDbExists = fs.existsSync(dbPath)
187
-
281
+
188
282
  const escapedForLocal = sql.replace(/"/g, '""')
189
283
  const escapedForDocker = sql.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')
190
284
 
@@ -214,20 +308,20 @@ async function createOrgViaSQLite(appName) {
214
308
 
215
309
  console.log(`[createOrgViaSQLite] Verificando se Organization '${appName}' já existe...`)
216
310
  const existingId = runSqlite(`SELECT id FROM org WHERE name='${appName}';`)
217
-
311
+
218
312
  if (existingId && existingId !== '') {
219
313
  console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' já existe (ID: ${existingId})`)
220
314
  return { id: parseInt(existingId), name: appName }
221
315
  }
222
-
316
+
223
317
  console.log(`[createOrgViaSQLite] Criando Organization '${appName}'...`)
224
318
  const newId = runSqlite(`INSERT INTO org (name, version, created, updated) VALUES ('${appName}', 1, datetime('now'), datetime('now')); SELECT id FROM org WHERE name='${appName}';`)
225
-
319
+
226
320
  if (newId && newId !== '') {
227
321
  console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' criada com sucesso (ID: ${newId})`)
228
322
  return { id: parseInt(newId), name: appName }
229
323
  }
230
-
324
+
231
325
  console.error(`[createOrgViaSQLite] ❌ Não foi possível obter ID da Organization criada`)
232
326
  return null
233
327
  } catch (error) {
@@ -249,13 +343,13 @@ async function setupGrafanaForApp(appName) {
249
343
  if (!setupGrafanaForApp._cache) {
250
344
  setupGrafanaForApp._cache = new Set()
251
345
  }
252
-
346
+
253
347
  if (setupGrafanaForApp._cache.has(appName)) {
254
348
  return
255
349
  }
256
-
350
+
257
351
  setupGrafanaForApp._cache.add(appName)
258
-
352
+
259
353
  console.log(`[setupGrafana] Processando app: ${appName}`)
260
354
 
261
355
  try {
@@ -263,13 +357,13 @@ async function setupGrafanaForApp(appName) {
263
357
  username: grafanaAdminUser,
264
358
  password: grafanaAdminPassword
265
359
  }
266
-
360
+
267
361
  console.log(`[setupGrafana] Usando autenticação: user=${grafanaAdminUser}, password=${grafanaAdminPassword ? '***' : 'VAZIO'}`)
268
362
 
269
363
  let org
270
-
364
+
271
365
  try {
272
- const orgResponse = await axios.get(`${grafanaUrl}/api/orgs/name/${encodeURIComponent(appName)}`, {
366
+ const orgResponse = await axios.get(`${grafanaUrl}/api/orgs/name/${encodeURIComponent(appName)}`, {
273
367
  auth,
274
368
  timeout: 3000
275
369
  })
@@ -319,7 +413,7 @@ async function setupGrafanaForApp(appName) {
319
413
  console.error(`[setupGrafana] ❌ Organization não encontrada/criada para ${appName}`)
320
414
  throw new Error(`Organization não encontrada ou criada para ${appName}`)
321
415
  }
322
-
416
+
323
417
  console.log(`[setupGrafana] ✅ Organization encontrada/criada: ${org.name} (ID: ${org.id})`)
324
418
 
325
419
  const adminEmails = process.env.ADMIN_EMAILS ? process.env.ADMIN_EMAILS.split(',') : []
@@ -327,16 +421,16 @@ async function setupGrafanaForApp(appName) {
327
421
  for (const email of adminEmails) {
328
422
  const trimmedEmail = email.trim()
329
423
  if (!trimmedEmail) continue
330
-
424
+
331
425
  try {
332
426
  const userResponse = await axios.get(`${grafanaUrl}/api/users/lookup?loginOrEmail=${encodeURIComponent(trimmedEmail)}`, {
333
427
  auth,
334
428
  timeout: 3000
335
429
  })
336
-
430
+
337
431
  if (userResponse.data && userResponse.data.id) {
338
432
  const userId = userResponse.data.id
339
-
433
+
340
434
  try {
341
435
  await axios.post(
342
436
  `${grafanaUrl}/api/orgs/${org.id}/users`,
@@ -378,7 +472,7 @@ async function setupGrafanaForApp(appName) {
378
472
  resolve(null)
379
473
  }
380
474
  })
381
-
475
+
382
476
  if (dbUserId) {
383
477
  try {
384
478
  runSqlite(`INSERT OR IGNORE INTO org_user (org_id, user_id, role, created, updated) VALUES (${org.id}, ${dbUserId}, 'Admin', datetime('now'), datetime('now')); UPDATE org_user SET role='Admin' WHERE org_id=${org.id} AND user_id=${dbUserId};`)
@@ -392,7 +486,7 @@ async function setupGrafanaForApp(appName) {
392
486
  }
393
487
 
394
488
  console.log(`[setupGrafana] 📊 Criando datasource para ${appName}...`)
395
-
489
+
396
490
  const datasourceUid = `opensearch-${appName.toLowerCase()}`
397
491
  const indexName = `logs-${appName}`
398
492
  const datasourceConfig = {
@@ -405,7 +499,7 @@ async function setupGrafanaForApp(appName) {
405
499
  jsonData: {
406
500
  index: indexName,
407
501
  database: indexName,
408
- timeField: '@timestamp',
502
+ timeField: 'time',
409
503
  esVersion: '2.11.1',
410
504
  version: '2.11.1',
411
505
  logMessageField: 'message',
@@ -500,10 +594,12 @@ async function setupGrafanaForApp(appName) {
500
594
  try {
501
595
  const tempoDatasourceUid = `tempo-${appName.toLowerCase()}`
502
596
  const tempoUrl = runningInDocker ? 'http://tempo:3200' : 'http://localhost:3200'
503
- const serviceFilter = `{ resource.service.name="${appName}" }`
597
+ const serviceNameNorm = String(appName).trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
504
598
  const tempoJsonData = {
505
599
  httpMethod: 'GET',
506
- defaultQuery: serviceFilter,
600
+ httpHeaderName1: 'X-Scope-OrgID',
601
+ httpHeaderValue1: serviceNameNorm,
602
+ defaultQuery: `{ resource.service.name="${serviceNameNorm}" }`,
507
603
  tracesToLogs: {
508
604
  datasourceUid: datasourceUid,
509
605
  tags: ['job', 'service', 'pod'],
@@ -517,7 +613,8 @@ async function setupGrafanaForApp(appName) {
517
613
  query: `traceId:"\${__trace.traceId}"`
518
614
  },
519
615
  nodeGraph: { enabled: false },
520
- search: { hide: true }
616
+ search: { hide: true },
617
+ lokiSearch: { hide: true }
521
618
  }
522
619
  const tempoDatasourceConfig = {
523
620
  name: `Tempo-${appName}`,
@@ -527,6 +624,7 @@ async function setupGrafanaForApp(appName) {
527
624
  uid: tempoDatasourceUid,
528
625
  isDefault: false,
529
626
  jsonData: tempoJsonData,
627
+ secureJsonData: { httpHeaderValue1: serviceNameNorm },
530
628
  editable: true,
531
629
  version: 1
532
630
  }
@@ -540,9 +638,23 @@ async function setupGrafanaForApp(appName) {
540
638
  })
541
639
  console.log(`[setupGrafana] Datasource Tempo existente encontrado: ${existingTempo.data?.id || 'N/A'}`)
542
640
  try {
641
+ const id = existingTempo.data.id
642
+ const putPayload = {
643
+ id,
644
+ orgId: existingTempo.data.orgId ?? org.id,
645
+ name: tempoDatasourceConfig.name,
646
+ type: 'tempo',
647
+ access: 'proxy',
648
+ url: tempoUrl,
649
+ uid: tempoDatasourceUid,
650
+ isDefault: false,
651
+ version: existingTempo.data.version ?? 1,
652
+ jsonData: { ...(existingTempo.data.jsonData || {}), ...tempoJsonData },
653
+ secureJsonData: { httpHeaderValue1: serviceNameNorm }
654
+ }
543
655
  await axios.put(
544
- `${grafanaUrl}/api/datasources/${existingTempo.data.id}`,
545
- tempoDatasourceConfig,
656
+ `${grafanaUrl}/api/datasources/${id}`,
657
+ putPayload,
546
658
  {
547
659
  auth,
548
660
  headers: { 'X-Grafana-Org-Id': org.id },
@@ -579,10 +691,10 @@ async function setupGrafanaForApp(appName) {
579
691
  }
580
692
 
581
693
  console.log(`[setupGrafana] 📈 Criando dashboard para ${appName}...`)
582
-
694
+
583
695
  const appNameLower = appName.toLowerCase()
584
696
  const indexFilter = `_index:logs-${appNameLower}`
585
-
697
+
586
698
  const dashboardConfig = {
587
699
  dashboard: {
588
700
  title: `Application Health - ${appName}`,
@@ -615,11 +727,11 @@ async function setupGrafanaForApp(appName) {
615
727
  defaults: {
616
728
  unit: 'short',
617
729
  color: { mode: 'thresholds' },
618
- custom: {
619
- reduceOptions: {
620
- values: false,
621
- calcs: ['sum']
622
- }
730
+ custom: {
731
+ reduceOptions: {
732
+ values: false,
733
+ calcs: ['sum']
734
+ }
623
735
  }
624
736
  }
625
737
  },
@@ -792,14 +904,14 @@ async function setupGrafanaForApp(appName) {
792
904
  auth,
793
905
  headers: { 'X-Grafana-Org-Id': org.id }
794
906
  })
795
-
907
+
796
908
  if (searchResponse.data && searchResponse.data.length > 0) {
797
909
  const existingDashboard = searchResponse.data[0]
798
910
  const dashboardDetail = await axios.get(`${grafanaUrl}/api/dashboards/uid/${existingDashboard.uid}`, {
799
911
  auth,
800
912
  headers: { 'X-Grafana-Org-Id': org.id }
801
913
  })
802
-
914
+
803
915
  dashboardConfig.dashboard.id = dashboardDetail.data.dashboard.id
804
916
  dashboardConfig.dashboard.uid = dashboardDetail.data.dashboard.uid
805
917
  dashboardConfig.dashboard.version = dashboardDetail.data.dashboard.version
@@ -857,7 +969,7 @@ async function setupGrafanaForApp(appName) {
857
969
  }
858
970
 
859
971
  console.log(`[setupGrafana] ✅ Setup completo para ${appName} (Organization, Datasource e Dashboard)`)
860
-
972
+
861
973
  setTimeout(() => {
862
974
  setupGrafanaForApp._cache.delete(appName)
863
975
  }, 3600000)
@@ -911,7 +1023,7 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
911
1023
  await setupGrafanaForApp(serviceName)
912
1024
 
913
1025
  const { grafanaUrl, auth, headers } = await getGrafanaClientConfig(options?.grafanaApiToken)
914
-
1026
+
915
1027
  console.log(`[alerts] 🔗 Conectando ao Grafana: ${grafanaUrl}`)
916
1028
 
917
1029
  const makeGrafanaRequestConfig = (extra = {}) => {
@@ -943,7 +1055,7 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
943
1055
  if (!orgId) {
944
1056
  throw new Error(`Org não encontrada para app ${serviceName}`)
945
1057
  }
946
-
1058
+
947
1059
  console.log(`[alerts] ✅ Organização encontrada: ${serviceName} (orgId=${orgId})`)
948
1060
 
949
1061
  const datasourceUid = `opensearch-${serviceName}`
@@ -1113,7 +1225,7 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
1113
1225
 
1114
1226
  const routeMatches = (route) => {
1115
1227
  if (!Array.isArray(route.matchers)) return false
1116
- return route.matchers.some(m =>
1228
+ return route.matchers.some(m =>
1117
1229
  (typeof m === 'string' && m.includes(`alertId=~${alertId}`)) ||
1118
1230
  (typeof m === 'object' && m.value === alertId)
1119
1231
  )
@@ -1236,8 +1348,8 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
1236
1348
  if (unit === 'h') windowSeconds = value * 60 * 60
1237
1349
  if (unit === 'd') windowSeconds = value * 60 * 60 * 24
1238
1350
  }
1239
-
1240
- const relativeTimeRangeSeconds = Math.max(windowSeconds, 300) // 5 minutos mínimo
1351
+
1352
+ const relativeTimeRangeSeconds = Math.max(windowSeconds, 300)
1241
1353
 
1242
1354
  const receiverName = await createOrUpdateContactPointForRule(rule)
1243
1355
 
@@ -1521,25 +1633,25 @@ function getOrCreateTraceContext(requestId) {
1521
1633
  if (traceContextMap.has(requestId)) {
1522
1634
  return traceContextMap.get(requestId)
1523
1635
  }
1524
-
1636
+
1525
1637
  const traceContext = {
1526
1638
  traceId: generateTraceId(),
1527
1639
  spanId: generateSpanId(),
1528
1640
  parentSpanId: null
1529
1641
  }
1530
-
1642
+
1531
1643
  traceContextMap.set(requestId, traceContext)
1532
-
1644
+
1533
1645
  setTimeout(() => {
1534
1646
  traceContextMap.delete(requestId)
1535
1647
  }, 30000)
1536
-
1648
+
1537
1649
  return traceContext
1538
1650
  }
1539
1651
 
1540
1652
  app.get('/health', (req, res) => {
1541
- res.json({
1542
- status: 'ok',
1653
+ res.json({
1654
+ status: 'ok',
1543
1655
  service: 'azify-logger',
1544
1656
  authEnabled
1545
1657
  })
@@ -1563,7 +1675,7 @@ function decodeHtmlEntities(str) {
1563
1675
  let prev = ''
1564
1676
  let iterations = 0
1565
1677
  const maxIterations = 10
1566
-
1678
+
1567
1679
  const namedEntities = {
1568
1680
  '&quot;': '"',
1569
1681
  '&apos;': "'",
@@ -1572,7 +1684,7 @@ function decodeHtmlEntities(str) {
1572
1684
  '&amp;': '&',
1573
1685
  '&nbsp;': ' '
1574
1686
  }
1575
-
1687
+
1576
1688
  while (decoded !== prev && iterations < maxIterations) {
1577
1689
  prev = decoded
1578
1690
  Object.keys(namedEntities).forEach(entity => {
@@ -1597,13 +1709,15 @@ function decodeHtmlEntities(str) {
1597
1709
  })
1598
1710
  iterations++
1599
1711
  }
1600
-
1712
+
1601
1713
  return decoded
1602
1714
  }
1603
1715
 
1604
1716
  async function handleLog(req, res) {
1605
- let { level, message, meta } = req.body
1606
-
1717
+ const body = req.body && typeof req.body === 'object' ? req.body : {}
1718
+ const payload = body.payload && typeof body.payload === 'object' ? body.payload : body
1719
+ let { level, message, meta } = payload
1720
+
1607
1721
  if (!level || !message) {
1608
1722
  return res.status(400).json({ success: false, message: 'Level and message are required.' })
1609
1723
  }
@@ -1615,7 +1729,7 @@ async function handleLog(req, res) {
1615
1729
  const requestPath = meta?.request?.path || meta?.request?.url || meta?.request?.baseUrl || meta?.url || meta?.path || ''
1616
1730
  const requestPathLower = String(requestPath).toLowerCase()
1617
1731
  const messageLower = String(message).toLowerCase()
1618
-
1732
+
1619
1733
  const isSwaggerPath = (
1620
1734
  requestPathLower.includes('/api-docs') ||
1621
1735
  requestPathLower.includes('/swagger-ui') ||
@@ -1654,7 +1768,7 @@ async function handleLog(req, res) {
1654
1768
  const requestId = meta && meta.requestId
1655
1769
 
1656
1770
  let traceContext = null
1657
-
1771
+
1658
1772
  if (meta && meta.traceId && meta.spanId) {
1659
1773
  traceContext = {
1660
1774
  traceId: meta.traceId,
@@ -1698,8 +1812,12 @@ async function handleLog(req, res) {
1698
1812
  }
1699
1813
  }
1700
1814
  const traceIdHex = (meta && meta.traceIdHex) || (traceContext.traceId ? String(traceContext.traceId).replace(/-/g, '').padStart(32, '0').slice(0, 32) : null)
1815
+ const ts = (meta && meta.timestamp) != null ? meta.timestamp : Date.now()
1816
+ const timestampMs = typeof ts === 'number' ? ts : new Date(ts).getTime()
1817
+ const timestampIso = new Date(timestampMs).toISOString()
1701
1818
  const logEntry = {
1702
- '@timestamp': (meta && meta.timestamp) || new Date().toISOString(),
1819
+ '@timestamp': timestampMs,
1820
+ time: timestampIso,
1703
1821
  level,
1704
1822
  message,
1705
1823
  service: {
@@ -1718,7 +1836,7 @@ async function handleLog(req, res) {
1718
1836
  Object.keys(meta).forEach(key => {
1719
1837
  if (!['timestamp', 'service', 'environment', 'hostname', 'traceId', 'traceIdHex', 'spanId', 'parentSpanId'].includes(key)) {
1720
1838
  let value = meta[key]
1721
-
1839
+
1722
1840
  if (typeof value === 'string') {
1723
1841
  if (key === 'url' || key === 'path' || key === 'baseUrl' || key === 'message') {
1724
1842
  value = decodeHtmlEntities(value)
@@ -1726,7 +1844,7 @@ async function handleLog(req, res) {
1726
1844
  value = decodeHtmlEntities(value)
1727
1845
  }
1728
1846
  }
1729
-
1847
+
1730
1848
  const truncateBody = (bodyValue, forResponse = false) => {
1731
1849
  if (forResponse) {
1732
1850
  if (typeof bodyValue === 'string') {
@@ -1742,7 +1860,7 @@ async function handleLog(req, res) {
1742
1860
  }
1743
1861
  return String(bodyValue)
1744
1862
  }
1745
-
1863
+
1746
1864
  if (typeof bodyValue === 'string') {
1747
1865
  return bodyValue
1748
1866
  } else if (typeof bodyValue === 'object' && bodyValue !== null) {
@@ -1761,7 +1879,7 @@ async function handleLog(req, res) {
1761
1879
  }
1762
1880
  return bodyValue
1763
1881
  }
1764
-
1882
+
1765
1883
  if (key === 'request' && value && typeof value === 'object' && value.body !== undefined) {
1766
1884
  const processedBody = truncateBody(value.body, false)
1767
1885
  if (typeof processedBody === 'string' && processedBody.length > 5000) {
@@ -1812,13 +1930,14 @@ async function handleLog(req, res) {
1812
1930
  const appName = logEntry.appName || logEntry.service?.name || 'unknown'
1813
1931
  const serviceName = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
1814
1932
  const indexName = `logs-${serviceName}`
1815
-
1933
+ addSeenServiceName(serviceName)
1934
+
1816
1935
  await axios.post(`${osUrl}/${indexName}/_doc`, logEntry, {
1817
1936
  headers: { 'Content-Type': 'application/json' }
1818
1937
  })
1819
-
1938
+
1820
1939
  console.log(`[setupGrafana] serviceName: ${serviceName}, appName: ${appName}`)
1821
-
1940
+
1822
1941
  if (serviceName !== 'unknown' && serviceName !== 'unknown-service') {
1823
1942
  console.log(`[setupGrafana] Iniciando setup para app: ${serviceName}`)
1824
1943
  setupGrafanaForApp(serviceName).then(() => {
@@ -1834,7 +1953,7 @@ async function handleLog(req, res) {
1834
1953
  } else {
1835
1954
  console.log(`[setupGrafana] ⚠️ Pulando setup para serviceName inválido: ${serviceName}`)
1836
1955
  }
1837
-
1956
+
1838
1957
  res.json({ success: true, message: 'Log enviado com sucesso', index: indexName })
1839
1958
  } catch (error) {
1840
1959
  const status = error?.response?.status
@@ -1864,10 +1983,41 @@ async function handleLog(req, res) {
1864
1983
 
1865
1984
  app.post('/log', (req, res) => handleLog(req, res))
1866
1985
 
1986
+ function extractServiceNamesFromTraceBody(body) {
1987
+ const names = new Set()
1988
+ try {
1989
+ const spans = body?.resourceSpans
1990
+ if (!Array.isArray(spans)) return names
1991
+ for (const rs of spans) {
1992
+ const attrs = rs?.resource?.attributes
1993
+ if (!Array.isArray(attrs)) continue
1994
+ for (const a of attrs) {
1995
+ if (a?.key === 'service.name') {
1996
+ const v = a?.value?.stringValue ?? a?.value
1997
+ if (typeof v === 'string' && v.trim()) names.add(v.trim())
1998
+ break
1999
+ }
2000
+ }
2001
+ }
2002
+ } catch (_) {}
2003
+ return names
2004
+ }
2005
+
1867
2006
  app.post('/v1/traces', async (req, res) => {
1868
2007
  try {
1869
- const collectorUrl = process.env.OTEL_COLLECTOR_URL || 'http://localhost:4318'
1870
- const response = await axios.post(`${collectorUrl}/v1/traces`, req.body, {
2008
+ const names = extractServiceNamesFromTraceBody(req.body)
2009
+ for (const name of names) addSeenServiceName(name)
2010
+ if (req.body?.resourceSpans?.length) scheduleOtelCollectorConfigWrite()
2011
+
2012
+ const ep = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
2013
+ let traceUrl = 'http://127.0.0.1:4318/v1/traces'
2014
+ if (ep) {
2015
+ try {
2016
+ const u = new URL(ep)
2017
+ traceUrl = ep.includes('/v1/') ? ep : `${u.origin.replace(/\/$/, '')}/v1/traces`
2018
+ } catch (_) {}
2019
+ }
2020
+ const response = await axios.post(traceUrl, req.body, {
1871
2021
  headers: {
1872
2022
  'Content-Type': 'application/json'
1873
2023
  },
@@ -1897,7 +2047,7 @@ app.post('/alerts/register', async (req, res) => {
1897
2047
  }
1898
2048
 
1899
2049
  console.log(`[alerts] 📥 Recebido registro de alertas para app=${appName}, ${rules.length} regra(s)`)
1900
-
2050
+
1901
2051
  const results = await registerAlertRulesForApp(appName, rules, { grafanaApiToken })
1902
2052
 
1903
2053
  res.json({
@@ -1962,7 +2112,7 @@ app.post('/dashboards/register', async (req, res) => {
1962
2112
  const orgId = org.id
1963
2113
 
1964
2114
  const datasourceUid = `opensearch-${appName.toLowerCase()}`
1965
-
2115
+
1966
2116
  const dashboardConfig = {
1967
2117
  ...dashboard,
1968
2118
  overwrite: true,
@@ -2064,7 +2214,22 @@ app.post('/dashboards/register', async (req, res) => {
2064
2214
 
2065
2215
  const port = process.env.PORT || 3001
2066
2216
 
2067
- app.listen(port)
2217
+ app.listen(port, () => {
2218
+ setTimeout(() => writeOtelCollectorConfigIfEnabled().catch(() => {}), 3000)
2219
+ setTimeout(async () => {
2220
+ try {
2221
+ const orgs = await fetchGrafanaOrgs()
2222
+ const toSetup = orgs.filter((o) => o.id !== 1 && o.name && o.name.toLowerCase() !== 'main org')
2223
+ if (setupGrafanaForApp._cache) setupGrafanaForApp._cache.clear()
2224
+ for (const org of toSetup) {
2225
+ await setupGrafanaForApp(org.name).catch((e) => console.warn('[setupGrafana] Reaplicar Tempo para org', org.name, ':', e?.message || e))
2226
+ }
2227
+ if (toSetup.length) await writeOtelCollectorConfigIfEnabled().catch(() => {})
2228
+ } catch (e) {
2229
+ console.warn('[setupGrafana] Reaplicar Tempo para todas as orgs:', e?.message || e)
2230
+ }
2231
+ }, 12000)
2232
+ })
2068
2233
 
2069
2234
  process.on('SIGTERM', () => {
2070
2235
  process.exit(0)
@@ -95,10 +95,6 @@ function resolveRedisConfig (overrides = {}) {
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
97
 
98
- if (!pass) {
99
- return null
100
- }
101
-
102
98
  return {
103
99
  url: redisUrl,
104
100
  streamKey,
@@ -141,9 +137,20 @@ function createHttpLoggerTransport (loggerUrl, overrides) {
141
137
  }
142
138
 
143
139
  function createRedisStreamTransport (loggerUrl, options, redisConfig) {
140
+ let useInlineFallback = false
141
+ let inlineTransport = null
142
+ const redisConfigWithCb = {
143
+ ...redisConfig,
144
+ onUnrecoverableError () {
145
+ if (useInlineFallback) return
146
+ useInlineFallback = true
147
+ inlineTransport = buildInlineTransport(loggerUrl, options)
148
+ }
149
+ }
150
+
144
151
  let producer = null
145
152
  try {
146
- producer = createRedisProducer(redisConfig)
153
+ producer = createRedisProducer(redisConfigWithCb)
147
154
  } catch (err) {
148
155
  throw new Error(`Failed to create Redis producer: ${err.message}`)
149
156
  }
@@ -152,9 +159,6 @@ function createRedisStreamTransport (loggerUrl, options, redisConfig) {
152
159
  throw new Error('Failed to create Redis producer: producer is null')
153
160
  }
154
161
 
155
- let useInlineFallback = false
156
- let inlineTransport = null
157
-
158
162
  let spool = null
159
163
  if (createFileSpool) {
160
164
  try {
package/trace-export.js CHANGED
@@ -25,6 +25,11 @@ function normalizeHex(val, len) {
25
25
  return hex || null
26
26
  }
27
27
 
28
+ function normalizeServiceNameForTraces(s) {
29
+ const t = String(s || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
30
+ return t && t !== 'unknown' && t !== 'unknown-service' ? t : 'default'
31
+ }
32
+
28
33
  function sendSpanToOtel(opts) {
29
34
  const endpoint = getEndpoint()
30
35
  if (!endpoint) return
@@ -32,7 +37,7 @@ function sendSpanToOtel(opts) {
32
37
  const traceIdHex = normalizeHex(traceId, 32)
33
38
  const spanIdHex = normalizeHex(spanId, 16)
34
39
  if (!traceIdHex || !spanIdHex || !serviceName) return
35
- const svc = String(serviceName).trim() || 'unknown'
40
+ const svc = normalizeServiceNameForTraces(String(serviceName).trim() || 'unknown')
36
41
  const spanName = name || 'request'
37
42
  const start = startTimeNs || BigInt(Date.now()) * BigInt(1e6)
38
43
  const end = endTimeNs || BigInt(Date.now()) * BigInt(1e6)