dspx 0.1.1-alpha.0

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.
Files changed (172) hide show
  1. package/.github/workflows/ci.yml +185 -0
  2. package/.vscode/c_cpp_properties.json +17 -0
  3. package/.vscode/settings.json +68 -0
  4. package/.vscode/tasks.json +28 -0
  5. package/DISCLAIMER.md +32 -0
  6. package/LICENSE +21 -0
  7. package/README.md +1803 -0
  8. package/ROADMAP.md +192 -0
  9. package/TECHNICAL_DEBT.md +165 -0
  10. package/binding.gyp +65 -0
  11. package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
  12. package/docs/AUTHENTICATION_SECURITY.md +396 -0
  13. package/docs/BACKEND_IMPROVEMENTS.md +399 -0
  14. package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
  15. package/docs/FFT_IMPLEMENTATION.md +490 -0
  16. package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
  17. package/docs/FFT_USER_GUIDE.md +494 -0
  18. package/docs/FILTERS_IMPLEMENTATION.md +260 -0
  19. package/docs/FILTER_API_GUIDE.md +418 -0
  20. package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
  21. package/docs/LOGGER_API_REFERENCE.md +350 -0
  22. package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
  23. package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
  24. package/docs/PHASES_5_7_SUMMARY.md +403 -0
  25. package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
  26. package/docs/SIMD_OPTIMIZATIONS.md +211 -0
  27. package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
  28. package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
  29. package/docs/TIMESERIES_QUICK_REF.md +85 -0
  30. package/docs/advanced.md +559 -0
  31. package/docs/time-series-guide.md +617 -0
  32. package/docs/time-series-migration.md +376 -0
  33. package/jest.config.js +37 -0
  34. package/package.json +42 -0
  35. package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
  36. package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
  37. package/scripts/test.js +24 -0
  38. package/src/build/dsp-ts-redis.node +0 -0
  39. package/src/native/DspPipeline.cc +675 -0
  40. package/src/native/DspPipeline.h +44 -0
  41. package/src/native/FftBindings.cc +817 -0
  42. package/src/native/FilterBindings.cc +1001 -0
  43. package/src/native/IDspStage.h +53 -0
  44. package/src/native/adapters/InterpolatorStage.h +201 -0
  45. package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
  46. package/src/native/adapters/MovingAverageStage.h +306 -0
  47. package/src/native/adapters/RectifyStage.h +88 -0
  48. package/src/native/adapters/ResamplerStage.h +238 -0
  49. package/src/native/adapters/RmsStage.h +299 -0
  50. package/src/native/adapters/SscStage.h +121 -0
  51. package/src/native/adapters/VarianceStage.h +307 -0
  52. package/src/native/adapters/WampStage.h +114 -0
  53. package/src/native/adapters/WaveformLengthStage.h +115 -0
  54. package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
  55. package/src/native/core/FftEngine.cc +441 -0
  56. package/src/native/core/FftEngine.h +224 -0
  57. package/src/native/core/FirFilter.cc +324 -0
  58. package/src/native/core/FirFilter.h +149 -0
  59. package/src/native/core/IirFilter.cc +576 -0
  60. package/src/native/core/IirFilter.h +210 -0
  61. package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
  62. package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
  63. package/src/native/core/MovingAverageFilter.cc +18 -0
  64. package/src/native/core/MovingAverageFilter.h +135 -0
  65. package/src/native/core/MovingFftFilter.cc +291 -0
  66. package/src/native/core/MovingFftFilter.h +203 -0
  67. package/src/native/core/MovingVarianceFilter.cc +194 -0
  68. package/src/native/core/MovingVarianceFilter.h +114 -0
  69. package/src/native/core/MovingZScoreFilter.cc +215 -0
  70. package/src/native/core/MovingZScoreFilter.h +113 -0
  71. package/src/native/core/Policies.h +352 -0
  72. package/src/native/core/RmsFilter.cc +18 -0
  73. package/src/native/core/RmsFilter.h +131 -0
  74. package/src/native/core/SscFilter.cc +16 -0
  75. package/src/native/core/SscFilter.h +137 -0
  76. package/src/native/core/WampFilter.cc +16 -0
  77. package/src/native/core/WampFilter.h +101 -0
  78. package/src/native/core/WaveformLengthFilter.cc +17 -0
  79. package/src/native/core/WaveformLengthFilter.h +98 -0
  80. package/src/native/utils/CircularBufferArray.cc +336 -0
  81. package/src/native/utils/CircularBufferArray.h +62 -0
  82. package/src/native/utils/CircularBufferVector.cc +145 -0
  83. package/src/native/utils/CircularBufferVector.h +45 -0
  84. package/src/native/utils/NapiUtils.cc +53 -0
  85. package/src/native/utils/NapiUtils.h +21 -0
  86. package/src/native/utils/SimdOps.h +870 -0
  87. package/src/native/utils/SlidingWindowFilter.cc +239 -0
  88. package/src/native/utils/SlidingWindowFilter.h +159 -0
  89. package/src/native/utils/TimeSeriesBuffer.cc +205 -0
  90. package/src/native/utils/TimeSeriesBuffer.h +140 -0
  91. package/src/ts/CircularLogBuffer.ts +87 -0
  92. package/src/ts/DriftDetector.ts +331 -0
  93. package/src/ts/TopicRouter.ts +428 -0
  94. package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
  95. package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
  96. package/src/ts/__tests__/Chaining.test.ts +387 -0
  97. package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
  98. package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
  99. package/src/ts/__tests__/DriftDetector.test.ts +389 -0
  100. package/src/ts/__tests__/Fft.test.ts +484 -0
  101. package/src/ts/__tests__/ListState.test.ts +153 -0
  102. package/src/ts/__tests__/Logger.test.ts +208 -0
  103. package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
  104. package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
  105. package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
  106. package/src/ts/__tests__/MovingAverage.test.ts +322 -0
  107. package/src/ts/__tests__/RMS.test.ts +315 -0
  108. package/src/ts/__tests__/Rectify.test.ts +272 -0
  109. package/src/ts/__tests__/Redis.test.ts +456 -0
  110. package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
  111. package/src/ts/__tests__/Tap.test.ts +164 -0
  112. package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
  113. package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
  114. package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
  115. package/src/ts/__tests__/TimeSeries.test.ts +254 -0
  116. package/src/ts/__tests__/TopicRouter.test.ts +332 -0
  117. package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
  118. package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
  119. package/src/ts/__tests__/Variance.test.ts +509 -0
  120. package/src/ts/__tests__/WaveformLength.test.ts +147 -0
  121. package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
  122. package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
  123. package/src/ts/advanced-dsp.ts +566 -0
  124. package/src/ts/backends.ts +1137 -0
  125. package/src/ts/bindings.ts +1225 -0
  126. package/src/ts/easter-egg.ts +42 -0
  127. package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
  128. package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
  129. package/src/ts/examples/MovingAverage/test-state.ts +85 -0
  130. package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
  131. package/src/ts/examples/RMS/test-state.ts +97 -0
  132. package/src/ts/examples/RMS/test-streaming.ts +253 -0
  133. package/src/ts/examples/Rectify/test-state.ts +107 -0
  134. package/src/ts/examples/Rectify/test-streaming.ts +242 -0
  135. package/src/ts/examples/Variance/test-state.ts +195 -0
  136. package/src/ts/examples/Variance/test-streaming.ts +260 -0
  137. package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
  138. package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
  139. package/src/ts/examples/advanced-dsp-examples.ts +397 -0
  140. package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
  141. package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
  142. package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
  143. package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
  144. package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
  145. package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
  146. package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
  147. package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
  148. package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
  149. package/src/ts/examples/chaining/test-chaining.ts +52 -0
  150. package/src/ts/examples/emg-features-example.ts +284 -0
  151. package/src/ts/examples/fft-example.ts +309 -0
  152. package/src/ts/examples/fft-examples.ts +349 -0
  153. package/src/ts/examples/filter-examples.ts +320 -0
  154. package/src/ts/examples/list-state-example.ts +131 -0
  155. package/src/ts/examples/logger-example.ts +91 -0
  156. package/src/ts/examples/notch-filter-examples.ts +243 -0
  157. package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
  158. package/src/ts/examples/phase6-7/production-observability.ts +476 -0
  159. package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
  160. package/src/ts/examples/redis/redis-example.ts +202 -0
  161. package/src/ts/examples/redis-example.ts +202 -0
  162. package/src/ts/examples/simd-benchmark.ts +126 -0
  163. package/src/ts/examples/tap-debugging.ts +230 -0
  164. package/src/ts/examples/timeseries/comparison-example.ts +290 -0
  165. package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
  166. package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
  167. package/src/ts/examples/waveform-length-example.ts +139 -0
  168. package/src/ts/fft.ts +722 -0
  169. package/src/ts/filters.ts +1078 -0
  170. package/src/ts/index.ts +120 -0
  171. package/src/ts/types.ts +589 -0
  172. package/tsconfig.json +15 -0
@@ -0,0 +1,1137 @@
1
+ /**
2
+ * Pluggable backend handlers for common observability systems
3
+ *
4
+ * Production-ready integrations for:
5
+ * - PagerDuty (alerting)
6
+ * - Prometheus (metrics)
7
+ * - Loki (centralized logging)
8
+ * - CloudWatch (AWS)
9
+ * - Datadog (observability platform)
10
+ *
11
+ * Features:
12
+ * - Pluggable formatters (JSON, text, custom)
13
+ * - Distributed tracing support (trace/span/correlation IDs)
14
+ * - Graceful shutdown with flush hooks
15
+ * - Extended log levels (trace -> fatal)
16
+ * - Internal performance metrics
17
+ */
18
+
19
+ import type { LogEntry, LogLevel } from "./types.js";
20
+ import { AsyncLocalStorage } from "node:async_hooks";
21
+
22
+ /**
23
+ * Formatter interface for encoding log entries
24
+ */
25
+ export interface Formatter {
26
+ format(log: LogEntry): any;
27
+ }
28
+
29
+ /**
30
+ * JSON formatter (default)
31
+ */
32
+ export class JSONFormatter implements Formatter {
33
+ format(log: LogEntry): any {
34
+ return log;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Text formatter for human-readable output
40
+ */
41
+ export class TextFormatter implements Formatter {
42
+ format(log: LogEntry): string {
43
+ const timestamp = new Date(log.timestamp).toISOString();
44
+ const level = log.level.toUpperCase().padEnd(5);
45
+ const topic = log.topic || "default";
46
+ const traceInfo = log.traceId ? ` [trace:${log.traceId.slice(0, 8)}]` : "";
47
+
48
+ let output = `[${timestamp}] ${level} [${topic}]${traceInfo} ${log.message}`;
49
+
50
+ if (log.context && Object.keys(log.context).length > 0) {
51
+ try {
52
+ output += `\n Context: ${JSON.stringify(log.context)}`;
53
+ } catch (error) {
54
+ // Handle circular references or non-serializable values
55
+ output += `\n Context: [Unable to stringify: ${
56
+ error instanceof Error ? error.message : "unknown error"
57
+ }]`;
58
+ }
59
+ }
60
+
61
+ return output;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Severity mapping for different observability systems
67
+ */
68
+ export interface SeverityMapping {
69
+ trace?: string;
70
+ debug?: string;
71
+ info?: string;
72
+ warn?: string;
73
+ error?: string;
74
+ fatal?: string;
75
+ }
76
+
77
+ /**
78
+ * Default severity mappings for common systems
79
+ */
80
+ export const SEVERITY_MAPPINGS = {
81
+ pagerduty: {
82
+ trace: "info",
83
+ debug: "info",
84
+ info: "info",
85
+ warn: "warning",
86
+ error: "error",
87
+ fatal: "critical",
88
+ },
89
+ datadog: {
90
+ trace: "debug",
91
+ debug: "debug",
92
+ info: "info",
93
+ warn: "warn",
94
+ error: "error",
95
+ fatal: "emergency",
96
+ },
97
+ syslog: {
98
+ trace: "7", // Debug
99
+ debug: "7", // Debug
100
+ info: "6", // Informational
101
+ warn: "4", // Warning
102
+ error: "3", // Error
103
+ fatal: "0", // Emergency
104
+ },
105
+ };
106
+
107
+ /**
108
+ * Async context for distributed tracing
109
+ */
110
+ export const tracingContext = new AsyncLocalStorage<{
111
+ traceId?: string;
112
+ spanId?: string;
113
+ correlationId?: string;
114
+ }>();
115
+
116
+ /**
117
+ * Helper to get current tracing context
118
+ */
119
+ export function getTracingContext() {
120
+ return tracingContext.getStore() || {};
121
+ }
122
+
123
+ /**
124
+ * Helper to run code with tracing context
125
+ */
126
+ export function withTracingContext<T>(
127
+ context: { traceId?: string; spanId?: string; correlationId?: string },
128
+ fn: () => T
129
+ ): T {
130
+ return tracingContext.run(context, fn);
131
+ }
132
+
133
+ /**
134
+ * Performance metrics for internal instrumentation
135
+ */
136
+ export interface LoggerMetrics {
137
+ logsProcessed: number;
138
+ logsFailed: number;
139
+ totalRetries: number;
140
+ flushCount: number;
141
+ averageFlushTimeMs: number;
142
+ queueSize: number;
143
+ handlerErrors: Map<string, number>;
144
+ }
145
+
146
+ /**
147
+ * Handler with optional flush capability
148
+ */
149
+ export interface HandlerWithFlush {
150
+ (log: LogEntry): Promise<void> | void;
151
+ flush?: () => Promise<void>;
152
+ metrics?: () => LoggerMetrics;
153
+ }
154
+
155
+ /**
156
+ * Schema version for log payloads
157
+ */
158
+ const SCHEMA_VERSION = "dspx/log/v1";
159
+
160
+ /**
161
+ * Normalize timestamps for different observability systems
162
+ * @param timestamp Unix timestamp (milliseconds or seconds)
163
+ * @param format Target format: 'ms' | 's' | 'ns'
164
+ * @returns Normalized timestamp
165
+ */
166
+ function normalizeTimestamp(
167
+ timestamp: number,
168
+ format: "ms" | "s" | "ns"
169
+ ): number {
170
+ // Assume input is milliseconds if > 1e12, otherwise seconds
171
+ const ms = timestamp > 1e12 ? timestamp : timestamp * 1000;
172
+
173
+ switch (format) {
174
+ case "s":
175
+ return Math.floor(ms / 1000);
176
+ case "ms":
177
+ return Math.floor(ms);
178
+ case "ns":
179
+ return Math.floor(ms * 1000000);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Sleep utility for retry backoff
185
+ */
186
+ function sleep(ms: number): Promise<void> {
187
+ return new Promise((resolve) => setTimeout(resolve, ms));
188
+ }
189
+
190
+ /**
191
+ * Retry an async operation with exponential backoff
192
+ * @param fn Function to retry
193
+ * @param maxAttempts Maximum number of attempts (default: 3)
194
+ * @param onRetry Optional callback on each retry
195
+ * @returns Result of successful attempt
196
+ */
197
+ async function retryWithBackoff<T>(
198
+ fn: () => Promise<T>,
199
+ maxAttempts: number = 3,
200
+ onRetry?: () => void
201
+ ): Promise<T> {
202
+ let lastError: Error | undefined;
203
+
204
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
205
+ try {
206
+ return await fn();
207
+ } catch (error) {
208
+ lastError = error instanceof Error ? error : new Error(String(error));
209
+
210
+ if (attempt < maxAttempts - 1) {
211
+ // Call retry callback if provided
212
+ if (onRetry) {
213
+ onRetry();
214
+ }
215
+
216
+ // Exponential backoff with jitter: 100ms, 400ms, 1600ms
217
+ const delay = Math.pow(2, attempt) * 100 + Math.random() * 50;
218
+ await sleep(delay);
219
+ }
220
+ }
221
+ }
222
+
223
+ throw lastError || new Error("Retry failed");
224
+ }
225
+
226
+ /**
227
+ * Generate W3C traceparent header from trace/span IDs
228
+ * Format: 00-{trace_id}-{span_id}-01
229
+ */
230
+ export function generateTraceparent(
231
+ traceId?: string,
232
+ spanId?: string
233
+ ): string | undefined {
234
+ if (!traceId || !spanId) return undefined;
235
+ return `00-${traceId}-${spanId}-01`;
236
+ }
237
+
238
+ /**
239
+ * Shared HTTP transport utility
240
+ * @param url Request URL
241
+ * @param body Request body (will be JSON stringified)
242
+ * @param headers Optional headers
243
+ * @param method HTTP method (default: POST)
244
+ * @returns Response
245
+ */
246
+ /**
247
+ * Shared HTTP transport utility
248
+ * @param url Request URL
249
+ * @param body Request body (will be JSON stringified)
250
+ * @param headers Optional headers
251
+ * @param method HTTP method (default: POST)
252
+ * @param log Optional log entry for trace context
253
+ * @returns Response
254
+ */
255
+ async function postJSON(
256
+ url: string,
257
+ body: any,
258
+ headers?: Record<string, string>,
259
+ method: string = "POST",
260
+ log?: LogEntry
261
+ ): Promise<Response> {
262
+ const finalHeaders: Record<string, string> = {
263
+ "Content-Type": "application/json",
264
+ ...headers,
265
+ };
266
+
267
+ // Add W3C traceparent header if trace context available
268
+ if (log?.traceId && log?.spanId) {
269
+ const traceparent = generateTraceparent(log.traceId, log.spanId);
270
+ if (traceparent) {
271
+ finalHeaders["traceparent"] = traceparent;
272
+ }
273
+ }
274
+
275
+ const response = await fetch(url, {
276
+ method,
277
+ headers: finalHeaders,
278
+ body: JSON.stringify(body),
279
+ });
280
+
281
+ if (!response.ok) {
282
+ throw new Error(`HTTP ${response.status} - ${response.statusText}`);
283
+ }
284
+
285
+ return response;
286
+ }
287
+
288
+ /**
289
+ * Concurrency limiter using p-limit pattern
290
+ */
291
+ class ConcurrencyLimiter {
292
+ private queue: Array<() => void> = [];
293
+ private active = 0;
294
+
295
+ constructor(private limit: number) {}
296
+
297
+ async run<T>(fn: () => Promise<T>): Promise<T> {
298
+ while (this.active >= this.limit) {
299
+ await new Promise<void>((resolve) => this.queue.push(resolve));
300
+ }
301
+
302
+ this.active++;
303
+ try {
304
+ return await fn();
305
+ } finally {
306
+ this.active--;
307
+ const next = this.queue.shift();
308
+ if (next) next();
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Global concurrency limiter (max 5 concurrent network requests)
315
+ */
316
+ const concurrencyLimiter = new ConcurrencyLimiter(5);
317
+
318
+ /**
319
+ * Backend handler configuration
320
+ */
321
+ export interface BackendConfig {
322
+ /** Endpoint URL */
323
+ endpoint?: string;
324
+ /** API key or token */
325
+ apiKey?: string;
326
+ /** Additional headers */
327
+ headers?: Record<string, string>;
328
+ /** Batch size for buffering */
329
+ batchSize?: number;
330
+ /** Flush interval in milliseconds */
331
+ flushInterval?: number;
332
+ }
333
+
334
+ /**
335
+ * PagerDuty alert handler
336
+ * Sends critical errors to PagerDuty for incident management
337
+ *
338
+ * Authentication: Requires a routing_key (Integration Key) from PagerDuty
339
+ * Regional endpoints: US (default), EU requires custom endpoint
340
+ *
341
+ * @throws Error if routing_key is invalid or endpoint is unreachable
342
+ */
343
+ export function createPagerDutyHandler(config: BackendConfig) {
344
+ const { endpoint, apiKey } = config;
345
+ const severityMap = SEVERITY_MAPPINGS.pagerduty;
346
+
347
+ if (!endpoint || !apiKey) {
348
+ console.warn(
349
+ "PagerDuty handler: endpoint or apiKey (routing_key) not configured. Logs will be dropped."
350
+ );
351
+ }
352
+
353
+ return async (log: LogEntry): Promise<void> => {
354
+ if (!endpoint || !apiKey) {
355
+ return;
356
+ }
357
+
358
+ await concurrencyLimiter.run(async () => {
359
+ await retryWithBackoff(
360
+ async () => {
361
+ const payload = {
362
+ schema: SCHEMA_VERSION,
363
+ routing_key: apiKey,
364
+ event_action: "trigger",
365
+ payload: {
366
+ summary: log.message,
367
+ severity: severityMap[log.level] || "warning",
368
+ source: log.topic || "dsp-pipeline",
369
+ timestamp: new Date(
370
+ normalizeTimestamp(log.timestamp, "ms")
371
+ ).toISOString(),
372
+ custom_details: {
373
+ ...log.context,
374
+ traceId: log.traceId,
375
+ spanId: log.spanId,
376
+ correlationId: log.correlationId,
377
+ },
378
+ },
379
+ };
380
+
381
+ try {
382
+ await postJSON(endpoint, payload, config.headers, "POST", log);
383
+ } catch (error) {
384
+ if (error instanceof Error && error.message.includes("401")) {
385
+ throw new Error(
386
+ `PagerDuty authentication failed: Invalid routing_key. Check your Integration Key.`
387
+ );
388
+ }
389
+ throw error;
390
+ }
391
+ },
392
+ 3,
393
+ // Retry callback (unused here, but could increment logger metrics)
394
+ undefined
395
+ );
396
+ });
397
+ };
398
+ }
399
+
400
+ /**
401
+ * Prometheus metrics handler
402
+ * Exports metrics to Prometheus Pushgateway
403
+ *
404
+ * Authentication: Varies by setup
405
+ * - May require HTTP Basic Auth (pass via config.headers)
406
+ * - Or rely on network-level controls (VPN, firewall)
407
+ *
408
+ * @throws Error if authentication fails or endpoint is unreachable
409
+ */
410
+ export function createPrometheusHandler(config: BackendConfig) {
411
+ const { endpoint } = config;
412
+
413
+ if (!endpoint) {
414
+ console.warn(
415
+ "Prometheus handler: endpoint not configured. Metrics will be dropped."
416
+ );
417
+ }
418
+
419
+ return async (log: LogEntry): Promise<void> => {
420
+ if (!endpoint) {
421
+ return;
422
+ }
423
+
424
+ await concurrencyLimiter.run(async () => {
425
+ await retryWithBackoff(async () => {
426
+ // Convert log to Prometheus metrics format
427
+ const metricName = log.topic?.replace(/\./g, "_") || "pipeline_metric";
428
+ const labels = Object.entries(log.context || {})
429
+ .map(([key, value]) => `${key}="${value}"`)
430
+ .join(",");
431
+
432
+ const timestamp = normalizeTimestamp(log.timestamp, "ms");
433
+ const metric = `${metricName}{${labels},schema="${SCHEMA_VERSION}"} ${
434
+ log.context?.value || 1
435
+ } ${timestamp}`;
436
+
437
+ try {
438
+ const response = await fetch(`${endpoint}/metrics/job/dsp_pipeline`, {
439
+ method: "POST",
440
+ headers: {
441
+ "Content-Type": "text/plain",
442
+ ...config.headers,
443
+ },
444
+ body: metric,
445
+ });
446
+
447
+ if (!response.ok) {
448
+ if (response.status === 401 || response.status === 403) {
449
+ throw new Error(
450
+ `Prometheus authentication failed (${response.status}). Check config.headers for HTTP Basic Auth credentials.`
451
+ );
452
+ }
453
+ throw new Error(`HTTP ${response.status} - ${response.statusText}`);
454
+ }
455
+ } catch (error) {
456
+ if (
457
+ error instanceof Error &&
458
+ error.message.includes("ECONNREFUSED")
459
+ ) {
460
+ throw new Error(
461
+ `Prometheus Pushgateway unreachable at ${endpoint}. Check network and endpoint.`
462
+ );
463
+ }
464
+ throw error;
465
+ }
466
+ });
467
+ });
468
+ };
469
+ }
470
+
471
+ /**
472
+ * Grafana Loki log handler
473
+ * Sends logs to Loki for centralized logging
474
+ *
475
+ * Authentication: Varies by setup
476
+ * - Bearer token (most common, passed as apiKey)
477
+ * - HTTP Basic Auth (pass via config.headers)
478
+ * - Multi-tenant ID in X-Scope-OrgID header (pass via config.headers)
479
+ *
480
+ * @throws Error if authentication fails or endpoint is unreachable
481
+ */
482
+ export function createLokiHandler(config: BackendConfig): HandlerWithFlush {
483
+ const { endpoint, apiKey } = config;
484
+ const buffer: LogEntry[] = [];
485
+ let flushTimer: NodeJS.Timeout | null = null;
486
+
487
+ if (!endpoint) {
488
+ console.warn(
489
+ "Loki handler: endpoint not configured. Logs will be buffered but never sent."
490
+ );
491
+ }
492
+
493
+ const flush = async (): Promise<void> => {
494
+ if (buffer.length === 0) return;
495
+
496
+ const logs = buffer.splice(0, buffer.length);
497
+
498
+ try {
499
+ await retryWithBackoff(async () => {
500
+ const streams = logs.map((log) => ({
501
+ stream: {
502
+ job: "dsp-pipeline",
503
+ level: log.level,
504
+ topic: log.topic || "unknown",
505
+ schema: SCHEMA_VERSION,
506
+ ...log.context,
507
+ },
508
+ values: [
509
+ [String(normalizeTimestamp(log.timestamp, "ns")), log.message],
510
+ ],
511
+ }));
512
+
513
+ const payload = { streams };
514
+
515
+ const headers: Record<string, string> = {
516
+ ...config.headers,
517
+ };
518
+
519
+ if (apiKey) {
520
+ headers["Authorization"] = `Bearer ${apiKey}`;
521
+ }
522
+
523
+ try {
524
+ await postJSON(`${endpoint}/loki/api/v1/push`, payload, headers);
525
+ } catch (error) {
526
+ if (error instanceof Error) {
527
+ if (
528
+ error.message.includes("401") ||
529
+ error.message.includes("403")
530
+ ) {
531
+ throw new Error(
532
+ `Loki authentication failed. Check apiKey (Bearer token) or config.headers for Basic Auth / X-Scope-OrgID.`
533
+ );
534
+ }
535
+ }
536
+ throw error;
537
+ }
538
+ });
539
+ } catch (error) {
540
+ // Requeue failed logs for next flush attempt
541
+ buffer.unshift(...logs);
542
+ console.error("Loki handler error (logs requeued):", error);
543
+ }
544
+ };
545
+
546
+ const handler: HandlerWithFlush = async (log: LogEntry): Promise<void> => {
547
+ if (!endpoint) {
548
+ return;
549
+ }
550
+
551
+ buffer.push(log);
552
+
553
+ // Auto-flush when batch size reached
554
+ if (buffer.length >= (config.batchSize || 100)) {
555
+ if (flushTimer) {
556
+ clearTimeout(flushTimer);
557
+ flushTimer = null;
558
+ }
559
+ await flush();
560
+ return;
561
+ }
562
+
563
+ // Schedule flush
564
+ if (!flushTimer) {
565
+ flushTimer = setTimeout(() => {
566
+ flushTimer = null;
567
+ flush().catch(console.error);
568
+ }, config.flushInterval || 5000);
569
+ }
570
+ };
571
+
572
+ // Attach flush method for graceful shutdown
573
+ handler.flush = flush;
574
+
575
+ return handler;
576
+ }
577
+
578
+ /**
579
+ * AWS CloudWatch Logs handler
580
+ * Sends logs to CloudWatch for AWS-native logging
581
+ *
582
+ * ⚠️ WARNING: Manual AWS authentication is error-prone!
583
+ * This implementation uses simple API key passing, which may not work
584
+ * in many AWS environments (IAM roles, instance profiles, temporary credentials).
585
+ *
586
+ * RECOMMENDED: Use @aws-sdk/client-cloudwatch-logs for automatic credential
587
+ * discovery and request signing instead of this manual approach.
588
+ *
589
+ * Authentication requirements:
590
+ * - AWS Access Key ID and Secret Access Key
591
+ * - Proper IAM permissions (logs:PutLogEvents, logs:CreateLogStream, etc.)
592
+ * - Regional endpoint configuration
593
+ *
594
+ * @deprecated Consider using AWS SDK instead for production use
595
+ * @throws Error if credentials are invalid or permissions are insufficient
596
+ */
597
+ export function createCloudWatchHandler(config: BackendConfig) {
598
+ const { endpoint, apiKey } = config;
599
+
600
+ if (!endpoint || !apiKey) {
601
+ console.warn(
602
+ "CloudWatch handler: endpoint or apiKey not configured. " +
603
+ "Note: AWS authentication is complex - consider using @aws-sdk/client-cloudwatch-logs " +
604
+ "for automatic credential discovery. Logs will be dropped."
605
+ );
606
+ }
607
+
608
+ return async (log: LogEntry): Promise<void> => {
609
+ if (!endpoint) {
610
+ return;
611
+ }
612
+
613
+ await concurrencyLimiter.run(async () => {
614
+ await retryWithBackoff(async () => {
615
+ const payload = {
616
+ schema: SCHEMA_VERSION,
617
+ logGroupName: "/dsp-pipeline/logs",
618
+ logStreamName: log.topic || "default",
619
+ logEvents: [
620
+ {
621
+ message: JSON.stringify({
622
+ level: log.level,
623
+ message: log.message,
624
+ ...log.context,
625
+ }),
626
+ timestamp: normalizeTimestamp(log.timestamp, "ms"),
627
+ },
628
+ ],
629
+ };
630
+
631
+ try {
632
+ await postJSON(endpoint, payload, {
633
+ "Content-Type": "application/x-amz-json-1.1",
634
+ "X-Amz-Target": "Logs_20140328.PutLogEvents",
635
+ Authorization: apiKey || "",
636
+ ...config.headers,
637
+ });
638
+ } catch (error) {
639
+ if (error instanceof Error) {
640
+ if (
641
+ error.message.includes("403") ||
642
+ error.message.includes("UnauthorizedOperation") ||
643
+ error.message.includes("InvalidClientTokenId")
644
+ ) {
645
+ throw new Error(
646
+ `CloudWatch authentication failed. AWS requires proper IAM credentials and request signing. ` +
647
+ `Consider using @aws-sdk/client-cloudwatch-logs instead of manual fetch. Error: ${error.message}`
648
+ );
649
+ }
650
+ }
651
+ throw error;
652
+ }
653
+ });
654
+ });
655
+ };
656
+ }
657
+
658
+ /**
659
+ * Datadog log handler
660
+ * Sends logs to Datadog for unified observability
661
+ *
662
+ * Authentication: API key embedded in URL path
663
+ * Regional endpoints: US (default), EU requires custom endpoint
664
+ *
665
+ * @throws Error if API key is invalid or endpoint is incorrect
666
+ */
667
+ export function createDatadogHandler(config: BackendConfig) {
668
+ const { endpoint = "https://http-intake.logs.datadoghq.com", apiKey } =
669
+ config;
670
+ const severityMap = SEVERITY_MAPPINGS.datadog;
671
+
672
+ if (!apiKey) {
673
+ console.warn(
674
+ "Datadog handler: apiKey not configured. Logs will be dropped."
675
+ );
676
+ }
677
+
678
+ return async (log: LogEntry): Promise<void> => {
679
+ if (!apiKey) {
680
+ return;
681
+ }
682
+
683
+ await concurrencyLimiter.run(async () => {
684
+ await retryWithBackoff(async () => {
685
+ const payload = {
686
+ schema: SCHEMA_VERSION,
687
+ ddsource: "dsp-pipeline",
688
+ ddtags: `topic:${log.topic},level:${log.level}`,
689
+ hostname: "localhost",
690
+ message: log.message,
691
+ service: "dsp-pipeline",
692
+ status: severityMap[log.level] || "info",
693
+ timestamp: normalizeTimestamp(log.timestamp, "ms"),
694
+ dd: {
695
+ trace_id: log.traceId,
696
+ span_id: log.spanId,
697
+ },
698
+ ...log.context,
699
+ };
700
+
701
+ try {
702
+ await postJSON(
703
+ `${endpoint}/v1/input/${apiKey}`,
704
+ payload,
705
+ config.headers
706
+ );
707
+ } catch (error) {
708
+ if (error instanceof Error) {
709
+ if (
710
+ error.message.includes("403") ||
711
+ error.message.includes("401")
712
+ ) {
713
+ throw new Error(
714
+ `Datadog authentication failed. Check apiKey validity and regional endpoint (US vs EU). Error: ${error.message}`
715
+ );
716
+ }
717
+ }
718
+ throw error;
719
+ }
720
+ });
721
+ });
722
+ };
723
+ }
724
+
725
+ /**
726
+ * Console handler (for development/debugging)
727
+ * Pretty-prints logs to console with colors
728
+ */
729
+ export function createConsoleHandler(config: Partial<BackendConfig> = {}) {
730
+ const colors = {
731
+ trace: "\x1b[90m", // Gray
732
+ debug: "\x1b[36m", // Cyan
733
+ info: "\x1b[32m", // Green
734
+ warn: "\x1b[33m", // Yellow
735
+ error: "\x1b[31m", // Red
736
+ fatal: "\x1b[35;1m", // Bright Magenta
737
+ reset: "\x1b[0m",
738
+ };
739
+
740
+ return (log: LogEntry): void => {
741
+ const color = colors[log.level] || colors.reset;
742
+ const timestamp = new Date(log.timestamp).toISOString();
743
+ const traceInfo = log.traceId ? ` [trace:${log.traceId.slice(0, 8)}]` : "";
744
+
745
+ console.log(
746
+ `${color}[${timestamp}] [${log.level.toUpperCase()}] [${
747
+ log.topic || "unknown"
748
+ }]${traceInfo}${colors.reset} ${log.message}`
749
+ );
750
+
751
+ if (log.context && Object.keys(log.context).length > 0) {
752
+ console.log(`${color} Context:${colors.reset}`, log.context);
753
+ }
754
+ };
755
+ }
756
+
757
+ /**
758
+ * Create a mock handler for testing
759
+ */
760
+ export function createMockHandler(onLog?: (log: LogEntry) => void) {
761
+ const logs: LogEntry[] = [];
762
+
763
+ const handler = (log: LogEntry): void => {
764
+ logs.push(log);
765
+ if (onLog) {
766
+ onLog(log);
767
+ }
768
+ };
769
+
770
+ return {
771
+ handler,
772
+ getLogs: () => [...logs],
773
+ clear: () => logs.splice(0, logs.length),
774
+ };
775
+ }
776
+
777
+ /**
778
+ * Sampling configuration for high-volume logs
779
+ */
780
+ export interface SamplingConfig {
781
+ trace?: number; // Sampling rate 0-1 (e.g., 0.01 = 1%)
782
+ debug?: number;
783
+ info?: number;
784
+ warn?: number;
785
+ error?: number;
786
+ fatal?: number;
787
+ }
788
+
789
+ /**
790
+ * Logger options
791
+ */
792
+ export interface LoggerOptions {
793
+ fallbackHandler?: (log: LogEntry) => void;
794
+ severityMapping?: SeverityMapping;
795
+ enableMetrics?: boolean;
796
+ formatter?: Formatter;
797
+ sampling?: SamplingConfig;
798
+ minLevel?: LogLevel; // Minimum level to log (dynamic control)
799
+ autoShutdown?: boolean; // Auto-register SIGTERM/SIGINT handlers
800
+ }
801
+
802
+ /**
803
+ * Unified Logger class - orchestrates multiple backend handlers
804
+ *
805
+ * Features:
806
+ * - Multiple handler dispatch with Promise.all
807
+ * - Error isolation (one handler failure doesn't affect others)
808
+ * - Structured error logging to fallback console handler
809
+ * - Extended log levels (trace -> fatal)
810
+ * - Distributed tracing support (auto-inject trace/span IDs)
811
+ * - Graceful shutdown with flushAll()
812
+ * - Internal performance metrics
813
+ * - Custom severity mappings
814
+ *
815
+ * @example
816
+ * ```ts
817
+ * const logger = new Logger([
818
+ * createConsoleHandler(),
819
+ * createLokiHandler({ endpoint: "...", apiKey: "..." }),
820
+ * ], {
821
+ * severityMapping: SEVERITY_MAPPINGS.datadog,
822
+ * enableMetrics: true,
823
+ * });
824
+ *
825
+ * await logger.info("Pipeline initialized");
826
+ * await logger.error("Connection failed", "redis.connection", { host: "localhost" });
827
+ *
828
+ * // Graceful shutdown
829
+ * process.on("SIGTERM", async () => {
830
+ * await logger.flushAll();
831
+ * process.exit(0);
832
+ * });
833
+ * ```
834
+ */
835
+ export class Logger {
836
+ private fallbackHandler: (log: LogEntry) => void;
837
+ private severityMapping?: SeverityMapping;
838
+ private enableMetrics: boolean;
839
+ private formatter: Formatter;
840
+ private sampling?: SamplingConfig;
841
+ private minLevel: LogLevel;
842
+ private readonly levelOrder: Record<LogLevel, number> = {
843
+ trace: 0,
844
+ debug: 1,
845
+ info: 2,
846
+ warn: 3,
847
+ error: 4,
848
+ fatal: 5,
849
+ };
850
+
851
+ // Internal metrics
852
+ private metrics: LoggerMetrics = {
853
+ logsProcessed: 0,
854
+ logsFailed: 0,
855
+ totalRetries: 0,
856
+ flushCount: 0,
857
+ averageFlushTimeMs: 0,
858
+ queueSize: 0,
859
+ handlerErrors: new Map(),
860
+ };
861
+
862
+ constructor(
863
+ private handlers: Array<HandlerWithFlush>,
864
+ options?: LoggerOptions
865
+ ) {
866
+ this.fallbackHandler = options?.fallbackHandler || createConsoleHandler();
867
+ this.severityMapping = options?.severityMapping;
868
+ this.enableMetrics = options?.enableMetrics ?? false;
869
+ this.formatter = options?.formatter || new JSONFormatter();
870
+ this.sampling = options?.sampling;
871
+ this.minLevel = options?.minLevel || "trace";
872
+
873
+ // Auto-register shutdown handlers if requested
874
+ if (options?.autoShutdown) {
875
+ ["SIGINT", "SIGTERM"].forEach((sig) => {
876
+ process.on(sig, async () => {
877
+ await this.flushAll();
878
+ process.exit(0);
879
+ });
880
+ });
881
+ }
882
+ }
883
+
884
+ /**
885
+ * Set minimum log level dynamically
886
+ */
887
+ setMinLevel(level: LogLevel): void {
888
+ this.minLevel = level;
889
+ }
890
+
891
+ /**
892
+ * Get current minimum log level
893
+ */
894
+ getMinLevel(): LogLevel {
895
+ return this.minLevel;
896
+ }
897
+
898
+ /**
899
+ * Check if a log should be sampled (returns true to log, false to skip)
900
+ */
901
+ private shouldSample(level: LogLevel): boolean {
902
+ if (!this.sampling) return true;
903
+
904
+ const rate = this.sampling[level];
905
+ if (rate === undefined) return true;
906
+
907
+ return Math.random() < rate;
908
+ }
909
+
910
+ /**
911
+ * Check if a log level meets the minimum threshold
912
+ */
913
+ private meetsMinLevel(level: LogLevel): boolean {
914
+ return this.levelOrder[level] >= this.levelOrder[this.minLevel];
915
+ }
916
+
917
+ /**
918
+ * Increment retry counter (called by handlers)
919
+ */
920
+ incrementRetries(): void {
921
+ if (this.enableMetrics) {
922
+ this.metrics.totalRetries++;
923
+ }
924
+ }
925
+
926
+ /**
927
+ * Log a message at the specified level
928
+ */
929
+ async log(
930
+ level: LogLevel,
931
+ message: string,
932
+ topic?: string,
933
+ context?: any
934
+ ): Promise<void> {
935
+ // Check minimum level threshold
936
+ if (!this.meetsMinLevel(level)) {
937
+ return;
938
+ }
939
+
940
+ // Apply sampling
941
+ if (!this.shouldSample(level)) {
942
+ return;
943
+ }
944
+
945
+ // Auto-inject tracing context if available
946
+ const tracingCtx = getTracingContext();
947
+
948
+ const entry: LogEntry = {
949
+ level,
950
+ message,
951
+ topic: topic || "default",
952
+ context,
953
+ timestamp: Date.now(),
954
+ traceId: tracingCtx.traceId,
955
+ spanId: tracingCtx.spanId,
956
+ correlationId: tracingCtx.correlationId,
957
+ };
958
+
959
+ if (this.enableMetrics) {
960
+ this.metrics.logsProcessed++;
961
+ }
962
+
963
+ // Apply formatter
964
+ const formattedEntry = this.formatter.format(entry);
965
+
966
+ // Dispatch to all handlers in parallel, isolating errors
967
+ await Promise.all(
968
+ this.handlers.map(async (handler) => {
969
+ try {
970
+ await handler(formattedEntry);
971
+ } catch (error) {
972
+ if (this.enableMetrics) {
973
+ this.metrics.logsFailed++;
974
+ const handlerName = handler.name || "anonymous";
975
+ this.metrics.handlerErrors.set(
976
+ handlerName,
977
+ (this.metrics.handlerErrors.get(handlerName) || 0) + 1
978
+ );
979
+ }
980
+
981
+ // Emit handler errors as structured LogEntry to fallback
982
+ const errorEntry: LogEntry = {
983
+ level: "error",
984
+ message: `Handler error: ${
985
+ error instanceof Error ? error.message : String(error)
986
+ }`,
987
+ topic: "logger.handler.error",
988
+ context: {
989
+ originalLog: entry,
990
+ error:
991
+ error instanceof Error
992
+ ? {
993
+ name: error.name,
994
+ message: error.message,
995
+ stack: error.stack,
996
+ }
997
+ : String(error),
998
+ },
999
+ timestamp: Date.now(),
1000
+ };
1001
+
1002
+ try {
1003
+ this.fallbackHandler(errorEntry);
1004
+ } catch (fallbackError) {
1005
+ console.error("Fallback handler failed:", fallbackError);
1006
+ }
1007
+ }
1008
+ })
1009
+ );
1010
+ }
1011
+
1012
+ /**
1013
+ * Log trace message (most verbose)
1014
+ */
1015
+ async trace(message: string, topic?: string, context?: any): Promise<void> {
1016
+ await this.log("trace", message, topic, context);
1017
+ }
1018
+
1019
+ /**
1020
+ * Log debug message
1021
+ */
1022
+ async debug(message: string, topic?: string, context?: any): Promise<void> {
1023
+ await this.log("debug", message, topic, context);
1024
+ }
1025
+
1026
+ /**
1027
+ * Log info message
1028
+ */
1029
+ async info(message: string, topic?: string, context?: any): Promise<void> {
1030
+ await this.log("info", message, topic, context);
1031
+ }
1032
+
1033
+ /**
1034
+ * Log warning message
1035
+ */
1036
+ async warn(message: string, topic?: string, context?: any): Promise<void> {
1037
+ await this.log("warn", message, topic, context);
1038
+ }
1039
+
1040
+ /**
1041
+ * Log error message
1042
+ */
1043
+ async error(message: string, topic?: string, context?: any): Promise<void> {
1044
+ await this.log("error", message, topic, context);
1045
+ }
1046
+
1047
+ /**
1048
+ * Log fatal message (most critical)
1049
+ */
1050
+ async fatal(message: string, topic?: string, context?: any): Promise<void> {
1051
+ await this.log("fatal", message, topic, context);
1052
+ }
1053
+
1054
+ /**
1055
+ * Flush all handlers (for graceful shutdown)
1056
+ */
1057
+ async flushAll(): Promise<void> {
1058
+ const startTime = Date.now();
1059
+
1060
+ await Promise.all(
1061
+ this.handlers.map(async (handler) => {
1062
+ if (handler.flush) {
1063
+ try {
1064
+ await handler.flush();
1065
+ } catch (error) {
1066
+ console.error("Handler flush error:", error);
1067
+ }
1068
+ }
1069
+ })
1070
+ );
1071
+
1072
+ if (this.enableMetrics) {
1073
+ this.metrics.flushCount++;
1074
+ const flushTime = Date.now() - startTime;
1075
+ this.metrics.averageFlushTimeMs =
1076
+ (this.metrics.averageFlushTimeMs * (this.metrics.flushCount - 1) +
1077
+ flushTime) /
1078
+ this.metrics.flushCount;
1079
+ }
1080
+ }
1081
+
1082
+ /**
1083
+ * Get internal performance metrics
1084
+ */
1085
+ getMetrics(): LoggerMetrics {
1086
+ return { ...this.metrics };
1087
+ }
1088
+
1089
+ /**
1090
+ * Reset internal metrics
1091
+ */
1092
+ resetMetrics(): void {
1093
+ this.metrics = {
1094
+ logsProcessed: 0,
1095
+ logsFailed: 0,
1096
+ totalRetries: 0,
1097
+ flushCount: 0,
1098
+ averageFlushTimeMs: 0,
1099
+ queueSize: 0,
1100
+ handlerErrors: new Map(),
1101
+ };
1102
+ }
1103
+
1104
+ /**
1105
+ * Get severity mapping for current logger
1106
+ */
1107
+ getSeverityMapping(): SeverityMapping | undefined {
1108
+ return this.severityMapping;
1109
+ }
1110
+
1111
+ /**
1112
+ * Create a child logger with a default topic prefix
1113
+ */
1114
+ child(topicPrefix: string): Logger {
1115
+ // Create a new logger that shares handlers but prefixes topics
1116
+ const childLogger = new Logger(this.handlers, {
1117
+ fallbackHandler: this.fallbackHandler,
1118
+ severityMapping: this.severityMapping,
1119
+ enableMetrics: false, // Don't double-count metrics
1120
+ formatter: this.formatter,
1121
+ sampling: this.sampling,
1122
+ minLevel: this.minLevel,
1123
+ autoShutdown: false, // Don't re-register shutdown handlers
1124
+ });
1125
+
1126
+ // Override the log method to prefix topics
1127
+ const originalLog = childLogger.log.bind(childLogger);
1128
+ childLogger.log = async (level, message, topic, context) => {
1129
+ const prefixedTopic = topic
1130
+ ? `${topicPrefix}.${topic}`
1131
+ : `${topicPrefix}.default`;
1132
+ return originalLog(level, message, prefixedTopic, context);
1133
+ };
1134
+
1135
+ return childLogger;
1136
+ }
1137
+ }