@buenojs/bueno 0.8.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 (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,969 @@
1
+ /**
2
+ * Log Transport System
3
+ *
4
+ * Provides transport implementations for external log aggregation services
5
+ * like Datadog, generic HTTP webhooks, and console output.
6
+ */
7
+
8
+ import type { LogEntry, LogLevel, LoggerConfig } from "../index";
9
+
10
+ // ============= Types =============
11
+
12
+ /**
13
+ * Error callback for transport errors
14
+ */
15
+ export type TransportErrorCallback = (error: Error, transport: LogTransport) => void;
16
+
17
+ /**
18
+ * Base interface for log transports
19
+ */
20
+ export interface LogTransport {
21
+ /** Transport name for identification */
22
+ readonly name: string;
23
+
24
+ /** Send a single log entry */
25
+ send(entry: LogEntry): Promise<void>;
26
+
27
+ /** Send multiple log entries (batch) */
28
+ sendBatch(entries: LogEntry[]): Promise<void>;
29
+
30
+ /** Flush any pending logs */
31
+ flush?(): Promise<void>;
32
+
33
+ /** Close the transport and cleanup resources */
34
+ close?(): Promise<void>;
35
+ }
36
+
37
+ /**
38
+ * Options for retry behavior
39
+ */
40
+ export interface RetryOptions {
41
+ /** Maximum number of retry attempts */
42
+ maxRetries: number;
43
+ /** Initial delay in milliseconds */
44
+ initialDelay: number;
45
+ /** Maximum delay in milliseconds */
46
+ maxDelay: number;
47
+ /** Backoff multiplier */
48
+ backoffMultiplier: number;
49
+ }
50
+
51
+ /**
52
+ * Options for batching behavior
53
+ */
54
+ export interface BatchOptions {
55
+ /** Maximum batch size before auto-flush */
56
+ batchSize: number;
57
+ /** Flush interval in milliseconds */
58
+ flushInterval: number;
59
+ }
60
+
61
+ // ============= HTTP Webhook Transport =============
62
+
63
+ /**
64
+ * Options for HTTPWebhookTransport
65
+ */
66
+ export interface HTTPWebhookTransportOptions {
67
+ /** Webhook URL */
68
+ url: string;
69
+ /** Additional headers */
70
+ headers?: Record<string, string>;
71
+ /** Batch options */
72
+ batchSize?: number;
73
+ /** Flush interval in milliseconds */
74
+ flushInterval?: number;
75
+ /** Retry options */
76
+ retries?: Partial<RetryOptions>;
77
+ /** Error callback */
78
+ onError?: TransportErrorCallback;
79
+ /** Request timeout in milliseconds */
80
+ timeout?: number;
81
+ }
82
+
83
+ /**
84
+ * Generic HTTP webhook transport for sending logs to any HTTP endpoint
85
+ */
86
+ export class HTTPWebhookTransport implements LogTransport {
87
+ readonly name = "HTTPWebhookTransport";
88
+ private url: string;
89
+ private headers: Record<string, string>;
90
+ private batchSize: number;
91
+ private flushInterval: number;
92
+ private retryOptions: RetryOptions;
93
+ private onError?: TransportErrorCallback;
94
+ private timeout: number;
95
+ private queue: LogEntry[] = [];
96
+ private flushTimer: ReturnType<typeof setInterval> | null = null;
97
+ private isFlushing = false;
98
+ private isClosed = false;
99
+
100
+ constructor(options: HTTPWebhookTransportOptions) {
101
+ this.url = options.url;
102
+ this.headers = options.headers ?? {};
103
+ this.batchSize = options.batchSize ?? 100;
104
+ this.flushInterval = options.flushInterval ?? 5000;
105
+ this.timeout = options.timeout ?? 30000;
106
+ this.retryOptions = {
107
+ maxRetries: 3,
108
+ initialDelay: 100,
109
+ maxDelay: 10000,
110
+ backoffMultiplier: 2,
111
+ ...options.retries,
112
+ };
113
+ this.onError = options.onError;
114
+
115
+ this.startFlushTimer();
116
+ }
117
+
118
+ /**
119
+ * Start the flush timer
120
+ */
121
+ private startFlushTimer(): void {
122
+ if (this.flushTimer) {
123
+ clearInterval(this.flushTimer);
124
+ }
125
+ this.flushTimer = setInterval(() => {
126
+ this.flush().catch((err) => this.handleError(err));
127
+ }, this.flushInterval);
128
+ }
129
+
130
+ /**
131
+ * Send a single log entry
132
+ */
133
+ async send(entry: LogEntry): Promise<void> {
134
+ if (this.isClosed) return;
135
+
136
+ this.queue.push(entry);
137
+
138
+ if (this.queue.length >= this.batchSize) {
139
+ await this.flush();
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Send multiple log entries
145
+ */
146
+ async sendBatch(entries: LogEntry[]): Promise<void> {
147
+ if (this.isClosed) return;
148
+
149
+ this.queue.push(...entries);
150
+
151
+ if (this.queue.length >= this.batchSize) {
152
+ await this.flush();
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Flush pending logs
158
+ */
159
+ async flush(): Promise<void> {
160
+ if (this.isFlushing || this.queue.length === 0 || this.isClosed) return;
161
+
162
+ this.isFlushing = true;
163
+ const entriesToSend = [...this.queue];
164
+ this.queue = [];
165
+
166
+ try {
167
+ await this.sendWithRetry(entriesToSend);
168
+ } catch (error) {
169
+ // Re-add entries to queue on failure
170
+ this.queue.unshift(...entriesToSend);
171
+ throw error;
172
+ } finally {
173
+ this.isFlushing = false;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Send entries with retry logic
179
+ */
180
+ private async sendWithRetry(entries: LogEntry[]): Promise<void> {
181
+ let lastError: Error | null = null;
182
+ let delay = this.retryOptions.initialDelay;
183
+
184
+ for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
185
+ try {
186
+ await this.makeRequest(entries);
187
+ return;
188
+ } catch (error) {
189
+ lastError = error instanceof Error ? error : new Error(String(error));
190
+
191
+ if (attempt < this.retryOptions.maxRetries) {
192
+ await this.sleep(delay);
193
+ delay = Math.min(
194
+ delay * this.retryOptions.backoffMultiplier,
195
+ this.retryOptions.maxDelay
196
+ );
197
+ }
198
+ }
199
+ }
200
+
201
+ throw lastError;
202
+ }
203
+
204
+ /**
205
+ * Make HTTP request
206
+ */
207
+ private async makeRequest(entries: LogEntry[]): Promise<void> {
208
+ const controller = new AbortController();
209
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
210
+
211
+ try {
212
+ const response = await fetch(this.url, {
213
+ method: "POST",
214
+ headers: {
215
+ "Content-Type": "application/json",
216
+ ...this.headers,
217
+ },
218
+ body: JSON.stringify(entries),
219
+ signal: controller.signal,
220
+ });
221
+
222
+ if (!response.ok) {
223
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
224
+ }
225
+ } finally {
226
+ clearTimeout(timeoutId);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Sleep utility
232
+ */
233
+ private sleep(ms: number): Promise<void> {
234
+ return new Promise((resolve) => setTimeout(resolve, ms));
235
+ }
236
+
237
+ /**
238
+ * Handle errors
239
+ */
240
+ private handleError(error: Error): void {
241
+ if (this.onError) {
242
+ this.onError(error, this);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Close the transport
248
+ */
249
+ async close(): Promise<void> {
250
+ this.isClosed = true;
251
+
252
+ if (this.flushTimer) {
253
+ clearInterval(this.flushTimer);
254
+ this.flushTimer = null;
255
+ }
256
+
257
+ // Flush remaining entries
258
+ if (this.queue.length > 0) {
259
+ try {
260
+ await this.flush();
261
+ } catch (error) {
262
+ this.handleError(error instanceof Error ? error : new Error(String(error)));
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ // ============= Datadog Transport =============
269
+
270
+ /**
271
+ * Options for DatadogTransport
272
+ */
273
+ export interface DatadogTransportOptions {
274
+ /** Datadog API key */
275
+ apiKey: string;
276
+ /** Service name */
277
+ service: string;
278
+ /** Environment (e.g., production, staging) */
279
+ env?: string;
280
+ /** Hostname */
281
+ hostname?: string;
282
+ /** Default tags */
283
+ tags?: string[];
284
+ /** Batch options */
285
+ batchSize?: number;
286
+ /** Flush interval in milliseconds */
287
+ flushInterval?: number;
288
+ /** Retry options */
289
+ retries?: Partial<RetryOptions>;
290
+ /** Error callback */
291
+ onError?: TransportErrorCallback;
292
+ /** Custom Datadog API endpoint */
293
+ endpoint?: string;
294
+ /** Request timeout in milliseconds */
295
+ timeout?: number;
296
+ }
297
+
298
+ /**
299
+ * Datadog log entry format
300
+ */
301
+ interface DatadogLogEntry {
302
+ ddsource: string;
303
+ ddsourcecategory: string;
304
+ ddtags: string;
305
+ hostname: string;
306
+ service: string;
307
+ status: string;
308
+ message: string;
309
+ timestamp: string;
310
+ [key: string]: unknown;
311
+ }
312
+
313
+ /**
314
+ * Datadog Logs API transport
315
+ */
316
+ export class DatadogTransport implements LogTransport {
317
+ readonly name = "DatadogTransport";
318
+ private apiKey: string;
319
+ private service: string;
320
+ private env: string;
321
+ private hostname: string;
322
+ private tags: string[];
323
+ private endpoint: string;
324
+ private batchSize: number;
325
+ private flushInterval: number;
326
+ private retryOptions: RetryOptions;
327
+ private onError?: TransportErrorCallback;
328
+ private timeout: number;
329
+ private queue: LogEntry[] = [];
330
+ private flushTimer: ReturnType<typeof setInterval> | null = null;
331
+ private isFlushing = false;
332
+ private isClosed = false;
333
+
334
+ constructor(options: DatadogTransportOptions) {
335
+ this.apiKey = options.apiKey;
336
+ this.service = options.service;
337
+ this.env = options.env ?? process.env.NODE_ENV ?? "development";
338
+ this.hostname = options.hostname ?? this.getDefaultHostname();
339
+ this.tags = options.tags ?? [];
340
+ this.endpoint = options.endpoint ?? "https://http-intake.logs.datadoghq.com/v1/input";
341
+ this.batchSize = options.batchSize ?? 100;
342
+ this.flushInterval = options.flushInterval ?? 5000;
343
+ this.timeout = options.timeout ?? 30000;
344
+ this.retryOptions = {
345
+ maxRetries: 3,
346
+ initialDelay: 100,
347
+ maxDelay: 10000,
348
+ backoffMultiplier: 2,
349
+ ...options.retries,
350
+ };
351
+ this.onError = options.onError;
352
+
353
+ this.startFlushTimer();
354
+ }
355
+
356
+ /**
357
+ * Get default hostname
358
+ */
359
+ private getDefaultHostname(): string {
360
+ try {
361
+ return process.env.HOSTNAME ?? process.env.COMPUTERNAME ?? "unknown";
362
+ } catch {
363
+ return "unknown";
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Start the flush timer
369
+ */
370
+ private startFlushTimer(): void {
371
+ if (this.flushTimer) {
372
+ clearInterval(this.flushTimer);
373
+ }
374
+ this.flushTimer = setInterval(() => {
375
+ this.flush().catch((err) => this.handleError(err));
376
+ }, this.flushInterval);
377
+ }
378
+
379
+ /**
380
+ * Convert log level to Datadog status
381
+ */
382
+ private toDatadogStatus(level: LogLevel): string {
383
+ const statusMap: Record<LogLevel, string> = {
384
+ debug: "debug",
385
+ info: "info",
386
+ warn: "warn",
387
+ error: "error",
388
+ fatal: "emerg",
389
+ };
390
+ return statusMap[level];
391
+ }
392
+
393
+ /**
394
+ * Convert LogEntry to Datadog format
395
+ */
396
+ private toDatadogFormat(entry: LogEntry): DatadogLogEntry {
397
+ const allTags = [...this.tags];
398
+
399
+ // Add environment tag
400
+ if (this.env) {
401
+ allTags.push(`env:${this.env}`);
402
+ }
403
+
404
+ // Add context as tags
405
+ if (entry.context) {
406
+ for (const [key, value] of Object.entries(entry.context)) {
407
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
408
+ allTags.push(`${key}:${value}`);
409
+ }
410
+ }
411
+ }
412
+
413
+ const datadogEntry: DatadogLogEntry = {
414
+ ddsource: "bueno",
415
+ ddsourcecategory: "framework",
416
+ ddtags: allTags.join(","),
417
+ hostname: this.hostname,
418
+ service: this.service,
419
+ status: this.toDatadogStatus(entry.level),
420
+ message: entry.message,
421
+ timestamp: entry.timestamp,
422
+ };
423
+
424
+ // Add error details
425
+ if (entry.error) {
426
+ datadogEntry.error = {
427
+ kind: entry.error.name,
428
+ message: entry.error.message,
429
+ stack: entry.error.stack,
430
+ };
431
+ }
432
+
433
+ // Add duration if present
434
+ if (entry.duration !== undefined) {
435
+ datadogEntry.duration = entry.duration;
436
+ }
437
+
438
+ // Add any additional fields from the entry
439
+ for (const [key, value] of Object.entries(entry)) {
440
+ if (!["level", "message", "timestamp", "context", "error", "duration"].includes(key)) {
441
+ datadogEntry[key] = value;
442
+ }
443
+ }
444
+
445
+ return datadogEntry;
446
+ }
447
+
448
+ /**
449
+ * Send a single log entry
450
+ */
451
+ async send(entry: LogEntry): Promise<void> {
452
+ if (this.isClosed) return;
453
+
454
+ this.queue.push(entry);
455
+
456
+ if (this.queue.length >= this.batchSize) {
457
+ await this.flush();
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Send multiple log entries
463
+ */
464
+ async sendBatch(entries: LogEntry[]): Promise<void> {
465
+ if (this.isClosed) return;
466
+
467
+ this.queue.push(...entries);
468
+
469
+ if (this.queue.length >= this.batchSize) {
470
+ await this.flush();
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Flush pending logs
476
+ */
477
+ async flush(): Promise<void> {
478
+ if (this.isFlushing || this.queue.length === 0 || this.isClosed) return;
479
+
480
+ this.isFlushing = true;
481
+ const entriesToSend = [...this.queue];
482
+ this.queue = [];
483
+
484
+ try {
485
+ await this.sendWithRetry(entriesToSend);
486
+ } catch (error) {
487
+ // Re-add entries to queue on failure
488
+ this.queue.unshift(...entriesToSend);
489
+ throw error;
490
+ } finally {
491
+ this.isFlushing = false;
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Send entries with retry logic
497
+ */
498
+ private async sendWithRetry(entries: LogEntry[]): Promise<void> {
499
+ let lastError: Error | null = null;
500
+ let delay = this.retryOptions.initialDelay;
501
+
502
+ for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
503
+ try {
504
+ await this.makeRequest(entries);
505
+ return;
506
+ } catch (error) {
507
+ lastError = error instanceof Error ? error : new Error(String(error));
508
+
509
+ if (attempt < this.retryOptions.maxRetries) {
510
+ await this.sleep(delay);
511
+ delay = Math.min(
512
+ delay * this.retryOptions.backoffMultiplier,
513
+ this.retryOptions.maxDelay
514
+ );
515
+ }
516
+ }
517
+ }
518
+
519
+ throw lastError;
520
+ }
521
+
522
+ /**
523
+ * Make HTTP request to Datadog
524
+ */
525
+ private async makeRequest(entries: LogEntry[]): Promise<void> {
526
+ const datadogEntries = entries.map((e) => this.toDatadogFormat(e));
527
+
528
+ const controller = new AbortController();
529
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
530
+
531
+ try {
532
+ const response = await fetch(this.endpoint, {
533
+ method: "POST",
534
+ headers: {
535
+ "Content-Type": "application/json",
536
+ "DD-API-KEY": this.apiKey,
537
+ },
538
+ body: datadogEntries.map((e) => JSON.stringify(e)).join("\n"),
539
+ signal: controller.signal,
540
+ });
541
+
542
+ if (!response.ok) {
543
+ throw new Error(`Datadog API error: HTTP ${response.status}: ${response.statusText}`);
544
+ }
545
+ } finally {
546
+ clearTimeout(timeoutId);
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Sleep utility
552
+ */
553
+ private sleep(ms: number): Promise<void> {
554
+ return new Promise((resolve) => setTimeout(resolve, ms));
555
+ }
556
+
557
+ /**
558
+ * Handle errors
559
+ */
560
+ private handleError(error: Error): void {
561
+ if (this.onError) {
562
+ this.onError(error, this);
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Close the transport
568
+ */
569
+ async close(): Promise<void> {
570
+ this.isClosed = true;
571
+
572
+ if (this.flushTimer) {
573
+ clearInterval(this.flushTimer);
574
+ this.flushTimer = null;
575
+ }
576
+
577
+ // Flush remaining entries
578
+ if (this.queue.length > 0) {
579
+ try {
580
+ await this.flush();
581
+ } catch (error) {
582
+ this.handleError(error instanceof Error ? error : new Error(String(error)));
583
+ }
584
+ }
585
+ }
586
+ }
587
+
588
+ // ============= Console Transport =============
589
+
590
+ /**
591
+ * Options for ConsoleTransport
592
+ */
593
+ export interface ConsoleTransportOptions {
594
+ /** Use pretty printing */
595
+ pretty?: boolean;
596
+ /** Output stream for logs */
597
+ stream?: "stdout" | "stderr";
598
+ /** Error callback */
599
+ onError?: TransportErrorCallback;
600
+ }
601
+
602
+ /**
603
+ * Enhanced console transport for local development
604
+ */
605
+ export class ConsoleTransport implements LogTransport {
606
+ readonly name = "ConsoleTransport";
607
+ private pretty: boolean;
608
+ private stream: "stdout" | "stderr";
609
+ private onError?: TransportErrorCallback;
610
+
611
+ constructor(options: ConsoleTransportOptions = {}) {
612
+ this.pretty = options.pretty ?? process.env.NODE_ENV !== "production";
613
+ this.stream = options.stream ?? "stdout";
614
+ this.onError = options.onError;
615
+ }
616
+
617
+ /**
618
+ * Get level color for pretty printing
619
+ */
620
+ private getLevelColor(level: LogLevel): string {
621
+ const colors: Record<LogLevel, string> = {
622
+ debug: "\x1b[36m", // cyan
623
+ info: "\x1b[32m", // green
624
+ warn: "\x1b[33m", // yellow
625
+ error: "\x1b[31m", // red
626
+ fatal: "\x1b[35m", // magenta
627
+ };
628
+ return colors[level];
629
+ }
630
+
631
+ /**
632
+ * Format log entry for console output
633
+ */
634
+ private formatEntry(entry: LogEntry): string {
635
+ if (this.pretty) {
636
+ const color = this.getLevelColor(entry.level);
637
+ const reset = "\x1b[0m";
638
+ let output = `${entry.timestamp} ${color}[${entry.level.toUpperCase()}]${reset} ${entry.message}`;
639
+
640
+ if (entry.context && Object.keys(entry.context).length > 0) {
641
+ output += ` ${reset}\x1b[90m${JSON.stringify(entry.context)}\x1b[0m`;
642
+ }
643
+
644
+ if (entry.duration !== undefined) {
645
+ output += ` \x1b[90m(${entry.duration}ms)\x1b[0m`;
646
+ }
647
+
648
+ if (entry.error) {
649
+ output += `\n \x1b[31m${entry.error.name}: ${entry.error.message}\x1b[0m`;
650
+ if (entry.error.stack) {
651
+ output += `\n \x1b[90m${entry.error.stack}\x1b[0m`;
652
+ }
653
+ }
654
+
655
+ return output;
656
+ }
657
+
658
+ return JSON.stringify(entry);
659
+ }
660
+
661
+ /**
662
+ * Output to console
663
+ */
664
+ private output(formatted: string, level: LogLevel): void {
665
+ if (this.stream === "stderr" || level === "error" || level === "fatal") {
666
+ console.error(formatted);
667
+ } else if (level === "warn") {
668
+ console.warn(formatted);
669
+ } else {
670
+ console.log(formatted);
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Send a single log entry
676
+ */
677
+ async send(entry: LogEntry): Promise<void> {
678
+ try {
679
+ const formatted = this.formatEntry(entry);
680
+ this.output(formatted, entry.level);
681
+ } catch (error) {
682
+ if (this.onError) {
683
+ this.onError(error instanceof Error ? error : new Error(String(error)), this);
684
+ }
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Send multiple log entries
690
+ */
691
+ async sendBatch(entries: LogEntry[]): Promise<void> {
692
+ for (const entry of entries) {
693
+ await this.send(entry);
694
+ }
695
+ }
696
+
697
+ /**
698
+ * Flush (no-op for console)
699
+ */
700
+ async flush(): Promise<void> {
701
+ // Console transport doesn't buffer
702
+ }
703
+
704
+ /**
705
+ * Close (no-op for console)
706
+ */
707
+ async close(): Promise<void> {
708
+ // Console transport doesn't need cleanup
709
+ }
710
+ }
711
+
712
+ // ============= Transport Manager =============
713
+
714
+ /**
715
+ * Manages multiple log transports
716
+ */
717
+ export class TransportManager {
718
+ private transports: Set<LogTransport> = new Set();
719
+ private onError?: TransportErrorCallback;
720
+
721
+ constructor(options?: { onError?: TransportErrorCallback }) {
722
+ this.onError = options?.onError;
723
+ }
724
+
725
+ /**
726
+ * Add a transport
727
+ */
728
+ addTransport(transport: LogTransport): void {
729
+ this.transports.add(transport);
730
+ }
731
+
732
+ /**
733
+ * Remove a transport
734
+ */
735
+ removeTransport(transport: LogTransport): boolean {
736
+ return this.transports.delete(transport);
737
+ }
738
+
739
+ /**
740
+ * Get all transports
741
+ */
742
+ getTransports(): LogTransport[] {
743
+ return [...this.transports];
744
+ }
745
+
746
+ /**
747
+ * Check if a transport is registered
748
+ */
749
+ hasTransport(transport: LogTransport): boolean {
750
+ return this.transports.has(transport);
751
+ }
752
+
753
+ /**
754
+ * Clear all transports
755
+ */
756
+ clearTransports(): void {
757
+ this.transports.clear();
758
+ }
759
+
760
+ /**
761
+ * Broadcast a log entry to all transports
762
+ */
763
+ async broadcast(entry: LogEntry): Promise<void> {
764
+ const promises = [...this.transports].map(async (transport) => {
765
+ try {
766
+ await transport.send(entry);
767
+ } catch (error) {
768
+ if (this.onError) {
769
+ this.onError(error instanceof Error ? error : new Error(String(error)), transport);
770
+ }
771
+ }
772
+ });
773
+
774
+ await Promise.allSettled(promises);
775
+ }
776
+
777
+ /**
778
+ * Broadcast multiple entries to all transports
779
+ */
780
+ async broadcastBatch(entries: LogEntry[]): Promise<void> {
781
+ const promises = [...this.transports].map(async (transport) => {
782
+ try {
783
+ await transport.sendBatch(entries);
784
+ } catch (error) {
785
+ if (this.onError) {
786
+ this.onError(error instanceof Error ? error : new Error(String(error)), transport);
787
+ }
788
+ }
789
+ });
790
+
791
+ await Promise.allSettled(promises);
792
+ }
793
+
794
+ /**
795
+ * Flush all transports
796
+ */
797
+ async flushAll(): Promise<void> {
798
+ const promises = [...this.transports].map(async (transport) => {
799
+ if (transport.flush) {
800
+ try {
801
+ await transport.flush();
802
+ } catch (error) {
803
+ if (this.onError) {
804
+ this.onError(error instanceof Error ? error : new Error(String(error)), transport);
805
+ }
806
+ }
807
+ }
808
+ });
809
+
810
+ await Promise.allSettled(promises);
811
+ }
812
+
813
+ /**
814
+ * Close all transports
815
+ */
816
+ async closeAll(): Promise<void> {
817
+ const promises = [...this.transports].map(async (transport) => {
818
+ if (transport.close) {
819
+ try {
820
+ await transport.close();
821
+ } catch (error) {
822
+ if (this.onError) {
823
+ this.onError(error instanceof Error ? error : new Error(String(error)), transport);
824
+ }
825
+ }
826
+ }
827
+ });
828
+
829
+ await Promise.allSettled(promises);
830
+ this.transports.clear();
831
+ }
832
+ }
833
+
834
+ // ============= Logger Integration =============
835
+
836
+ import { Logger } from "../index";
837
+
838
+ /**
839
+ * Configuration for logger with transports
840
+ */
841
+ export interface LoggerWithTransportsConfig extends LoggerConfig {
842
+ /** Transports to add to the logger */
843
+ transports?: LogTransport[];
844
+ /** Error callback for transport errors */
845
+ onTransportError?: TransportErrorCallback;
846
+ }
847
+
848
+ /**
849
+ * Logger with transport support
850
+ */
851
+ export class LoggerWithTransports extends Logger {
852
+ private transportManager: TransportManager;
853
+
854
+ constructor(config: LoggerWithTransportsConfig = {}) {
855
+ const { transports, onTransportError, ...loggerConfig } = config;
856
+
857
+ // Create transport manager
858
+ const transportManager = new TransportManager({ onError: onTransportError });
859
+
860
+ // Add transports
861
+ if (transports) {
862
+ for (const transport of transports) {
863
+ transportManager.addTransport(transport);
864
+ }
865
+ }
866
+
867
+ // Create output function that broadcasts to transports
868
+ const originalOutput = loggerConfig.output;
869
+ const outputFn = (entry: LogEntry) => {
870
+ // Call original output if specified
871
+ if (originalOutput) {
872
+ if (typeof originalOutput === "function") {
873
+ originalOutput(entry);
874
+ }
875
+ } else {
876
+ // Default console output
877
+ console.log(JSON.stringify(entry));
878
+ }
879
+
880
+ // Broadcast to transports (async, fire-and-forget)
881
+ transportManager.broadcast(entry).catch(() => {
882
+ // Errors are handled by the transport manager
883
+ });
884
+ };
885
+
886
+ super({
887
+ ...loggerConfig,
888
+ output: outputFn,
889
+ });
890
+
891
+ this.transportManager = transportManager;
892
+ }
893
+
894
+ /**
895
+ * Add a transport
896
+ */
897
+ addTransport(transport: LogTransport): void {
898
+ this.transportManager.addTransport(transport);
899
+ }
900
+
901
+ /**
902
+ * Remove a transport
903
+ */
904
+ removeTransport(transport: LogTransport): boolean {
905
+ return this.transportManager.removeTransport(transport);
906
+ }
907
+
908
+ /**
909
+ * Get all transports
910
+ */
911
+ getTransports(): LogTransport[] {
912
+ return this.transportManager.getTransports();
913
+ }
914
+
915
+ /**
916
+ * Flush all transports
917
+ */
918
+ async flushTransports(): Promise<void> {
919
+ await this.transportManager.flushAll();
920
+ }
921
+
922
+ /**
923
+ * Close all transports
924
+ */
925
+ async closeTransports(): Promise<void> {
926
+ await this.transportManager.closeAll();
927
+ }
928
+ }
929
+
930
+ /**
931
+ * Create a logger with transports
932
+ */
933
+ export function createLoggerWithTransports(
934
+ config: LoggerWithTransportsConfig = {}
935
+ ): LoggerWithTransports {
936
+ return new LoggerWithTransports(config);
937
+ }
938
+
939
+ /**
940
+ * Create a transport output function for use with existing Logger
941
+ */
942
+ export function createTransportOutput(
943
+ transports: LogTransport[],
944
+ options?: { onError?: TransportErrorCallback }
945
+ ): (entry: LogEntry) => void {
946
+ const manager = new TransportManager({ onError: options?.onError });
947
+
948
+ for (const transport of transports) {
949
+ manager.addTransport(transport);
950
+ }
951
+
952
+ return (entry: LogEntry) => {
953
+ manager.broadcast(entry).catch(() => {
954
+ // Errors are handled by the transport manager
955
+ });
956
+ };
957
+ }
958
+
959
+ // ============= Exports =============
960
+
961
+ export default {
962
+ HTTPWebhookTransport,
963
+ DatadogTransport,
964
+ ConsoleTransport,
965
+ TransportManager,
966
+ LoggerWithTransports,
967
+ createLoggerWithTransports,
968
+ createTransportOutput,
969
+ };