oneuptime/Telemetry/Utils/SyslogParser.ts
Nawaz Dhandala f49b1995df
feat(telemetry): add new Telemetry service (OTel, Syslog, Fluent, Metrics, Traces) and unified ingestion pipeline
- Add Telemetry service entrypoint
  - Telemetry/Index.ts: app bootstrap, routes mounting, infrastructure init and Telemetry SDK init.

- Unified queue + worker
  - Telemetry/Jobs/TelemetryIngest/ProcessTelemetry.ts: single worker that dispatches queued jobs to specific processors (logs, traces, metrics, syslog, fluent logs).
  - Telemetry/Services/Queue/TelemetryQueueService.ts: central queue API and job payload types.
  - Per-type Queue wrappers (LogsQueueService, MetricsQueueService, TracesQueueService, FluentLogsQueueService, SyslogQueueService).

- OpenTelemetry ingestion middleware and proto support
  - Telemetry/Middleware/OtelRequestMiddleware.ts: detect OTLP endpoint (logs/traces/metrics), decode protobuf bodies using protobufjs and set product type.
  - Telemetry/ProtoFiles/OTel/v1/*.proto: include common.proto, logs.proto, metrics.proto, resource.proto, traces.proto for OTLP v1 messages.

- Ingest services
  - Telemetry/Services/OtelLogsIngestService.ts: parse incoming OTLP logs, map attributes, convert timestamps, batch insert logs.
  - Telemetry/Services/OtelTracesIngestService.ts: parse OTLP traces, build span rows, extract exceptions, batch insert spans and exceptions, save telemetry exception summary.
  - Telemetry/Services/OtelMetricsIngestService.ts: parse OTLP metrics, normalize datapoints, batch insert metrics and index metric name -> service map.
  - Telemetry/Services/SyslogIngestService.ts: syslog ingestion endpoints, parser integration, map syslog fields to attributes and logs.
  - Telemetry/Services/FluentLogsIngestService.ts: ingest Fluentd style logs, normalize entries and insert into log backend.
  - Telemetry/Services/OtelIngestBaseService.ts: helpers to resolve service name from attributes/headers.

- Syslog parser and utilities
  - Telemetry/Utils/SyslogParser.ts: robust RFC5424 and RFC3164 parser, structured data extraction and sanitization.
  - Telemetry/Tests/Utils/SyslogParser.test.ts: unit tests for parser behavior.

- Telemetry exception utilities
  - Telemetry/Utils/Exception.ts: generate exception fingerprint and upsert telemetry exception status (saveOrUpdateTelemetryException).

- Queue & job integration
  - New integration with Common/Server/Infrastructure/Queue and QueueWorker, job id generation and telemetry job types.
  - Telemetry services add ingestion jobs instead of processing synchronously.

- Config, build and dev tooling
  - Add Telemetry/package.json, package-lock.json, tsconfig.json, nodemon.json, jest config.
  - New script configs and dependencies (protobufjs, ts-node, jest, nodemon, etc).

- Docker / environment updates
  - docker-compose.base.yml, docker-compose.dev.yml, docker-compose.yml: rename service from open-telemetry-ingest -> telemetry and wire TELEMETRY_* envs.
  - config.example.env: rename and consolidate environment variables (OPEN_TELEMETRY_* -> TELEMETRY_*, update hostnames and ports).
  - Tests/Scripts/status-check.sh: update ready-check target to telemetry/status/ready.

- Other
  - Telemetry/Services/Queue/*: export helpers and legacy-compatible job interface shims.
  - Memory cleanup and batching safeguards across ingest services.
  - Logging and capture spans added to key code paths.

BREAKING CHANGES / MIGRATION NOTES:
- Environment variables and docker service names changed:
  - Replace OPEN_TELEMETRY_... vars with TELEMETRY_... (PORT, HOSTNAME, CONCURRENCY, DISABLE_TELEMETRY, etc).
  - docker-compose entries moved from "open-telemetry-ingest" to "telemetry" and image name changed to oneuptime/telemetry.
  - Update any deployment automation and monitoring checks referencing the old service name or endpoints.
- Consumers: OTLP endpoints and behavior remain supported, but ingestion is now queued and processed asynchronously.

Testing / Running:
- Install deps in Telemetry/ (npm install) after syncing Common workspace.
- Run dev: npx nodemon (nodemon.json) or build & start using provided scripts.
- Run tests with jest (Telemetry test suite includes SyslogParser unit tests).

Files added/modified (high level):
- Added many files under Telemetry/: Index, Jobs, Middleware, ProtoFiles, Services, Utils, Tests, package and config artifacts.
- Modified docker-compose.* and config.example.env and status check script to use new TELEMETRY service/vars.
2025-11-07 21:36:47 +00:00

359 lines
8.8 KiB
TypeScript

import OneUptimeDate from "Common/Types/Date";
export interface ParsedSyslogStructuredData {
[sdId: string]: {
[key: string]: string;
};
}
export interface ParsedSyslogMessage {
raw: string;
message: string;
priority?: number | undefined;
severity?: number | undefined;
facility?: number | undefined;
version?: number | undefined;
timestamp?: Date | undefined;
hostname?: string | undefined;
appName?: string | undefined;
procId?: string | undefined;
msgId?: string | undefined;
structuredDataRaw?: string | undefined;
structuredData?: ParsedSyslogStructuredData | undefined;
}
export function parseSyslogMessage(raw: string): ParsedSyslogMessage | null {
if (!raw) {
return null;
}
const trimmed: string = raw.trim();
if (!trimmed) {
return null;
}
let remaining: string = trimmed;
let priority: number | undefined;
let severity: number | undefined;
let facility: number | undefined;
const priorityMatch: RegExpMatchArray | null =
remaining.match(/^<(\d{1,3})>/);
if (priorityMatch) {
priority = parseInt(priorityMatch[1]!, 10);
if (!isNaN(priority)) {
severity = priority % 8;
facility = Math.floor(priority / 8);
}
remaining = remaining.slice(priorityMatch[0]!.length);
}
const rfc5424Parsed: ParsedSyslogMessage | null = parseRfc5424(remaining);
if (rfc5424Parsed) {
return {
raw: trimmed,
priority,
severity: rfc5424Parsed.severity ?? severity,
facility: rfc5424Parsed.facility ?? facility,
version: rfc5424Parsed.version,
timestamp: rfc5424Parsed.timestamp,
hostname: rfc5424Parsed.hostname,
appName: rfc5424Parsed.appName,
procId: rfc5424Parsed.procId,
msgId: rfc5424Parsed.msgId,
structuredDataRaw: rfc5424Parsed.structuredDataRaw,
structuredData: rfc5424Parsed.structuredData,
message: stripBom(rfc5424Parsed.message ?? ""),
};
}
const rfc3164Parsed: ParsedSyslogMessage | null = parseRfc3164(remaining);
if (rfc3164Parsed) {
return {
raw: trimmed,
priority,
severity,
facility,
timestamp: rfc3164Parsed.timestamp,
hostname: rfc3164Parsed.hostname,
appName: rfc3164Parsed.appName,
procId: rfc3164Parsed.procId,
message: stripBom(rfc3164Parsed.message ?? ""),
};
}
return {
raw: trimmed,
priority,
severity,
facility,
message: stripBom(remaining.trim()),
};
}
function parseRfc5424(payload: string): ParsedSyslogMessage | null {
const tokens: Array<string> = splitTokens(payload, 7);
if (tokens.length < 7) {
return null;
}
const versionToken: string = tokens[0]!;
const versionRegex: RegExp = /^\d+$/;
if (!versionRegex.test(versionToken)) {
return null;
}
const version: number = parseInt(versionToken, 10);
const timestampToken: string = tokens[1]!.trim().replace(/^NILVALUE$/i, "-");
const hostnameToken: string = tokens[2]!;
const appNameToken: string = tokens[3]!;
const procIdToken: string = tokens[4]!;
const msgIdToken: string = tokens[5]!;
const structuredDataAndMessage: string = tokens[6]!;
const timestamp: Date | undefined =
timestampToken && timestampToken !== "-"
? OneUptimeDate.parseRfc5424Timestamp(timestampToken)
: undefined;
const hostname: string | undefined =
hostnameToken && hostnameToken !== "-" ? hostnameToken : undefined;
const appName: string | undefined =
appNameToken && appNameToken !== "-" ? appNameToken : undefined;
const procId: string | undefined =
procIdToken && procIdToken !== "-" ? procIdToken : undefined;
const msgId: string | undefined =
msgIdToken && msgIdToken !== "-" ? msgIdToken : undefined;
const structuredDataParsed: {
structuredDataRaw?: string;
message?: string;
structuredData?: ParsedSyslogStructuredData;
} = extractStructuredData(structuredDataAndMessage);
return {
raw: payload,
version,
timestamp,
hostname,
appName,
procId,
msgId,
structuredDataRaw: structuredDataParsed.structuredDataRaw,
structuredData: structuredDataParsed.structuredData,
message: structuredDataParsed.message ?? "",
};
}
function parseRfc3164(payload: string): ParsedSyslogMessage | null {
const match: RegExpMatchArray | null = payload.match(
/^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+(\S+)\s+(.*)$/,
);
if (!match) {
return null;
}
const timestampToken: string = match[1]!;
const hostname: string = match[2]!;
const rest: string = match[3] ?? "";
const timestamp: Date | undefined =
OneUptimeDate.parseRfc3164Timestamp(timestampToken);
let appName: string | undefined;
let procId: string | undefined;
let message: string = rest.trim();
const colonIndex: number = rest.indexOf(":");
if (colonIndex !== -1) {
const tag: string = rest.slice(0, colonIndex);
message = rest.slice(colonIndex + 1).trim();
const procMatch: RegExpMatchArray | null = tag.match(/^([^[]+)\[(.+)\]$/);
if (procMatch) {
appName = procMatch[1]?.trim();
procId = procMatch[2]?.trim();
} else {
appName = tag.trim();
}
} else {
const firstTokenMatch: RegExpMatchArray | null = rest.match(/^(\S+)/);
if (firstTokenMatch) {
const firstToken: string = firstTokenMatch[1]!;
const procMatch: RegExpMatchArray | null =
firstToken.match(/^([^[]+)\[(.+)\]$/);
if (procMatch) {
appName = procMatch[1]?.trim();
procId = procMatch[2]?.trim();
} else {
appName = firstToken.trim();
}
message = rest.slice(firstToken.length).trim();
}
}
return {
raw: payload,
timestamp,
hostname,
appName,
procId,
message,
};
}
function splitTokens(source: string, expected: number): Array<string> {
const tokens: Array<string> = [];
let remaining: string = source.trimStart();
for (let i: number = 0; i < expected - 1; i++) {
if (!remaining) {
tokens.push("");
continue;
}
const match: RegExpMatchArray | null = remaining.match(/^(\S+)/);
if (!match) {
tokens.push("");
remaining = "";
continue;
}
const token: string = match[1]!;
tokens.push(token);
remaining = remaining.slice(token.length).trimStart();
}
tokens.push(remaining);
return tokens;
}
function extractStructuredData(value: string): {
structuredDataRaw?: string;
message?: string;
structuredData?: ParsedSyslogStructuredData;
} {
const trimmed: string = value.trimStart();
if (!trimmed) {
return { message: "" };
}
if (trimmed.startsWith("-")) {
return { message: trimmed.slice(1).trimStart() };
}
if (!trimmed.startsWith("[")) {
return { message: trimmed };
}
let depth: number = 0;
for (let i: number = 0; i < trimmed.length; i++) {
const char: string = trimmed[i]!;
if (char === "[") {
depth++;
} else if (char === "]") {
depth--;
if (depth === 0) {
let peekIndex: number = i + 1;
while (peekIndex < trimmed.length && trimmed[peekIndex] === " ") {
peekIndex++;
}
if (trimmed[peekIndex] === "[") {
i = peekIndex - 1;
continue;
}
const structuredDataRaw: string = trimmed.slice(0, i + 1).trimEnd();
const message: string = trimmed.slice(i + 1).trimStart();
return {
structuredDataRaw,
structuredData: parseStructuredData(structuredDataRaw),
message,
};
}
}
}
const structuredDataRaw: string = trimmed.trim();
return {
structuredDataRaw,
structuredData: parseStructuredData(structuredDataRaw),
message: "",
};
}
function parseStructuredData(raw: string): ParsedSyslogStructuredData {
const result: ParsedSyslogStructuredData = {};
const sdRegex: RegExp = /\[([^\s\]]+)((?:\s+[^\s=]+="[^"]*")*)\]/g;
let match: RegExpExecArray | null;
while ((match = sdRegex.exec(raw)) !== null) {
const sdIdRaw: string = match[1]!;
const params: string = match[2] ?? "";
const sdId: string = sanitizeKey(sdIdRaw);
if (!result[sdId]) {
result[sdId] = {};
}
const paramRegex: RegExp = /([^\s=]+)="([^"]*)"/g;
let paramMatch: RegExpExecArray | null;
while ((paramMatch = paramRegex.exec(params)) !== null) {
const keyRaw: string = paramMatch[1]!;
const value: string = paramMatch[2] ?? "";
const key: string = sanitizeKey(keyRaw);
const entry: { [key: string]: string } = result[sdId] ?? {};
entry[key] = value;
result[sdId] = entry;
}
}
return result;
}
function sanitizeKey(key: string): string {
return key.replace(/[^A-Za-z0-9_.-]/g, "_");
}
function stripBom(value: string): string {
if (!value) {
return value;
}
let output: string = value.replace(/^\uFEFF/, "");
if (output.startsWith("BOM")) {
output = output.slice(3);
}
return output.trimStart();
}