@open-mercato/queue 0.6.7-develop.6828.1.ab1620a63e → 0.6.7-develop.6842.1.8cd8a8883c

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.
@@ -3,6 +3,7 @@ import path from 'node:path'
3
3
  import crypto from 'node:crypto'
4
4
  import { createLogger } from '@open-mercato/shared/lib/logger'
5
5
  import type { Queue, QueuedJob, JobHandler, LocalQueueOptions, ProcessOptions, ProcessResult, EnqueueOptions, QueueJobScope } from '../types'
6
+ import { attachTraceMetadata, runJobInTrace } from '../tracing'
6
7
 
7
8
  const packageLogger = createLogger('queue')
8
9
 
@@ -205,11 +206,13 @@ export function createLocalQueue<T = unknown>(
205
206
  const availableAt = options?.delayMs && options.delayMs > 0
206
207
  ? new Date(Date.now() + options.delayMs).toISOString()
207
208
  : undefined
209
+ const metadata = attachTraceMetadata(undefined)
208
210
  const job: StoredJob<T> = {
209
211
  id: generateId(),
210
212
  payload: data,
211
213
  createdAt: new Date().toISOString(),
212
214
  ...(availableAt ? { availableAt } : {}),
215
+ ...(metadata ? { metadata } : {}),
213
216
  }
214
217
  await withFileLock(async () => {
215
218
  const jobs = await readQueue()
@@ -255,12 +258,14 @@ export function createLocalQueue<T = unknown>(
255
258
  for (const job of jobsToProcess) {
256
259
  const attemptNumber = (job.attemptCount ?? 0) + 1
257
260
  try {
258
- await Promise.resolve(
259
- handler(job, {
260
- jobId: job.id,
261
- attemptNumber,
262
- queueName: name,
263
- })
261
+ await runJobInTrace(name, job.metadata, () =>
262
+ Promise.resolve(
263
+ handler(job, {
264
+ jobId: job.id,
265
+ attemptNumber,
266
+ queueName: name,
267
+ })
268
+ )
264
269
  )
265
270
  processed++
266
271
  lastJobId = job.id
package/src/tracing.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
2
+
3
+ /**
4
+ * Distributed-trace propagation across the enqueue → worker boundary.
5
+ *
6
+ * The W3C trace carrier rides on the job's `metadata._trace` (a first-class
7
+ * metadata channel, not the user payload). Both halves are automatic — the
8
+ * strategies call `attachTraceMetadata` at enqueue and `runJobInTrace` at
9
+ * dispatch — so a worker joins the enqueuing request's trace with no per-worker
10
+ * code. Everything here is a cheap no-op when telemetry is off.
11
+ *
12
+ * This also covers anything that rides the queue: persistent event subscribers
13
+ * (the event bus enqueues) and outbound webhook delivery (queued) become part of
14
+ * the originating request's trace for free.
15
+ */
16
+ const TRACE_META_KEY = '_trace'
17
+
18
+ /**
19
+ * Attach the active trace context to a job's metadata. Returns `metadata`
20
+ * unchanged when telemetry is off (no active span → empty carrier), so jobs stay
21
+ * clean unless tracing is active.
22
+ */
23
+ export function attachTraceMetadata(
24
+ metadata: Record<string, unknown> | undefined,
25
+ ): Record<string, unknown> | undefined {
26
+ const carrier = getTelemetryRuntime()?.captureTraceContext() ?? {}
27
+ if (Object.keys(carrier).length === 0) return metadata
28
+ return { ...(metadata ?? {}), [TRACE_META_KEY]: carrier }
29
+ }
30
+
31
+ /**
32
+ * Run a job handler inside a span (`queue.<queueName>`) that continues the
33
+ * producer's trace from the carrier on `metadata`. With no carrier (or telemetry
34
+ * off) it runs `fn` under a fresh root span — and a no-op when off. The span
35
+ * ends when `fn` settles (sync or async).
36
+ */
37
+ export function runJobInTrace<T>(
38
+ queueName: string,
39
+ metadata: Record<string, unknown> | undefined,
40
+ fn: () => T,
41
+ ): T {
42
+ const runtime = getTelemetryRuntime()
43
+ if (!runtime) return fn()
44
+ return runtime.continueTrace(
45
+ readTraceCarrier(metadata),
46
+ `queue.${queueName}`,
47
+ fn,
48
+ { kind: 'consumer' },
49
+ )
50
+ }
51
+
52
+ function readTraceCarrier(
53
+ metadata: Record<string, unknown> | undefined,
54
+ ): Record<string, string> | undefined {
55
+ const raw = metadata?.[TRACE_META_KEY]
56
+ if (!raw || typeof raw !== 'object') return undefined
57
+ return raw as Record<string, string>
58
+ }
package/src/types.ts CHANGED
@@ -81,6 +81,8 @@ export type RedisConnectionOptions = {
81
81
  db?: number
82
82
  /** TLS configuration for rediss / encrypted Redis */
83
83
  tls?: Record<string, unknown>
84
+ /** IP family used by Redis DNS resolution */
85
+ family?: number
84
86
  }
85
87
 
86
88
  /**
@@ -1,5 +1,9 @@
1
1
  import { createQueue } from '../factory'
2
2
  import { createLogger } from '@open-mercato/shared/lib/logger'
3
+ import {
4
+ getTelemetryRuntime,
5
+ isTelemetryBackendEnabled,
6
+ } from '@open-mercato/shared/lib/telemetry/runtime'
3
7
  import type { Queue, JobHandler, AsyncQueueOptions, QueueStrategyType } from '../types'
4
8
 
5
9
  const logger = createLogger('queue').child({ component: 'worker' })
@@ -71,6 +75,17 @@ function registerShutdownHandlers(): void {
71
75
  unregisterShutdownHandlers(sigtermHandler, sigintHandler)
72
76
  shutdownInProgress = false
73
77
 
78
+ // Flush buffered spans/logs before the process dies. A worker never returns
79
+ // from run(), so bin.ts's post-run shutdownTelemetry() is unreachable on this
80
+ // path — without this, the BatchSpanProcessor's ~5s tail is dropped on every
81
+ // restart/redeploy. Idempotent and a no-op when telemetry is off; a flush
82
+ // failure must not turn a clean shutdown into a failed one.
83
+ try {
84
+ await getTelemetryRuntime()?.shutdown()
85
+ } catch (error) {
86
+ logger.error('Error flushing telemetry during shutdown', { err: error })
87
+ }
88
+
74
89
  if (!hasError) {
75
90
  logger.info('Worker closed successfully')
76
91
  }
@@ -141,6 +156,15 @@ export async function runWorker<T = unknown>(
141
156
  strategy: strategyOption,
142
157
  } = options
143
158
 
159
+ // Worker processes don't run Next's instrumentation hook, so initialize
160
+ // telemetry here — this is the single bootstrap every standalone worker passes
161
+ // through. Import the telemetry package only for an explicit enabled backend;
162
+ // with the default/unset backend the worker never evaluates the package.
163
+ if (!getTelemetryRuntime() && isTelemetryBackendEnabled()) {
164
+ const { initTelemetry } = await import('@open-mercato/telemetry')
165
+ await initTelemetry()
166
+ }
167
+
144
168
  // Determine queue strategy from option, env var, or default to 'local'
145
169
  const strategy: QueueStrategyType = strategyOption
146
170
  ?? (process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local')