UNPKG

@logtape/syslog

Version:

Syslog sink for LogTape

468 lines (466 loc) 13.2 kB
import { createSocket } from "node:dgram"; import { Socket } from "node:net"; import { hostname } from "node:os"; import process from "node:process"; //#region syslog.ts /** * Syslog facility code mapping. * @since 0.12.0 */ const FACILITY_CODES = { kernel: 0, user: 1, mail: 2, daemon: 3, security: 4, syslog: 5, lpr: 6, news: 7, uucp: 8, cron: 9, authpriv: 10, ftp: 11, ntp: 12, logaudit: 13, logalert: 14, clock: 15, local0: 16, local1: 17, local2: 18, local3: 19, local4: 20, local5: 21, local6: 22, local7: 23 }; /** * Syslog severity levels as defined in RFC 5424. * @since 0.12.0 */ const SEVERITY_LEVELS = { fatal: 0, error: 3, warning: 4, info: 6, debug: 7, trace: 7 }; /** * Calculates the priority value for a syslog message. * Priority = Facility * 8 + Severity * @since 0.12.0 */ function calculatePriority(facility, severity) { const facilityCode = FACILITY_CODES[facility]; return facilityCode * 8 + severity; } /** * Formats a timestamp (number) as RFC 3339 timestamp for syslog. * @since 0.12.0 */ function formatTimestamp(timestamp) { return new Date(timestamp).toISOString(); } /** * Escapes special characters in structured data values. * @since 0.12.0 */ function escapeStructuredDataValue(value) { return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/]/g, "\\]"); } /** * Formats structured data from log record properties. * @since 0.12.0 */ function formatStructuredData(record, structuredDataId) { if (!record.properties || Object.keys(record.properties).length === 0) return "-"; const elements = []; for (const [key, value] of Object.entries(record.properties)) { const escapedValue = escapeStructuredDataValue(String(value)); elements.push(`${key}="${escapedValue}"`); } return `[${structuredDataId} ${elements.join(" ")}]`; } /** * Formats a log record as RFC 5424 syslog message. * @since 0.12.0 */ function formatSyslogMessage(record, options) { const severity = SEVERITY_LEVELS[record.level]; const priority = calculatePriority(options.facility, severity); const timestamp = formatTimestamp(record.timestamp); const hostname$1 = options.syslogHostname || "-"; const appName = options.appName || "-"; const processId = options.processId || "-"; const msgId = "-"; let structuredData = "-"; if (options.includeStructuredData) structuredData = formatStructuredData(record, options.structuredDataId); let message = ""; for (let i = 0; i < record.message.length; i++) if (i % 2 === 0) message += record.message[i]; else message += JSON.stringify(record.message[i]); return `<${priority}>1 ${timestamp} ${hostname$1} ${appName} ${processId} ${msgId} ${structuredData} ${message}`; } /** * Gets the system hostname. * @since 0.12.0 */ function getSystemHostname() { try { if (typeof Deno !== "undefined" && Deno.hostname) return Deno.hostname(); return hostname(); } catch { return process.env.HOSTNAME || "localhost"; } } /** * Gets the current process ID. * @since 0.12.0 */ function getProcessId() { try { if (typeof Deno !== "undefined" && Deno.pid) return Deno.pid.toString(); return process.pid.toString(); } catch { return "-"; } } /** * Deno UDP syslog connection implementation. * @since 0.12.0 */ var DenoUdpSyslogConnection = class { encoder = new TextEncoder(); constructor(hostname$1, port, timeout) { this.hostname = hostname$1; this.port = port; this.timeout = timeout; } connect() {} send(message) { const data = this.encoder.encode(message); try { const socket = createSocket("udp4"); if (this.timeout > 0) return new Promise((resolve, reject) => { const timeout = setTimeout(() => { socket.close(); reject(new Error("UDP send timeout")); }, this.timeout); socket.send(data, this.port, this.hostname, (error) => { clearTimeout(timeout); socket.close(); if (error) reject(error); else resolve(); }); }); else return new Promise((resolve, reject) => { socket.send(data, this.port, this.hostname, (error) => { socket.close(); if (error) reject(error); else resolve(); }); }); } catch (error) { throw new Error(`Failed to send syslog message: ${error}`); } } close() {} }; /** * Node.js UDP syslog connection implementation. * @since 0.12.0 */ var NodeUdpSyslogConnection = class { encoder = new TextEncoder(); constructor(hostname$1, port, timeout) { this.hostname = hostname$1; this.port = port; this.timeout = timeout; } connect() {} send(message) { const data = this.encoder.encode(message); try { const socket = createSocket("udp4"); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { socket.close(); reject(new Error("UDP send timeout")); }, this.timeout); socket.send(data, this.port, this.hostname, (error) => { clearTimeout(timeout); socket.close(); if (error) reject(error); else resolve(); }); }); } catch (error) { throw new Error(`Failed to send syslog message: ${error}`); } } close() {} }; /** * Deno TCP syslog connection implementation. * @since 0.12.0 */ var DenoTcpSyslogConnection = class { connection; encoder = new TextEncoder(); constructor(hostname$1, port, timeout) { this.hostname = hostname$1; this.port = port; this.timeout = timeout; } async connect() { try { if (this.timeout > 0) { const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, this.timeout); try { this.connection = await Deno.connect({ hostname: this.hostname, port: this.port, transport: "tcp", signal: controller.signal }); clearTimeout(timeoutId); } catch (error) { clearTimeout(timeoutId); if (controller.signal.aborted) throw new Error("TCP connection timeout"); throw error; } } else this.connection = await Deno.connect({ hostname: this.hostname, port: this.port, transport: "tcp" }); } catch (error) { throw new Error(`Failed to connect to syslog server: ${error}`); } } async send(message) { if (!this.connection) throw new Error("Connection not established"); const data = this.encoder.encode(message + "\n"); try { if (this.timeout > 0) { const writePromise = this.connection.write(data); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error("TCP send timeout")); }, this.timeout); }); await Promise.race([writePromise, timeoutPromise]); } else await this.connection.write(data); } catch (error) { throw new Error(`Failed to send syslog message: ${error}`); } } close() { if (this.connection) { try { this.connection.close(); } catch {} this.connection = void 0; } } }; /** * Node.js TCP syslog connection implementation. * @since 0.12.0 */ var NodeTcpSyslogConnection = class { connection; encoder = new TextEncoder(); constructor(hostname$1, port, timeout) { this.hostname = hostname$1; this.port = port; this.timeout = timeout; } connect() { try { return new Promise((resolve, reject) => { const socket = new Socket(); const timeout = setTimeout(() => { socket.destroy(); reject(new Error("TCP connection timeout")); }, this.timeout); socket.on("connect", () => { clearTimeout(timeout); this.connection = socket; resolve(); }); socket.on("error", (error) => { clearTimeout(timeout); reject(error); }); socket.connect(this.port, this.hostname); }); } catch (error) { throw new Error(`Failed to connect to syslog server: ${error}`); } } send(message) { if (!this.connection) throw new Error("Connection not established"); const data = this.encoder.encode(message + "\n"); try { return new Promise((resolve, reject) => { this.connection.write(data, (error) => { if (error) reject(error); else resolve(); }); }); } catch (error) { throw new Error(`Failed to send syslog message: ${error}`); } } close() { if (this.connection) { try { this.connection.end(); } catch {} this.connection = void 0; } } }; /** * Creates a syslog sink that sends log messages to a syslog server using the * RFC 5424 syslog protocol format. * * This sink supports both UDP and TCP protocols for reliable log transmission * to centralized logging systems. It automatically formats log records according * to RFC 5424 specification, including structured data support for log properties. * * ## Features * * - **RFC 5424 Compliance**: Full implementation of the RFC 5424 syslog protocol * - **Cross-Runtime Support**: Works with Deno, Node.js, Bun, and browsers * - **Multiple Protocols**: Supports both UDP (fire-and-forget) and TCP (reliable) delivery * - **Structured Data**: Automatically includes log record properties as RFC 5424 structured data * - **Facility Support**: All standard syslog facilities (kern, user, mail, daemon, local0-7, etc.) * - **Automatic Escaping**: Proper escaping of special characters in structured data values * - **Connection Management**: Automatic connection handling with configurable timeouts * * ## Protocol Differences * * - **UDP**: Fast, connectionless delivery suitable for high-throughput logging. * Messages may be lost during network issues but has minimal performance impact. * - **TCP**: Reliable, connection-based delivery that ensures message delivery. * Higher overhead but guarantees that log messages reach the server. * * @param options Configuration options for the syslog sink * @returns A sink function that sends log records to the syslog server, implementing AsyncDisposable for proper cleanup * * @example Basic usage with default options * ```typescript * import { configure } from "@logtape/logtape"; * import { getSyslogSink } from "@logtape/syslog"; * * await configure({ * sinks: { * syslog: getSyslogSink(), // Sends to localhost:514 via UDP * }, * loggers: [ * { category: [], sinks: ["syslog"], lowestLevel: "info" }, * ], * }); * ``` * * @example Custom syslog server configuration * ```typescript * import { configure } from "@logtape/logtape"; * import { getSyslogSink } from "@logtape/syslog"; * * await configure({ * sinks: { * syslog: getSyslogSink({ * hostname: "log-server.example.com", * port: 1514, * protocol: "tcp", * facility: "mail", * appName: "my-application", * timeout: 10000, * }), * }, * loggers: [ * { category: [], sinks: ["syslog"], lowestLevel: "debug" }, * ], * }); * ``` * * @example Using structured data for log properties * ```typescript * import { configure, getLogger } from "@logtape/logtape"; * import { getSyslogSink } from "@logtape/syslog"; * * await configure({ * sinks: { * syslog: getSyslogSink({ * includeStructuredData: true, * structuredDataId: "myapp@12345", * }), * }, * loggers: [ * { category: [], sinks: ["syslog"], lowestLevel: "info" }, * ], * }); * * const logger = getLogger(); * // This will include userId and action as structured data * logger.info("User action completed", { userId: 123, action: "login" }); * // Results in: <134>1 2024-01-01T12:00:00.000Z hostname myapp 1234 - [myapp@12345 userId="123" action="login"] User action completed * ``` * * @since 0.12.0 * @see {@link https://tools.ietf.org/html/rfc5424} RFC 5424 - The Syslog Protocol * @see {@link SyslogSinkOptions} for detailed configuration options */ function getSyslogSink(options = {}) { const hostname$1 = options.hostname ?? "localhost"; const port = options.port ?? 514; const protocol = options.protocol ?? "udp"; const facility = options.facility ?? "local0"; const appName = options.appName ?? "logtape"; const syslogHostname = options.syslogHostname ?? getSystemHostname(); const processId = options.processId ?? getProcessId(); const timeout = options.timeout ?? 5e3; const includeStructuredData = options.includeStructuredData ?? false; const structuredDataId = options.structuredDataId ?? "logtape@32473"; const formatOptions = { facility, appName, syslogHostname, processId, includeStructuredData, structuredDataId }; const connection = (() => { if (typeof Deno !== "undefined") return protocol === "tcp" ? new DenoTcpSyslogConnection(hostname$1, port, timeout) : new DenoUdpSyslogConnection(hostname$1, port, timeout); else return protocol === "tcp" ? new NodeTcpSyslogConnection(hostname$1, port, timeout) : new NodeUdpSyslogConnection(hostname$1, port, timeout); })(); let isConnected = false; let lastPromise = Promise.resolve(); const sink = (record) => { const syslogMessage = formatSyslogMessage(record, formatOptions); lastPromise = lastPromise.then(async () => { if (!isConnected) { await connection.connect(); isConnected = true; } await connection.send(syslogMessage); }).catch((error) => { isConnected = false; throw error; }); }; sink[Symbol.asyncDispose] = async () => { await lastPromise.catch(() => {}); connection.close(); isConnected = false; }; return sink; } //#endregion export { getSyslogSink }; //# sourceMappingURL=syslog.js.map