@logtape/syslog
Version:
Syslog sink for LogTape
468 lines (466 loc) • 13.5 kB
JavaScript
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
const node_dgram = require_rolldown_runtime.__toESM(require("node:dgram"));
const node_net = require_rolldown_runtime.__toESM(require("node:net"));
const node_os = require_rolldown_runtime.__toESM(require("node:os"));
const node_process = require_rolldown_runtime.__toESM(require("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 (0, node_os.hostname)();
} catch {
return node_process.default.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 node_process.default.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 = (0, node_dgram.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 = (0, node_dgram.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 node_net.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
exports.getSyslogSink = getSyslogSink;