@logtape/syslog 1.3.1 → 1.3.3

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/src/syslog.ts DELETED
@@ -1,860 +0,0 @@
1
- import type { LogLevel, LogRecord, Sink } from "@logtape/logtape";
2
- import { createSocket } from "node:dgram";
3
- import { Socket } from "node:net";
4
- import { hostname } from "node:os";
5
- import process from "node:process";
6
- import * as tls from "node:tls";
7
-
8
- /**
9
- * Syslog protocol type.
10
- * @since 0.12.0
11
- */
12
- export type SyslogProtocol = "udp" | "tcp";
13
-
14
- /**
15
- * Syslog facility codes as defined in RFC 5424.
16
- * @since 0.12.0
17
- */
18
- export type SyslogFacility =
19
- | "kernel" // 0 - kernel messages
20
- | "user" // 1 - user-level messages
21
- | "mail" // 2 - mail system
22
- | "daemon" // 3 - system daemons
23
- | "security" // 4 - security/authorization messages
24
- | "syslog" // 5 - messages generated internally by syslogd
25
- | "lpr" // 6 - line printer subsystem
26
- | "news" // 7 - network news subsystem
27
- | "uucp" // 8 - UUCP subsystem
28
- | "cron" // 9 - clock daemon
29
- | "authpriv" // 10 - security/authorization messages
30
- | "ftp" // 11 - FTP daemon
31
- | "ntp" // 12 - NTP subsystem
32
- | "logaudit" // 13 - log audit
33
- | "logalert" // 14 - log alert
34
- | "clock" // 15 - clock daemon
35
- | "local0" // 16 - local use 0
36
- | "local1" // 17 - local use 1
37
- | "local2" // 18 - local use 2
38
- | "local3" // 19 - local use 3
39
- | "local4" // 20 - local use 4
40
- | "local5" // 21 - local use 5
41
- | "local6" // 22 - local use 6
42
- | "local7"; // 23 - local use 7
43
-
44
- /**
45
- * Syslog facility code mapping.
46
- * @since 0.12.0
47
- */
48
- const FACILITY_CODES: Record<SyslogFacility, number> = {
49
- kernel: 0,
50
- user: 1,
51
- mail: 2,
52
- daemon: 3,
53
- security: 4,
54
- syslog: 5,
55
- lpr: 6,
56
- news: 7,
57
- uucp: 8,
58
- cron: 9,
59
- authpriv: 10,
60
- ftp: 11,
61
- ntp: 12,
62
- logaudit: 13,
63
- logalert: 14,
64
- clock: 15,
65
- local0: 16,
66
- local1: 17,
67
- local2: 18,
68
- local3: 19,
69
- local4: 20,
70
- local5: 21,
71
- local6: 22,
72
- local7: 23,
73
- };
74
-
75
- /**
76
- * Syslog severity levels as defined in RFC 5424.
77
- * @since 0.12.0
78
- */
79
- const SEVERITY_LEVELS: Record<LogLevel, number> = {
80
- fatal: 0, // Emergency: system is unusable
81
- error: 3, // Error: error conditions
82
- warning: 4, // Warning: warning conditions
83
- info: 6, // Informational: informational messages
84
- debug: 7, // Debug: debug-level messages
85
- trace: 7, // Debug: debug-level messages (same as debug)
86
- };
87
-
88
- /**
89
- * TLS options for secure TCP connections.
90
- * @since 1.3.0
91
- */
92
- export interface SyslogTlsOptions {
93
- /**
94
- * Whether to reject connections with invalid certificates.
95
- * Setting this to `false` disables certificate validation, which makes
96
- * the connection vulnerable to man-in-the-middle attacks.
97
- * @default `true`
98
- */
99
- readonly rejectUnauthorized?: boolean;
100
-
101
- /**
102
- * Custom CA certificates to trust. If not provided, the default system
103
- * CA certificates are used.
104
- */
105
- readonly ca?: string | readonly string[];
106
- }
107
-
108
- /**
109
- * Options for the syslog sink.
110
- * @since 0.12.0
111
- */
112
- export interface SyslogSinkOptions {
113
- /**
114
- * The hostname or IP address of the syslog server.
115
- * @default "localhost"
116
- */
117
- readonly hostname?: string;
118
-
119
- /**
120
- * The port number of the syslog server.
121
- * @default 514
122
- */
123
- readonly port?: number;
124
-
125
- /**
126
- * The protocol to use for sending syslog messages.
127
- * @default "udp"
128
- */
129
- readonly protocol?: SyslogProtocol;
130
-
131
- /**
132
- * Whether to use TLS for TCP connections.
133
- * This option is ignored for UDP connections.
134
- * @default `false`
135
- * @since 1.3.0
136
- */
137
- readonly secure?: boolean;
138
-
139
- /**
140
- * TLS options for secure TCP connections.
141
- * This option is only used when `secure` is `true` and `protocol` is `"tcp"`.
142
- * @since 1.3.0
143
- */
144
- readonly tlsOptions?: SyslogTlsOptions;
145
-
146
- /**
147
- * The syslog facility to use for all messages.
148
- * @default "local0"
149
- */
150
- readonly facility?: SyslogFacility;
151
-
152
- /**
153
- * The application name to include in syslog messages.
154
- * @default "logtape"
155
- */
156
- readonly appName?: string;
157
-
158
- /**
159
- * The hostname to include in syslog messages.
160
- * If not provided, the system hostname will be used.
161
- */
162
- readonly syslogHostname?: string;
163
-
164
- /**
165
- * The process ID to include in syslog messages.
166
- * If not provided, the current process ID will be used.
167
- */
168
- readonly processId?: string;
169
-
170
- /**
171
- * Connection timeout in milliseconds.
172
- * @default 5000
173
- */
174
- readonly timeout?: number;
175
-
176
- /**
177
- * Whether to include structured data in syslog messages.
178
- * @default `false`
179
- */
180
- readonly includeStructuredData?: boolean;
181
-
182
- /**
183
- * The structured data ID to use for log properties.
184
- * Should follow the format "name@private-enterprise-number".
185
- * @default "logtape@32473"
186
- */
187
- readonly structuredDataId?: string;
188
- }
189
-
190
- /**
191
- * Calculates the priority value for a syslog message.
192
- * Priority = Facility * 8 + Severity
193
- * @since 0.12.0
194
- */
195
- function calculatePriority(facility: SyslogFacility, severity: number): number {
196
- const facilityCode = FACILITY_CODES[facility];
197
- return facilityCode * 8 + severity;
198
- }
199
-
200
- /**
201
- * Formats a timestamp (number) as RFC 3339 timestamp for syslog.
202
- * @since 0.12.0
203
- */
204
- function formatTimestamp(timestamp: number): string {
205
- return new Date(timestamp).toISOString();
206
- }
207
-
208
- /**
209
- * Escapes special characters in structured data values.
210
- * @since 0.12.0
211
- */
212
- function escapeStructuredDataValue(value: string): string {
213
- return value
214
- .replace(/\\/g, "\\\\")
215
- .replace(/"/g, '\\"')
216
- .replace(/]/g, "\\]");
217
- }
218
-
219
- /**
220
- * Formats structured data from log record properties.
221
- * @since 0.12.0
222
- */
223
- function formatStructuredData(
224
- record: LogRecord,
225
- structuredDataId: string,
226
- ): string {
227
- if (!record.properties || Object.keys(record.properties).length === 0) {
228
- return "-";
229
- }
230
-
231
- const elements: string[] = [];
232
- for (const [key, value] of Object.entries(record.properties)) {
233
- const escapedValue = escapeStructuredDataValue(String(value));
234
- elements.push(`${key}="${escapedValue}"`);
235
- }
236
-
237
- return `[${structuredDataId} ${elements.join(" ")}]`;
238
- }
239
-
240
- /**
241
- * Formats a log record as RFC 5424 syslog message.
242
- * @since 0.12.0
243
- */
244
- function formatSyslogMessage(
245
- record: LogRecord,
246
- options: Required<
247
- Pick<
248
- SyslogSinkOptions,
249
- | "facility"
250
- | "appName"
251
- | "syslogHostname"
252
- | "processId"
253
- | "includeStructuredData"
254
- | "structuredDataId"
255
- >
256
- >,
257
- ): string {
258
- const severity = SEVERITY_LEVELS[record.level];
259
- const priority = calculatePriority(options.facility, severity);
260
- const timestamp = formatTimestamp(record.timestamp);
261
- const hostname = options.syslogHostname || "-";
262
- const appName = options.appName || "-";
263
- const processId = options.processId || "-";
264
- const msgId = "-"; // Could be enhanced to include message ID
265
-
266
- let structuredData = "-";
267
- if (options.includeStructuredData) {
268
- structuredData = formatStructuredData(record, options.structuredDataId);
269
- }
270
-
271
- // Format the message text
272
- let message = "";
273
- for (let i = 0; i < record.message.length; i++) {
274
- if (i % 2 === 0) {
275
- message += record.message[i];
276
- } else {
277
- message += JSON.stringify(record.message[i]);
278
- }
279
- }
280
-
281
- // RFC 5424 format: <PRI>VERSION TIMESTAMP HOSTNAME APP-NAME PROCID MSGID STRUCTURED-DATA MSG
282
- return `<${priority}>1 ${timestamp} ${hostname} ${appName} ${processId} ${msgId} ${structuredData} ${message}`;
283
- }
284
-
285
- /**
286
- * Gets the system hostname.
287
- * @since 0.12.0
288
- */
289
- function getSystemHostname(): string {
290
- try {
291
- // Try Deno first
292
- if (typeof Deno !== "undefined" && Deno.hostname) {
293
- return Deno.hostname();
294
- }
295
-
296
- // Try Node.js os.hostname()
297
- return hostname();
298
- } catch {
299
- // Fallback to environment variable or localhost
300
- return process.env.HOSTNAME || "localhost";
301
- }
302
- }
303
-
304
- /**
305
- * Gets the current process ID.
306
- * @since 0.12.0
307
- */
308
- function getProcessId(): string {
309
- try {
310
- // Try Deno first
311
- if (typeof Deno !== "undefined" && Deno.pid) {
312
- return Deno.pid.toString();
313
- }
314
-
315
- // Try Node.js
316
- return process.pid.toString();
317
- } catch {
318
- return "-";
319
- }
320
- }
321
-
322
- /**
323
- * Base interface for syslog connections.
324
- * @since 0.12.0
325
- */
326
- export interface SyslogConnection {
327
- connect(): void | Promise<void>;
328
- send(message: string): Promise<void>;
329
- close(): void;
330
- }
331
-
332
- /**
333
- * Deno UDP syslog connection implementation.
334
- * @since 0.12.0
335
- */
336
- export class DenoUdpSyslogConnection implements SyslogConnection {
337
- private encoder = new TextEncoder();
338
-
339
- constructor(
340
- private hostname: string,
341
- private port: number,
342
- private timeout: number,
343
- ) {}
344
-
345
- connect(): void {
346
- // For UDP, we don't need to establish a persistent connection
347
- }
348
-
349
- send(message: string): Promise<void> {
350
- const data = this.encoder.encode(message);
351
-
352
- try {
353
- // Deno doesn't have native UDP support, use Node.js APIs
354
- const socket = createSocket("udp4");
355
-
356
- if (this.timeout > 0) {
357
- return new Promise<void>((resolve, reject) => {
358
- const timeout = setTimeout(() => {
359
- socket.close();
360
- reject(new Error("UDP send timeout"));
361
- }, this.timeout);
362
-
363
- socket.send(data, this.port, this.hostname, (error) => {
364
- clearTimeout(timeout);
365
- socket.close();
366
- if (error) {
367
- reject(error);
368
- } else {
369
- resolve();
370
- }
371
- });
372
- });
373
- } else {
374
- // No timeout
375
- return new Promise<void>((resolve, reject) => {
376
- socket.send(data, this.port, this.hostname, (error) => {
377
- socket.close();
378
- if (error) {
379
- reject(error);
380
- } else {
381
- resolve();
382
- }
383
- });
384
- });
385
- }
386
- } catch (error) {
387
- throw new Error(`Failed to send syslog message: ${error}`);
388
- }
389
- }
390
-
391
- close(): void {
392
- // UDP connections don't need explicit closing
393
- }
394
- }
395
-
396
- /**
397
- * Node.js UDP syslog connection implementation.
398
- * @since 0.12.0
399
- */
400
- export class NodeUdpSyslogConnection implements SyslogConnection {
401
- private encoder = new TextEncoder();
402
-
403
- constructor(
404
- private hostname: string,
405
- private port: number,
406
- private timeout: number,
407
- ) {}
408
-
409
- connect(): void {
410
- // For UDP, we don't need to establish a persistent connection
411
- }
412
-
413
- send(message: string): Promise<void> {
414
- const data = this.encoder.encode(message);
415
-
416
- try {
417
- const socket = createSocket("udp4");
418
-
419
- return new Promise<void>((resolve, reject) => {
420
- const timeout = setTimeout(() => {
421
- socket.close();
422
- reject(new Error("UDP send timeout"));
423
- }, this.timeout);
424
-
425
- socket.send(data, this.port, this.hostname, (error) => {
426
- clearTimeout(timeout);
427
- socket.close();
428
- if (error) {
429
- reject(error);
430
- } else {
431
- resolve();
432
- }
433
- });
434
- });
435
- } catch (error) {
436
- throw new Error(`Failed to send syslog message: ${error}`);
437
- }
438
- }
439
-
440
- close(): void {
441
- // UDP connections don't need explicit closing
442
- }
443
- }
444
-
445
- /**
446
- * Deno TCP syslog connection implementation.
447
- * @since 0.12.0
448
- */
449
- export class DenoTcpSyslogConnection implements SyslogConnection {
450
- private connection?: Deno.TcpConn | Deno.TlsConn;
451
- private encoder = new TextEncoder();
452
-
453
- constructor(
454
- private hostname: string,
455
- private port: number,
456
- private timeout: number,
457
- private secure: boolean,
458
- private tlsOptions?: SyslogTlsOptions,
459
- ) {}
460
-
461
- async connect(): Promise<void> {
462
- try {
463
- const connectOptions: Deno.ConnectOptions = {
464
- hostname: this.hostname,
465
- port: this.port,
466
- transport: "tcp",
467
- };
468
-
469
- if (this.secure) {
470
- const tlsConnectOptions: Deno.ConnectTlsOptions = {
471
- hostname: this.hostname,
472
- port: this.port,
473
- caCerts: this.tlsOptions?.ca
474
- ? (Array.isArray(this.tlsOptions.ca)
475
- ? [...this.tlsOptions.ca]
476
- : [this.tlsOptions.ca])
477
- : undefined,
478
- };
479
- const connectPromise = Deno.connectTls(tlsConnectOptions);
480
- if (this.timeout > 0) {
481
- let timeoutId: number;
482
- let timedOut = false;
483
- const timeoutPromise = new Promise<never>((_, reject) => {
484
- timeoutId = setTimeout(() => {
485
- timedOut = true;
486
- reject(new Error("TCP connection timeout"));
487
- }, this.timeout);
488
- });
489
-
490
- try {
491
- this.connection = await Promise.race([
492
- connectPromise,
493
- timeoutPromise,
494
- ]);
495
- } catch (error) {
496
- // If timed out, clean up the connection when it eventually completes
497
- if (timedOut) {
498
- connectPromise
499
- .then((conn) => {
500
- try {
501
- conn.close();
502
- } catch {
503
- // Ignore close errors
504
- }
505
- })
506
- .catch(() => {
507
- // Ignore connection errors
508
- });
509
- }
510
- throw error;
511
- } finally {
512
- clearTimeout(timeoutId!);
513
- }
514
- } else {
515
- this.connection = await connectPromise;
516
- }
517
- } else { // Insecure TCP connection
518
- if (this.timeout > 0) {
519
- // Use AbortController for proper timeout handling
520
- const controller = new AbortController();
521
- const timeoutId = setTimeout(() => {
522
- controller.abort();
523
- }, this.timeout);
524
-
525
- try {
526
- this.connection = await Deno.connect({
527
- ...connectOptions,
528
- signal: controller.signal,
529
- });
530
- clearTimeout(timeoutId);
531
- } catch (error) {
532
- clearTimeout(timeoutId);
533
- if (controller.signal.aborted) {
534
- throw new Error("TCP connection timeout");
535
- }
536
- throw error;
537
- }
538
- } else {
539
- this.connection = await Deno.connect(connectOptions);
540
- }
541
- }
542
- } catch (error) {
543
- throw new Error(`Failed to connect to syslog server: ${error}`);
544
- }
545
- }
546
-
547
- async send(message: string): Promise<void> {
548
- if (!this.connection) {
549
- throw new Error("Connection not established");
550
- }
551
-
552
- const data = this.encoder.encode(message + "\n");
553
-
554
- try {
555
- if (this.timeout > 0) {
556
- // Implement timeout for send using Promise.race
557
- const writePromise = this.connection.write(data);
558
-
559
- const timeoutPromise = new Promise<never>((_, reject) => {
560
- setTimeout(() => {
561
- reject(new Error("TCP send timeout"));
562
- }, this.timeout);
563
- });
564
-
565
- await Promise.race([writePromise, timeoutPromise]);
566
- } else {
567
- // No timeout
568
- await this.connection.write(data);
569
- }
570
- } catch (error) {
571
- throw new Error(`Failed to send syslog message: ${error}`);
572
- }
573
- }
574
-
575
- close(): void {
576
- if (this.connection) {
577
- try {
578
- this.connection.close();
579
- } catch {
580
- // Ignore errors during close
581
- }
582
- this.connection = undefined;
583
- }
584
- }
585
- }
586
-
587
- /**
588
- * Node.js TCP syslog connection implementation.
589
- * @since 0.12.0
590
- */
591
- export class NodeTcpSyslogConnection implements SyslogConnection {
592
- private connection?: Socket | tls.TLSSocket;
593
- private encoder = new TextEncoder();
594
-
595
- constructor(
596
- private hostname: string,
597
- private port: number,
598
- private timeout: number,
599
- private secure: boolean,
600
- private tlsOptions?: SyslogTlsOptions,
601
- ) {}
602
-
603
- connect(): Promise<void> {
604
- try {
605
- return new Promise<void>((resolve, reject) => {
606
- const connectionOptions: tls.ConnectionOptions = {
607
- port: this.port,
608
- host: this.hostname,
609
- timeout: this.timeout,
610
- rejectUnauthorized: this.tlsOptions?.rejectUnauthorized ?? true,
611
- ca: this.tlsOptions?.ca
612
- ? (Array.isArray(this.tlsOptions.ca)
613
- ? [...this.tlsOptions.ca]
614
- : [this.tlsOptions.ca])
615
- : undefined,
616
- };
617
-
618
- const socket: Socket | tls.TLSSocket = this.secure
619
- ? tls.connect(connectionOptions)
620
- : new Socket();
621
-
622
- const timeout = setTimeout(() => {
623
- socket.destroy();
624
- reject(new Error("TCP connection timeout"));
625
- }, this.timeout);
626
-
627
- socket.on("connect", () => {
628
- clearTimeout(timeout);
629
- this.connection = socket;
630
- resolve();
631
- });
632
-
633
- socket.on("error", (error) => {
634
- clearTimeout(timeout);
635
- reject(error);
636
- });
637
-
638
- // For non-TLS sockets, explicitly call connect
639
- if (!this.secure) {
640
- (socket as Socket).connect(this.port, this.hostname);
641
- }
642
- });
643
- } catch (error) {
644
- throw new Error(`Failed to connect to syslog server: ${error}`);
645
- }
646
- }
647
-
648
- send(message: string): Promise<void> {
649
- if (!this.connection) {
650
- throw new Error("Connection not established");
651
- }
652
-
653
- const data = this.encoder.encode(message + "\n");
654
-
655
- try {
656
- return new Promise<void>((resolve, reject) => {
657
- this.connection!.write(data, (error?: Error | null) => {
658
- if (error) {
659
- reject(error);
660
- } else {
661
- resolve();
662
- }
663
- });
664
- });
665
- } catch (error) {
666
- throw new Error(`Failed to send syslog message: ${error}`);
667
- }
668
- }
669
-
670
- close(): void {
671
- if (this.connection) {
672
- try {
673
- this.connection.end();
674
- } catch {
675
- // Ignore errors during close
676
- }
677
- this.connection = undefined;
678
- }
679
- }
680
- }
681
-
682
- /**
683
- * Creates a syslog sink that sends log messages to a syslog server using the
684
- * RFC 5424 syslog protocol format.
685
- *
686
- * This sink supports both UDP and TCP protocols for reliable log transmission
687
- * to centralized logging systems. It automatically formats log records according
688
- * to RFC 5424 specification, including structured data support for log properties.
689
- *
690
- * ## Features
691
- *
692
- * - **RFC 5424 Compliance**: Full implementation of the RFC 5424 syslog protocol
693
- * - **Cross-Runtime Support**: Works with Deno, Node.js, Bun, and browsers
694
- * - **Multiple Protocols**: Supports both UDP (fire-and-forget) and TCP (reliable) delivery
695
- * - **Structured Data**: Automatically includes log record properties as RFC 5424 structured data
696
- * - **Facility Support**: All standard syslog facilities (kern, user, mail, daemon, local0-7, etc.)
697
- * - **Automatic Escaping**: Proper escaping of special characters in structured data values
698
- * - **Connection Management**: Automatic connection handling with configurable timeouts
699
- *
700
- * ## Protocol Differences
701
- *
702
- * - **UDP**: Fast, connectionless delivery suitable for high-throughput logging.
703
- * Messages may be lost during network issues but has minimal performance impact.
704
- * - **TCP**: Reliable, connection-based delivery that ensures message delivery.
705
- * Higher overhead but guarantees that log messages reach the server.
706
- *
707
- * @param options Configuration options for the syslog sink
708
- * @returns A sink function that sends log records to the syslog server, implementing AsyncDisposable for proper cleanup
709
- *
710
- * @example Basic usage with default options
711
- * ```typescript
712
- * import { configure } from "@logtape/logtape";
713
- * import { getSyslogSink } from "@logtape/syslog";
714
- *
715
- * await configure({
716
- * sinks: {
717
- * syslog: getSyslogSink(), // Sends to localhost:514 via UDP
718
- * },
719
- * loggers: [
720
- * { category: [], sinks: ["syslog"], lowestLevel: "info" },
721
- * ],
722
- * });
723
- * ```
724
- *
725
- * @example Custom syslog server configuration
726
- * ```typescript
727
- * import { configure } from "@logtape/logtape";
728
- * import { getSyslogSink } from "@logtape/syslog";
729
- *
730
- * await configure({
731
- * sinks: {
732
- * syslog: getSyslogSink({
733
- * hostname: "log-server.example.com",
734
- * port: 1514,
735
- * protocol: "tcp",
736
- * facility: "mail",
737
- * appName: "my-application",
738
- * timeout: 10000,
739
- * }),
740
- * },
741
- * loggers: [
742
- * { category: [], sinks: ["syslog"], lowestLevel: "debug" },
743
- * ],
744
- * });
745
- * ```
746
- *
747
- * @example Using structured data for log properties
748
- * ```typescript
749
- * import { configure, getLogger } from "@logtape/logtape";
750
- * import { getSyslogSink } from "@logtape/syslog";
751
- *
752
- * await configure({
753
- * sinks: {
754
- * syslog: getSyslogSink({
755
- * includeStructuredData: true,
756
- * structuredDataId: "myapp@12345",
757
- * }),
758
- * },
759
- * loggers: [
760
- * { category: [], sinks: ["syslog"], lowestLevel: "info" },
761
- * ],
762
- * });
763
- *
764
- * const logger = getLogger();
765
- * // This will include userId and action as structured data
766
- * logger.info("User action completed", { userId: 123, action: "login" });
767
- * // Results in: <134>1 2024-01-01T12:00:00.000Z hostname myapp 1234 - [myapp@12345 userId="123" action="login"] User action completed
768
- * ```
769
- *
770
- * @since 0.12.0
771
- * @see {@link https://tools.ietf.org/html/rfc5424} RFC 5424 - The Syslog Protocol
772
- * @see {@link SyslogSinkOptions} for detailed configuration options
773
- */
774
- export function getSyslogSink(
775
- options: SyslogSinkOptions = {},
776
- ): Sink & AsyncDisposable {
777
- const hostname = options.hostname ?? "localhost";
778
- const port = options.port ?? 514;
779
- const protocol = options.protocol ?? "udp";
780
- const secure = options.secure ?? false;
781
- const tlsOptions = options.tlsOptions;
782
- const facility = options.facility ?? "local0";
783
- const appName = options.appName ?? "logtape";
784
- const syslogHostname = options.syslogHostname ?? getSystemHostname();
785
- const processId = options.processId ?? getProcessId();
786
- const timeout = options.timeout ?? 5000;
787
- const includeStructuredData = options.includeStructuredData ?? false;
788
- const structuredDataId = options.structuredDataId ?? "logtape@32473";
789
-
790
- const formatOptions = {
791
- facility,
792
- appName,
793
- syslogHostname,
794
- processId,
795
- includeStructuredData,
796
- structuredDataId,
797
- };
798
-
799
- // Create connection based on protocol and runtime
800
- const connection: SyslogConnection = (() => {
801
- if (typeof Deno !== "undefined") {
802
- // Deno runtime
803
- return protocol === "tcp"
804
- ? new DenoTcpSyslogConnection(
805
- hostname,
806
- port,
807
- timeout,
808
- secure,
809
- tlsOptions,
810
- )
811
- : new DenoUdpSyslogConnection(hostname, port, timeout);
812
- } else {
813
- // Node.js runtime (and Bun, which uses Node.js APIs)
814
- return protocol === "tcp"
815
- ? new NodeTcpSyslogConnection(
816
- hostname,
817
- port,
818
- timeout,
819
- secure,
820
- tlsOptions,
821
- )
822
- : new NodeUdpSyslogConnection(hostname, port, timeout);
823
- }
824
- })();
825
-
826
- let isConnected = false;
827
- let lastPromise = Promise.resolve();
828
-
829
- const sink: Sink & AsyncDisposable = (record: LogRecord) => {
830
- const syslogMessage = formatSyslogMessage(record, formatOptions);
831
-
832
- lastPromise = lastPromise
833
- .then(async () => {
834
- if (!isConnected) {
835
- await connection.connect();
836
- isConnected = true;
837
- }
838
- await connection.send(syslogMessage);
839
- })
840
- .catch((error) => {
841
- // If connection fails, try to reconnect on next message
842
- isConnected = false;
843
- throw error;
844
- });
845
- };
846
-
847
- sink[Symbol.asyncDispose] = async () => {
848
- await lastPromise.catch(() => {}); // Wait for any pending operations
849
- connection.close();
850
- isConnected = false;
851
- };
852
-
853
- // Expose for testing purposes
854
- Object.defineProperty(sink, "_internal_lastPromise", {
855
- get: () => lastPromise,
856
- enumerable: false,
857
- });
858
-
859
- return sink;
860
- }