mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-01-11 19:56:44 +00:00
- 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.
359 lines
8.8 KiB
TypeScript
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();
|
|
}
|