azify-logger 1.0.45 → 1.0.46

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azify-logger",
3
- "version": "1.0.45",
3
+ "version": "1.0.46",
4
4
  "description": "Azify Logger Client - Centralized logging for OpenSearch",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -65,6 +65,7 @@
65
65
  "passport": "^0.6.0",
66
66
  "passport-azure-ad": "^4.3.5",
67
67
  "require-in-the-middle": "^7.4.0",
68
+ "semver": "7.5.2",
68
69
  "uuid": "^9.0.1"
69
70
  },
70
71
  "overrides": {
@@ -73,10 +74,13 @@
73
74
  "@opentelemetry/resources": "1.0.1",
74
75
  "@opentelemetry/semantic-conventions": "1.0.1",
75
76
  "@opentelemetry/sdk-node": "0.27.0",
76
- "@opentelemetry/auto-instrumentations-node": "0.27.0"
77
+ "@opentelemetry/auto-instrumentations-node": "0.27.0",
78
+ "@opentelemetry/resource-detector-gcp": {
79
+ "semver": "7.5.2"
80
+ }
77
81
  },
78
82
  "engines": {
79
- "node": ">=12 <=22"
83
+ "node": ">=20 <=22"
80
84
  },
81
85
  "bin": {
82
86
  "azify-logger-worker": "scripts/redis-worker.js"
@@ -91,6 +95,7 @@
91
95
  "init.js",
92
96
  "register.js",
93
97
  "store.js",
98
+ "store.d.ts",
94
99
  "register-otel.js",
95
100
  "register-restify.js",
96
101
  "middleware-restify.js",
package/register.js CHANGED
@@ -1137,21 +1137,24 @@ try {
1137
1137
  const traceCtx = ctx || otelCtx
1138
1138
  const method = (options?.method || 'GET').toUpperCase()
1139
1139
  const urlString = typeof url === 'string' ? url : (url && url.toString && url.toString()) || 'unknown'
1140
- const rawTraceId = traceCtx?.traceId || (require('crypto').randomUUID && require('crypto').randomUUID())
1140
+ const existingHeaders = (options && options.headers && typeof options.headers === 'object') ? options.headers : {}
1141
+ const headerTraceId = existingHeaders['x-trace-id'] || existingHeaders['X-Trace-ID'] || null
1142
+ const headerSpanId = existingHeaders['x-span-id'] || existingHeaders['X-Span-ID'] || null
1143
+ const headerParentSpanId = existingHeaders['x-parent-span-id'] || existingHeaders['X-Parent-Span-ID'] || null
1144
+ const headerRequestId = existingHeaders['x-request-id'] || existingHeaders['X-Request-ID'] || null
1145
+ const rawTraceId = headerTraceId || traceCtx?.traceId || (require('crypto').randomUUID && require('crypto').randomUUID())
1141
1146
  const traceId = typeof rawTraceId === 'string' ? toTraceIdHex(rawTraceId) : rawTraceId
1142
- const parentSpanId = traceCtx?.spanId || null
1143
- const requestId = traceCtx?.requestId || ctx?.requestId || (require('crypto').randomUUID && require('crypto').randomUUID())
1144
- const spanId = randomBytes(8).toString('hex')
1147
+ const parentSpanId = headerParentSpanId || traceCtx?.spanId || null
1148
+ const requestId = headerRequestId || traceCtx?.requestId || ctx?.requestId || (require('crypto').randomUUID && require('crypto').randomUUID())
1149
+ const spanId = headerSpanId || randomBytes(8).toString('hex')
1145
1150
  const requestMeta = { traceId, spanId, parentSpanId, requestId, method, url: urlString }
1146
1151
  markSource(requestMeta, 'http-client')
1147
1152
  const startTime = performance.now()
1148
1153
  if (HTTP_CLIENT_MODE !== 'off') {
1149
1154
  sendOutboundLog('info', `[REQUEST] ${method} ${urlString}`, requestMeta)
1150
1155
  }
1151
- if (traceCtx && traceCtx.traceId) {
1152
- const headers = (options && options.headers) || {}
1153
- options = { ...options, headers: { ...headers, 'X-Trace-ID': traceId, 'X-Span-ID': spanId, 'X-Parent-Span-ID': traceCtx.parentSpanId || '', 'X-Request-ID': requestId } }
1154
- }
1156
+ const headers = (options && options.headers && typeof options.headers === 'object') ? options.headers : {}
1157
+ options = { ...options, headers: { ...headers, 'X-Trace-ID': traceId, 'X-Span-ID': spanId, 'X-Parent-Span-ID': parentSpanId || '', 'X-Request-ID': requestId } }
1155
1158
  const wrappedCb = callback ? function(err, data) {
1156
1159
  const duration = Number((performance.now() - startTime).toFixed(2))
1157
1160
  if (HTTP_CLIENT_MODE !== 'off') {
@@ -1203,19 +1206,24 @@ try {
1203
1206
  const origin = (opts && opts.origin) || ''
1204
1207
  const path = (opts && opts.path) != null ? opts.path : '/'
1205
1208
  const urlString = origin ? (origin + (path.startsWith('/') ? path : '/' + path)) : 'unknown'
1206
- const rawTraceId = traceCtx?.traceId || (require('crypto').randomUUID && require('crypto').randomUUID())
1209
+ const existingHeaders = (opts && opts.headers && typeof opts.headers === 'object') ? opts.headers : {}
1210
+ const headerTraceId = existingHeaders['x-trace-id'] || existingHeaders['X-Trace-ID'] || null
1211
+ const headerSpanId = existingHeaders['x-span-id'] || existingHeaders['X-Span-ID'] || null
1212
+ const headerParentSpanId = existingHeaders['x-parent-span-id'] || existingHeaders['X-Parent-Span-ID'] || null
1213
+ const headerRequestId = existingHeaders['x-request-id'] || existingHeaders['X-Request-ID'] || null
1214
+ const rawTraceId = headerTraceId || traceCtx?.traceId || (require('crypto').randomUUID && require('crypto').randomUUID())
1207
1215
  const traceId = typeof rawTraceId === 'string' ? toTraceIdHex(rawTraceId) : rawTraceId
1208
- const parentSpanId = traceCtx?.spanId || null
1209
- const requestId = traceCtx?.requestId || ctx?.requestId || (require('crypto').randomUUID && require('crypto').randomUUID())
1210
- const spanId = randomBytes(8).toString('hex')
1216
+ const parentSpanId = headerParentSpanId || traceCtx?.spanId || null
1217
+ const requestId = headerRequestId || traceCtx?.requestId || ctx?.requestId || (require('crypto').randomUUID && require('crypto').randomUUID())
1218
+ const spanId = headerSpanId || randomBytes(8).toString('hex')
1211
1219
  const requestMeta = { traceId, spanId, parentSpanId, requestId, method: methodStr, url: urlString }
1212
1220
  markSource(requestMeta, 'http-client')
1213
1221
  const startTime = performance.now()
1214
1222
  if (HTTP_CLIENT_MODE !== 'off') {
1215
1223
  sendOutboundLog('info', `[REQUEST] ${methodStr} ${urlString}`, requestMeta)
1216
1224
  }
1217
- if (traceCtx && traceCtx.traceId && opts && typeof opts === 'object') {
1218
- const headers = { ...(opts.headers || {}), 'X-Trace-ID': traceId, 'X-Span-ID': spanId, 'X-Parent-Span-ID': traceCtx.parentSpanId || '', 'X-Request-ID': requestId }
1225
+ if (opts && typeof opts === 'object') {
1226
+ const headers = { ...(opts.headers || {}), 'X-Trace-ID': traceId, 'X-Span-ID': spanId, 'X-Parent-Span-ID': parentSpanId || '', 'X-Request-ID': requestId }
1219
1227
  opts = { ...opts, headers }
1220
1228
  }
1221
1229
  const wrappedCb = typeof callback === 'function' ? function(err, data) {
@@ -1273,19 +1281,24 @@ try {
1273
1281
  const origin = (opts && opts.origin) || ''
1274
1282
  const path = (opts && opts.path) != null ? opts.path : '/'
1275
1283
  const urlString = origin ? (origin + (path.startsWith('/') ? path : '/' + path)) : 'unknown'
1276
- const rawTraceId = traceCtx?.traceId || (require('crypto').randomUUID && require('crypto').randomUUID())
1284
+ const existingHeaders = (opts && opts.headers && typeof opts.headers === 'object') ? opts.headers : {}
1285
+ const headerTraceId = existingHeaders['x-trace-id'] || existingHeaders['X-Trace-ID'] || null
1286
+ const headerSpanId = existingHeaders['x-span-id'] || existingHeaders['X-Span-ID'] || null
1287
+ const headerParentSpanId = existingHeaders['x-parent-span-id'] || existingHeaders['X-Parent-Span-ID'] || null
1288
+ const headerRequestId = existingHeaders['x-request-id'] || existingHeaders['X-Request-ID'] || null
1289
+ const rawTraceId = headerTraceId || traceCtx?.traceId || (require('crypto').randomUUID && require('crypto').randomUUID())
1277
1290
  const traceId = typeof rawTraceId === 'string' ? toTraceIdHex(rawTraceId) : rawTraceId
1278
- const parentSpanId = traceCtx?.spanId || null
1279
- const requestId = traceCtx?.requestId || ctx?.requestId || (require('crypto').randomUUID && require('crypto').randomUUID())
1280
- const spanId = randomBytes(8).toString('hex')
1291
+ const parentSpanId = headerParentSpanId || traceCtx?.spanId || null
1292
+ const requestId = headerRequestId || traceCtx?.requestId || ctx?.requestId || (require('crypto').randomUUID && require('crypto').randomUUID())
1293
+ const spanId = headerSpanId || randomBytes(8).toString('hex')
1281
1294
  const requestMeta = { traceId, spanId, parentSpanId, requestId, method: methodStr, url: urlString }
1282
1295
  markSource(requestMeta, 'http-client')
1283
1296
  const startTime = performance.now()
1284
1297
  if (HTTP_CLIENT_MODE !== 'off') {
1285
1298
  sendOutboundLog('info', `[REQUEST] ${methodStr} ${urlString}`, requestMeta)
1286
1299
  }
1287
- if (traceCtx && traceCtx.traceId && opts && typeof opts === 'object') {
1288
- const headers = { ...(opts.headers || {}), 'X-Trace-ID': traceId, 'X-Span-ID': spanId, 'X-Parent-Span-ID': traceCtx.parentSpanId || '', 'X-Request-ID': requestId }
1300
+ if (opts && typeof opts === 'object') {
1301
+ const headers = { ...(opts.headers || {}), 'X-Trace-ID': traceId, 'X-Span-ID': spanId, 'X-Parent-Span-ID': parentSpanId || '', 'X-Request-ID': requestId }
1289
1302
  opts = { ...opts, headers }
1290
1303
  }
1291
1304
  const wrappedCb = typeof callback === 'function' ? function(err, data) {
package/server.js CHANGED
@@ -1697,14 +1697,14 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
1697
1697
  }
1698
1698
 
1699
1699
  function generateTraceId() {
1700
- return Math.random().toString(36).substring(2, 15) +
1701
- Math.random().toString(36).substring(2, 15)
1700
+ return require('crypto').randomBytes(16).toString('hex')
1702
1701
  }
1703
1702
 
1704
1703
  function generateSpanId() {
1705
- return Math.random().toString(36).substring(2, 15)
1704
+ return require('crypto').randomBytes(8).toString('hex')
1706
1705
  }
1707
1706
 
1707
+
1708
1708
  function getOrCreateTraceContext(requestId) {
1709
1709
  if (traceContextMap.has(requestId)) {
1710
1710
  return traceContextMap.get(requestId)
@@ -1859,12 +1859,12 @@ async function handleLog(req, res) {
1859
1859
 
1860
1860
  let traceContext = null
1861
1861
 
1862
- if (meta && meta.traceId && meta.spanId) {
1862
+ if (meta && meta.traceId) {
1863
1863
  const tid = toTraceIdHex32(meta.traceId)
1864
1864
  if (tid) {
1865
1865
  traceContext = {
1866
1866
  traceId: tid,
1867
- spanId: String(meta.spanId).slice(0, 16),
1867
+ spanId: meta.spanId != null && meta.spanId !== '' ? String(meta.spanId).slice(0, 16) : generateSpanId(),
1868
1868
  parentSpanId: meta.parentSpanId != null && meta.parentSpanId !== '' ? String(meta.parentSpanId) : null
1869
1869
  }
1870
1870
  }
@@ -2054,7 +2054,57 @@ async function handleLog(req, res) {
2054
2054
 
2055
2055
  res.json({ success: true, message: 'Log enviado com sucesso', index: indexName })
2056
2056
 
2057
- _enqueueOpenSearchWrite(osUrl, indexName, logEntry, serviceName)
2057
+ const docToSend = _trimLogEntryForOpenSearch(logEntry)
2058
+ _enqueueOpenSearchWrite(osUrl, indexName, docToSend, serviceName)
2059
+ }
2060
+
2061
+ const _opensearchMaxFields = Math.min(Number(process.env.AZIFY_LOGGER_OPENSEARCH_MAX_FIELDS) || 0, 2000) || 900
2062
+
2063
+ function _countFields(obj, depth = 0) {
2064
+ if (depth > 15) return 0
2065
+ if (obj == null || typeof obj !== 'object') return 1
2066
+ let n = 0
2067
+ for (const key of Object.keys(obj)) {
2068
+ const v = obj[key]
2069
+ if (v != null && typeof v === 'object' && !Array.isArray(v) && !Buffer.isBuffer(v)) n += _countFields(v, depth + 1)
2070
+ else n += 1
2071
+ }
2072
+ return n
2073
+ }
2074
+
2075
+ function _stringifyValue(value) {
2076
+ try {
2077
+ return typeof value === 'object' ? JSON.stringify(value) : String(value)
2078
+ } catch (_) {
2079
+ return '[Unable to serialize]'
2080
+ }
2081
+ }
2082
+
2083
+ function _trimLogEntryForOpenSearch(doc) {
2084
+ if (_countFields(doc) <= _opensearchMaxFields) return doc
2085
+ const out = {}
2086
+ for (const key of Object.keys(doc)) {
2087
+ const value = doc[key]
2088
+ if (value != null && typeof value === 'object' && !Array.isArray(value) && !Buffer.isBuffer(value)) {
2089
+ if (_countFields(value) > 80) {
2090
+ out[key + 'Json'] = _stringifyValue(value)
2091
+ } else {
2092
+ out[key] = value
2093
+ }
2094
+ } else {
2095
+ out[key] = value
2096
+ }
2097
+ }
2098
+ if (_countFields(out) > _opensearchMaxFields) {
2099
+ for (const k of Object.keys(out)) {
2100
+ const v = out[k]
2101
+ if (v != null && typeof v === 'object' && !Array.isArray(v) && !Buffer.isBuffer(v)) {
2102
+ out[k + 'Json'] = _stringifyValue(v)
2103
+ delete out[k]
2104
+ }
2105
+ }
2106
+ }
2107
+ return out
2058
2108
  }
2059
2109
 
2060
2110
  const _openSearchWriteQueue = []
@@ -2072,23 +2122,82 @@ const _logDedupeWindowMs = 5000
2072
2122
  const _logDedupeMaxKeys = 15000
2073
2123
  const _logDedupeSeen = new Map()
2074
2124
  const _logDedupeBucketMs = 80
2125
+ const _logDedupeGenericBucketMs = 250
2126
+
2127
+ function _normalizeReqResMessageForDedupe(msg) {
2128
+ const s = typeof msg === 'string' ? msg : String(msg || '')
2129
+ return s.replace(/\s+\d+(\.\d+)?ms\s*$/i, '').trimEnd()
2130
+ }
2131
+
2132
+ function _httpClientDedupeFingerprint(logEntry) {
2133
+ if (!logEntry || logEntry.source !== 'http-client') return null
2134
+ const msg = typeof logEntry.message === 'string' ? logEntry.message.substring(0, 500) : String(logEntry.message || '')
2135
+ const msgLower = msg.trimStart().toLowerCase()
2136
+ const isReq = msgLower.startsWith('[request]')
2137
+ const isRes = msgLower.startsWith('[response]')
2138
+ if (!isReq && !isRes) return null
2139
+
2140
+ const methodRaw =
2141
+ (logEntry.method != null && String(logEntry.method).trim()) ||
2142
+ (logEntry.request && logEntry.request.method != null && String(logEntry.request.method).trim()) ||
2143
+ ''
2144
+ const m = methodRaw.toUpperCase()
2145
+ let url = ''
2146
+ if (logEntry.url != null && String(logEntry.url).trim()) {
2147
+ url = String(logEntry.url).trim()
2148
+ } else if (logEntry.request && typeof logEntry.request === 'object') {
2149
+ url = String(logEntry.request.url || logEntry.request.path || '').trim()
2150
+ }
2151
+ if (!m || !url) return null
2152
+
2153
+ if (isReq) {
2154
+ return `REQUEST|${m}|${url}`
2155
+ }
2156
+ let st = logEntry.statusCode != null ? String(logEntry.statusCode).trim() : ''
2157
+ if (!st && /\bERROR\b/i.test(msg)) {
2158
+ st = 'ERR'
2159
+ }
2160
+ if (!st) {
2161
+ const mStatus = msg.match(/\s(\d{3})\s+(?:\d+(?:\.\d+)?ms\s*)?$/i)
2162
+ if (mStatus) st = mStatus[1]
2163
+ }
2164
+ return `RESPONSE|${m}|${url}|${st || 'na'}`
2165
+ }
2075
2166
 
2076
2167
  function _logDedupeKey(serviceName, logEntry) {
2077
2168
  const msg = typeof logEntry.message === 'string' ? logEntry.message.substring(0, 500) : String(logEntry.message || '')
2078
- const msgLower = msg.toLowerCase()
2169
+ const msgLower = msg.trimStart().toLowerCase()
2079
2170
  const isReqRes = msgLower.includes('[request]') || msgLower.includes('[response]')
2080
2171
  const traceId = logEntry.traceId != null ? String(logEntry.traceId).trim() : ''
2081
2172
  const spanId = logEntry.spanId != null ? String(logEntry.spanId).trim() : ''
2173
+ const msgNorm = _normalizeReqResMessageForDedupe(msg)
2174
+ const msgSig = msgNorm.length > 200 ? msgNorm.slice(0, 200) : msgNorm
2082
2175
  if (isReqRes) {
2083
- const kind = msgLower.includes('[request]') ? 'REQUEST' : 'RESPONSE'
2084
- if (traceId && spanId) return `r|${serviceName}|${traceId}|${spanId}|${kind}`
2176
+ const kind = msgLower.startsWith('[request]')
2177
+ ? 'REQUEST'
2178
+ : msgLower.startsWith('[response]')
2179
+ ? 'RESPONSE'
2180
+ : (msgLower.includes('[request]') ? 'REQUEST' : 'RESPONSE')
2181
+ const httpFp = _httpClientDedupeFingerprint(logEntry)
2182
+ if (httpFp) {
2183
+ if (traceId) return `rh|${serviceName}|${traceId}|${httpFp}`
2184
+ const requestId = (logEntry.requestId != null ? String(logEntry.requestId).trim() : '') || ''
2185
+ if (requestId) return `rh|${serviceName}|${requestId}|${httpFp}`
2186
+ return null
2187
+ }
2188
+ if (traceId && spanId) return `r|${serviceName}|${traceId}|${spanId}|${kind}|${msgSig}`
2085
2189
  const ts = logEntry['@timestamp'] != null ? Number(logEntry['@timestamp']) : Date.now()
2086
2190
  const bucket = Math.floor(ts / _logDedupeBucketMs) * _logDedupeBucketMs
2087
- return `r|${serviceName}|${bucket}|${kind}`
2191
+ return `r|${serviceName}|${bucket}|${kind}|${msgSig}`
2088
2192
  }
2089
2193
  if (traceId && spanId) return `t|${serviceName}|${traceId}|${spanId}|${msg}`
2090
2194
  const requestId = (logEntry.requestId != null ? String(logEntry.requestId).trim() : '') || ''
2091
- if (!requestId) return null
2195
+ if (!requestId) {
2196
+ const ts = logEntry['@timestamp'] != null ? Number(logEntry['@timestamp']) : Date.now()
2197
+ const bucket = Math.floor(ts / _logDedupeGenericBucketMs) * _logDedupeGenericBucketMs
2198
+ const msgSig = msg.length > 200 ? msg.slice(0, 200) : msg
2199
+ return `g|${serviceName}|${bucket}|${msgSig}`
2200
+ }
2092
2201
  const ts = logEntry['@timestamp'] != null ? Number(logEntry['@timestamp']) : Date.now()
2093
2202
  const bucket = Math.floor(ts / _logDedupeBucketMs) * _logDedupeBucketMs
2094
2203
  return `b|${serviceName}|${requestId}|${msg}|${bucket}`
package/store.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ export type AzifyRequestContext = {
2
+ traceId: string
3
+ spanId: string
4
+ parentSpanId: string | null
5
+ requestId: string | null
6
+ }
7
+
8
+ export type AzifyJobTraceRecord = {
9
+ traceId: string
10
+ spanId?: string | null
11
+ parentSpanId?: string | null
12
+ requestId?: string | null
13
+ }
14
+
15
+ export declare const AZIFY_JOB_TRACE_FIELD: '__azifyTraceContext'
16
+
17
+ export function prepareBullJobTraceContext(data: unknown): {
18
+ cleanedForLog: unknown
19
+ ctx: AzifyRequestContext
20
+ dataForDownstream: unknown
21
+ }
22
+
23
+ export function captureOriginTraceMeta(): {
24
+ originTraceId?: string
25
+ originRequestId?: string
26
+ }
27
+
28
+ export function addCurrentTraceToBullJobPayload<T>(data: T): T
29
+
30
+ export function traceAttachmentForBullJob(): Record<string, unknown>
31
+
32
+ export function runBullJobHandlerInTraceContext<TJob extends { data: unknown }, R>(
33
+ job: TJob,
34
+ handler: (job: TJob) => R,
35
+ ): R
36
+
37
+ export function runInBullJobTraceContext<T>(
38
+ jobData: unknown,
39
+ fn: (x: {
40
+ ctx: AzifyRequestContext
41
+ cleanedForLog: unknown
42
+ dataForDownstream: unknown
43
+ }) => T,
44
+ ): T
45
+
46
+ export function runNestBullJobWithTrace<TJob extends { data: unknown }, R>(
47
+ job: TJob,
48
+ fn: (x: {
49
+ job: TJob
50
+ ctx: AzifyRequestContext
51
+ cleanedForLog: unknown
52
+ dataForDownstream: unknown
53
+ }) => R,
54
+ ): R
55
+
56
+ export function startRequestContext(
57
+ initial?: Record<string, unknown>,
58
+ ): AzifyRequestContext
59
+ export function runWithRequestContext<T>(ctx: AzifyRequestContext, fn: () => T): T
60
+ export function getRequestContext(): AzifyRequestContext | null
61
+ export function getActiveRequestId(): string | null
62
+ export function getLastJobContext(): AzifyRequestContext | null
63
+ export function setLastJobContext(ctx: AzifyRequestContext | null): void
64
+ export function mergeTraceIntoJobPayload(
65
+ data: unknown,
66
+ traceContext: AzifyJobTraceRecord | null,
67
+ ): unknown
68
+ export function toTraceIdHex(hex32: string | null | undefined): string
package/store.js CHANGED
@@ -1,5 +1,7 @@
1
1
  const { AsyncLocalStorage } = require('async_hooks')
2
2
 
3
+ const AZIFY_JOB_TRACE_FIELD = '__azifyTraceContext'
4
+
3
5
  const als = new AsyncLocalStorage()
4
6
  let lastJobContext = null
5
7
 
@@ -21,6 +23,16 @@ function fastGenerateId(length = 16) {
21
23
  return result
22
24
  }
23
25
 
26
+ function newRequestIdForJob() {
27
+ try {
28
+ const { randomUUID } = require('crypto')
29
+ if (typeof randomUUID === 'function') {
30
+ return randomUUID()
31
+ }
32
+ } catch (_) {}
33
+ return `${fastGenerateId(16)}-${fastGenerateId(8)}`
34
+ }
35
+
24
36
  function toTraceIdHex(hex32) {
25
37
  const raw = hex32 != null && typeof hex32 === 'string' ? String(hex32).replace(/[^0-9a-fA-F]/g, '').slice(0, 32) : ''
26
38
  const hex = raw || fastGenerateId(16)
@@ -47,6 +59,29 @@ function getRequestContext() {
47
59
  return als.getStore() || null
48
60
  }
49
61
 
62
+ function getActiveRequestId() {
63
+ const ctx = getRequestContext()
64
+ if (!ctx?.requestId || String(ctx.requestId) === '') return null
65
+ return String(ctx.requestId)
66
+ }
67
+
68
+ function captureOriginTraceMeta() {
69
+ try {
70
+ const ctx = getRequestContext() || getLastJobContext()
71
+ if (!ctx || typeof ctx !== 'object') return {}
72
+ const out = {}
73
+ if (ctx.traceId != null && String(ctx.traceId) !== '') {
74
+ out.originTraceId = String(ctx.traceId)
75
+ }
76
+ if (ctx.requestId != null && String(ctx.requestId) !== '') {
77
+ out.originRequestId = String(ctx.requestId)
78
+ }
79
+ return out
80
+ } catch (_) {
81
+ return {}
82
+ }
83
+ }
84
+
50
85
  function mergeTraceIntoJobPayload(data, traceContext) {
51
86
  if (!traceContext || !traceContext.traceId) return data
52
87
  const base = data && typeof data === 'object' && !Array.isArray(data) ? data : {}
@@ -59,14 +94,132 @@ function mergeTraceIntoJobPayload(data, traceContext) {
59
94
  }
60
95
  }
61
96
 
97
+ function normalizedJobTrace(raw) {
98
+ if (!raw || typeof raw !== 'object' || typeof raw.traceId !== 'string') return null
99
+ return {
100
+ traceId: String(raw.traceId),
101
+ spanId: raw.spanId != null ? raw.spanId : null,
102
+ parentSpanId: raw.parentSpanId != null ? raw.parentSpanId : null,
103
+ requestId: raw.requestId != null && raw.requestId !== '' ? String(raw.requestId) : null
104
+ }
105
+ }
106
+
107
+ function alsContextFromJobTrace(trace) {
108
+ const fallbackRid = newRequestIdForJob()
109
+ if (!trace) {
110
+ return startRequestContext({ requestId: fallbackRid })
111
+ }
112
+ const traceHex = String(trace.traceId).replace(/[^0-9a-fA-F]/g, '').slice(0, 32)
113
+ const spanHex = trace.spanId
114
+ ? String(trace.spanId).replace(/[^0-9a-fA-F]/g, '').slice(0, 16)
115
+ : undefined
116
+ return startRequestContext({
117
+ traceHex: traceHex || undefined,
118
+ spanHex: spanHex || undefined,
119
+ parentSpanId: trace.parentSpanId != null && trace.parentSpanId !== '' ? String(trace.parentSpanId) : undefined,
120
+ requestId: trace.requestId || fallbackRid
121
+ })
122
+ }
123
+
124
+ function captureTraceForJobEnqueue() {
125
+ try {
126
+ const ctx = getRequestContext() || getLastJobContext()
127
+ if (!ctx || typeof ctx !== 'object' || ctx.traceId == null || String(ctx.traceId) === '') {
128
+ return null
129
+ }
130
+ return normalizedJobTrace({
131
+ traceId: String(ctx.traceId),
132
+ spanId: ctx.spanId,
133
+ parentSpanId: ctx.parentSpanId,
134
+ requestId: ctx.requestId
135
+ })
136
+ } catch (_) {
137
+ return null
138
+ }
139
+ }
140
+
141
+ function attachTraceToBullJobData(data, traceRecord) {
142
+ if (!traceRecord || !traceRecord.traceId) return data
143
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return data
144
+ return {
145
+ ...data,
146
+ [AZIFY_JOB_TRACE_FIELD]: traceRecord
147
+ }
148
+ }
149
+
150
+ function addCurrentTraceToBullJobPayload(data) {
151
+ const trace = captureTraceForJobEnqueue()
152
+ if (!trace) return data
153
+ return attachTraceToBullJobData(data, trace)
154
+ }
155
+
156
+ function traceAttachmentForBullJob() {
157
+ const trace = captureTraceForJobEnqueue()
158
+ if (!trace) return {}
159
+ return { [AZIFY_JOB_TRACE_FIELD]: trace }
160
+ }
161
+
162
+ function prepareBullJobTraceContext(data) {
163
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
164
+ const ctx = alsContextFromJobTrace(null)
165
+ return { cleanedForLog: data, ctx, dataForDownstream: data }
166
+ }
167
+ const rawTrace = data[AZIFY_JOB_TRACE_FIELD]
168
+ const hasKey = Object.prototype.hasOwnProperty.call(data, AZIFY_JOB_TRACE_FIELD)
169
+ let cleaned
170
+ if (hasKey) {
171
+ const copy = { ...data }
172
+ delete copy[AZIFY_JOB_TRACE_FIELD]
173
+ cleaned = copy
174
+ } else {
175
+ cleaned = { ...data }
176
+ }
177
+ const trace = normalizedJobTrace(rawTrace)
178
+ const ctx = alsContextFromJobTrace(trace)
179
+ const dataForDownstream = trace
180
+ ? { ...cleaned, [AZIFY_JOB_TRACE_FIELD]: trace }
181
+ : cleaned
182
+ return { cleanedForLog: cleaned, ctx, dataForDownstream }
183
+ }
184
+
185
+ function runBullJobHandlerInTraceContext(job, handler) {
186
+ const { ctx, cleanedForLog } = prepareBullJobTraceContext(job.data)
187
+ const nextJob =
188
+ cleanedForLog !== job.data ? { ...job, data: cleanedForLog } : job
189
+ return runWithRequestContext(ctx, () => handler(nextJob))
190
+ }
191
+
192
+ function runInBullJobTraceContext(jobData, fn) {
193
+ const { ctx, cleanedForLog, dataForDownstream } =
194
+ prepareBullJobTraceContext(jobData)
195
+ return runWithRequestContext(ctx, () =>
196
+ fn({ ctx, cleanedForLog, dataForDownstream }),
197
+ )
198
+ }
199
+
200
+ function runNestBullJobWithTrace(job, fn) {
201
+ return runInBullJobTraceContext(job.data, (bundle) =>
202
+ fn({ job, ...bundle }),
203
+ )
204
+ }
205
+
62
206
  module.exports = {
63
207
  als,
208
+ AZIFY_JOB_TRACE_FIELD,
64
209
  startRequestContext,
65
210
  runWithRequestContext,
66
211
  getRequestContext,
212
+ getActiveRequestId,
67
213
  getLastJobContext,
68
214
  setLastJobContext,
69
215
  mergeTraceIntoJobPayload,
216
+ prepareBullJobTraceContext,
217
+ runBullJobHandlerInTraceContext,
218
+ runInBullJobTraceContext,
219
+ runNestBullJobWithTrace,
220
+ addCurrentTraceToBullJobPayload,
221
+ traceAttachmentForBullJob,
222
+ captureOriginTraceMeta,
70
223
  toTraceIdHex
71
224
  }
72
225