Skip to content

Instantly share code, notes, and snippets.

@trvswgnr
Created November 21, 2024 22:43
Show Gist options
  • Save trvswgnr/24447a12ef01a90e428b187af9b79b6b to your computer and use it in GitHub Desktop.
Save trvswgnr/24447a12ef01a90e428b187af9b79b6b to your computer and use it in GitHub Desktop.
Syslog Protocol (RFC 5424) Log to Console in TypeScript
/**
* 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