Created
November 21, 2024 22:43
-
-
Save trvswgnr/24447a12ef01a90e428b187af9b79b6b to your computer and use it in GitHub Desktop.
Syslog Protocol (RFC 5424) Log to Console in TypeScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Syslog Facility codes as defined in RFC 5424 | |
*/ | |
enum SyslogFacility { | |
KERN = 0, // kernel messages | |
USER = 1, // user-level messages | |
MAIL = 2, // mail system | |
DAEMON = 3, // system daemons | |
AUTH = 4, // security/authorization messages | |
SYSLOG = 5, // messages generated internally by syslogd | |
LPR = 6, // line printer subsystem | |
NEWS = 7, // network news subsystem | |
UUCP = 8, // UUCP subsystem | |
CRON = 9, // clock daemon | |
AUTHPRIV = 10, // security/authorization messages | |
FTP = 11, // FTP daemon | |
NTP = 12, // NTP subsystem | |
AUDIT = 13, // log audit | |
ALERT = 14, // log alert | |
CLOCK = 15, // clock daemon | |
LOCAL0 = 16, // local use 0 | |
LOCAL1 = 17, // local use 1 | |
LOCAL2 = 18, // local use 2 | |
LOCAL3 = 19, // local use 3 | |
LOCAL4 = 20, // local use 4 | |
LOCAL5 = 21, // local use 5 | |
LOCAL6 = 22, // local use 6 | |
LOCAL7 = 23, // local use 7 | |
} | |
/** | |
* Syslog Severity levels as defined in RFC 5424 | |
*/ | |
enum SyslogSeverity { | |
EMERG = 0, // Emergency: system is unusable | |
ALERT = 1, // Alert: action must be taken immediately | |
CRIT = 2, // Critical: critical conditions | |
ERR = 3, // Error: error conditions | |
WARNING = 4, // Warning: warning conditions | |
NOTICE = 5, // Notice: normal but significant condition | |
INFO = 6, // Informational: informational messages | |
DEBUG = 7, // Debug: debug-level messages | |
} | |
/** | |
* Standard Syslog structured data IDs as defined in RFC 5424 | |
*/ | |
enum StandardSD_ID { | |
TIME_QUALITY = "timeQuality", | |
ORIGIN = "origin", | |
META = "meta", | |
} | |
/** | |
* Time Quality parameters for structured data | |
*/ | |
interface TimeQualityParams { | |
tzKnown?: "0" | "1"; | |
isSynced?: "0" | "1"; | |
syncAccuracy?: string; | |
} | |
/** | |
* Origin parameters for structured data | |
*/ | |
interface OriginParams { | |
ip?: string; | |
enterpriseId?: string; | |
software?: string; | |
swVersion?: string; | |
} | |
/** | |
* Meta parameters for structured data | |
*/ | |
interface MetaParams { | |
sequenceId?: string; | |
sysUpTime?: string; | |
language?: string; | |
} | |
/** | |
* Custom structured data parameters | |
*/ | |
interface CustomSD { | |
[id: string]: { | |
[param: string]: string; | |
}; | |
} | |
/** | |
* Structured data type combining standard and custom elements | |
*/ | |
type StructuredData = Partial<{ | |
[StandardSD_ID.TIME_QUALITY]: TimeQualityParams; | |
[StandardSD_ID.ORIGIN]: OriginParams; | |
[StandardSD_ID.META]: MetaParams; | |
}> & | |
CustomSD; | |
/** | |
* Main syslog message interface with strongly typed fields | |
*/ | |
interface SyslogMessage { | |
facility: SyslogFacility; | |
severity: SyslogSeverity; | |
timestamp?: Date; | |
hostname?: string; | |
appName?: string; | |
procId?: string | number; | |
msgId?: string; | |
structuredData?: StructuredData; | |
message?: string; | |
} | |
const NILVALUE = "-"; | |
const MAX_LENGTH = { | |
HOSTNAME: 255, | |
APP_NAME: 48, | |
PROC_ID: 128, | |
MSG_ID: 32, | |
}; | |
/** | |
* Formats a value according to syslog spec, applying length limits | |
* and converting undefined/null to NILVALUE | |
*/ | |
function formatField( | |
value: string | number | undefined | null, | |
maxLength?: number, | |
): string { | |
if (value === undefined || value === null) return NILVALUE; | |
const stringValue = String(value); | |
return maxLength ? stringValue.substring(0, maxLength) : stringValue; | |
} | |
/** | |
* Escapes special characters in structured data parameter values | |
*/ | |
function escapeSDParam(value: string): string { | |
return value | |
.replace(/\\/g, "\\\\") | |
.replace(/"/g, '\\"') | |
.replace(/\]/g, "\\]"); | |
} | |
/** | |
* Formats structured data according to RFC 5424 | |
*/ | |
function formatStructuredData(data: StructuredData | undefined): string { | |
if (!data || Object.keys(data).length === 0) return NILVALUE; | |
return Object.entries(data) | |
.map(([sdId, params]) => { | |
const formattedParams = Object.entries(params) | |
.map(([name, value]) => `${name}="${escapeSDParam(String(value))}"`) | |
.join(" "); | |
return `[${sdId} ${formattedParams}]`; | |
}) | |
.join(""); | |
} | |
/** | |
* Formats a syslog message according to RFC 5424 | |
* @throws {Error} If facility or severity values are invalid | |
*/ | |
function formatSyslogMessage(input: SyslogMessage): string { | |
// Calculate priority value (PRI) | |
const pri = input.facility * 8 + input.severity; | |
// Format timestamp in RFC 5424 format with timezone | |
const timestamp = input.timestamp | |
? input.timestamp.toISOString().replace(/\.(\d{3})Z$/, ".$1+00:00") | |
: NILVALUE; | |
// Format fields with proper length limits | |
const hostname = formatField(input.hostname, MAX_LENGTH.HOSTNAME); | |
const appName = formatField(input.appName, MAX_LENGTH.APP_NAME); | |
const procId = formatField(input.procId, MAX_LENGTH.PROC_ID); | |
const msgId = formatField(input.msgId, MAX_LENGTH.MSG_ID); | |
// Format structured data | |
const structuredData = formatStructuredData(input.structuredData); | |
// Format message (if exists) | |
const message = input.message ? ` ${input.message}` : ""; | |
// Assemble the final message according to RFC 5424 format | |
return `<${pri}>1 ${timestamp} ${hostname} ${appName} ${procId} ${msgId} ${structuredData}${message}`; | |
} | |
const Syslog = { | |
format: formatSyslogMessage, | |
Facility: SyslogFacility, | |
Severity: SyslogSeverity, | |
StandardSD_ID, | |
} as const; | |
/** | |
* Logger configuration options | |
*/ | |
interface LoggerOptions { | |
/** Minimum severity level to log */ | |
minLevel?: SyslogSeverity; | |
/** Application name to include in logs */ | |
appName?: string; | |
/** Whether to use colors in console output */ | |
useColors?: boolean; | |
/** Custom facility to use (defaults to USER) */ | |
facility?: SyslogFacility; | |
} | |
/** | |
* Type for extra fields that can be included in log messages | |
*/ | |
type LogContext = Record<string, unknown>; | |
/** | |
* Color configurations for different severity levels | |
*/ | |
const COLORS = { | |
[SyslogSeverity.EMERG]: "\x1b[41m\x1b[37m", // white on red background | |
[SyslogSeverity.ALERT]: "\x1b[45m\x1b[37m", // white on magenta background | |
[SyslogSeverity.CRIT]: "\x1b[41m\x1b[37m", // white on red background | |
[SyslogSeverity.ERR]: "\x1b[31m", // red | |
[SyslogSeverity.WARNING]: "\x1b[33m", // yellow | |
[SyslogSeverity.NOTICE]: "\x1b[36m", // cyan | |
[SyslogSeverity.INFO]: "\x1b[32m", // green | |
[SyslogSeverity.DEBUG]: "\x1b[90m", // gray | |
reset: "\x1b[0m", | |
} as const; | |
export class ConsoleLogger { | |
private options: Required<LoggerOptions>; | |
private hostname: string; | |
private processId: string; | |
constructor(options: LoggerOptions = {}) { | |
this.options = { | |
minLevel: SyslogSeverity.INFO, | |
appName: "app", | |
useColors: process.stdout.isTTY, // Auto-detect color support | |
facility: SyslogFacility.USER, | |
...options, | |
}; | |
this.hostname = this.getHostname(); | |
this.processId = process.pid.toString(); | |
} | |
/** | |
* Log methods for each severity level | |
*/ | |
public emergency(message: string, context?: LogContext) { | |
this.log(SyslogSeverity.EMERG, message, context); | |
} | |
public alert(message: string, context?: LogContext) { | |
this.log(SyslogSeverity.ALERT, message, context); | |
} | |
public critical(message: string, context?: LogContext) { | |
this.log(SyslogSeverity.CRIT, message, context); | |
} | |
public error(message: string, context?: LogContext) { | |
this.log(SyslogSeverity.ERR, message, context); | |
} | |
public warning(message: string, context?: LogContext) { | |
this.log(SyslogSeverity.WARNING, message, context); | |
} | |
public notice(message: string, context?: LogContext) { | |
this.log(SyslogSeverity.NOTICE, message, context); | |
} | |
public info(message: string, context?: LogContext) { | |
this.log(SyslogSeverity.INFO, message, context); | |
} | |
public debug(message: string, context?: LogContext) { | |
this.log(SyslogSeverity.DEBUG, message, context); | |
} | |
/** | |
* Main logging method that uses our Syslog formatter | |
*/ | |
private log( | |
severity: SyslogSeverity, | |
message: string, | |
context?: LogContext, | |
): void { | |
// Check minimum log level | |
if (severity > this.options.minLevel) return; | |
// Create structured data from context | |
const structuredData: StructuredData = { | |
[StandardSD_ID.META]: { | |
sequenceId: this.generateSequenceId(), | |
}, | |
}; | |
if (context) { | |
structuredData["context@0"] = this.flattenContext(context); | |
} | |
// Create syslog message | |
const syslogMessage: SyslogMessage = { | |
facility: this.options.facility, | |
severity, | |
timestamp: new Date(), | |
hostname: this.hostname, | |
appName: this.options.appName, | |
procId: this.processId, | |
msgId: this.generateMsgId(), | |
structuredData, | |
message, | |
}; | |
// Format using our syslog formatter | |
const formattedMessage = Syslog.format(syslogMessage); | |
// Add colors if enabled and output | |
if (this.options.useColors) { | |
const color = COLORS[severity]; | |
const output = `${color}${formattedMessage}${COLORS.reset}`; | |
this.writeToConsole(severity, output); | |
} else { | |
this.writeToConsole(severity, formattedMessage); | |
} | |
} | |
/** | |
* Write to appropriate console method based on severity | |
*/ | |
private writeToConsole(severity: SyslogSeverity, message: string): void { | |
if (severity <= SyslogSeverity.ERR) { | |
console.error(message); | |
} else if (severity === SyslogSeverity.WARNING) { | |
console.warn(message); | |
} else if (severity === SyslogSeverity.DEBUG) { | |
console.debug(message); | |
} else { | |
console.log(message); | |
} | |
} | |
/** | |
* Flatten context object for structured data | |
*/ | |
private flattenContext( | |
context: LogContext, | |
prefix = "", | |
): Record<string, string> { | |
return Object.entries(context).reduce((acc, [key, value]) => { | |
const fullKey = prefix ? `${prefix}_${key}` : key; | |
if (value && typeof value === "object" && !Array.isArray(value)) { | |
if (value instanceof Error) { | |
return { | |
// biome-ignore lint/performance/noAccumulatingSpread: <explanation> | |
...acc, | |
[fullKey]: value.message, | |
[`${fullKey}_stack`]: value.stack || "", | |
}; | |
} | |
// biome-ignore lint/performance/noAccumulatingSpread: <explanation> | |
return { ...acc, ...this.flattenContext(value as LogContext, fullKey) }; | |
} | |
// biome-ignore lint/performance/noAccumulatingSpread: <explanation> | |
return { ...acc, [fullKey]: String(value) }; | |
}, {}); | |
} | |
/** | |
* Generate a unique message ID | |
*/ | |
private generateMsgId(): string { | |
return `MSG-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`; | |
} | |
/** | |
* Generate a sequence ID for the meta SD-ID | |
*/ | |
private generateSequenceId(): string { | |
return Date.now().toString(); | |
} | |
/** | |
* Get system hostname | |
*/ | |
private getHostname(): string { | |
try { | |
return require("node:os").hostname(); | |
} catch { | |
return "unknown-host"; | |
} | |
} | |
} | |
// Example usage showing proper RFC 5424 formatting: | |
function example() { | |
const logger = new ConsoleLogger({ | |
appName: "MyApp", | |
minLevel: SyslogSeverity.DEBUG, | |
facility: SyslogFacility.LOCAL0, | |
}); | |
// Will output RFC 5424 formatted logs with proper facility, severity, structured data, etc. | |
logger.info("Application started", { | |
version: "1.0.0", | |
environment: "production", | |
}); | |
logger.error("Database connection failed", { | |
error: new Error("Connection timeout"), | |
database: { | |
host: "localhost", | |
port: 5432, | |
}, | |
}); | |
logger.info("Application started", { | |
version: "1.0.0", | |
environment: "production", | |
}); | |
} | |
example(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment