diff --git a/libs/common/src/tools/log/ecs-format/core.ts b/libs/common/src/tools/log/ecs-format/core.ts index 3667be590d5..d7d81471901 100644 --- a/libs/common/src/tools/log/ecs-format/core.ts +++ b/libs/common/src/tools/log/ecs-format/core.ts @@ -1,22 +1,35 @@ +import { Primitive } from "type-fest"; + /** Elastic Common Schema log format - core fields. */ export interface EcsFormat { - "@timestamp": Date, + "@timestamp": number; /** custom key/value pairs */ - labels?: Record, + labels?: Record; /** system message related to the event */ - message?: string, + message?: string; /** keywords tagging the event */ - tags?: Array, + tags?: Array; /** describe the event; it is recommended that all events have these. */ event: { - kind?: "alert" | "enrichment" | "event" | "metric" | "state", - category?: "api" | "authentication" | "iam" | "process" | "session" , - type?: "access" | "admin" | "allowed" | "creation" | "deletion" | "denied" | "end" | "error" | "info" | "start" | "user", - outcome?: "failure" | "success" | "unknown", - } -}; + kind?: "alert" | "enrichment" | "event" | "metric" | "state"; + category?: "api" | "authentication" | "iam" | "process" | "session"; + type?: + | "access" + | "admin" + | "allowed" + | "creation" + | "deletion" + | "denied" + | "end" + | "error" + | "info" + | "start" + | "user"; + outcome?: "failure" | "success" | "unknown"; + }; +} diff --git a/libs/common/src/tools/log/ecs-format/event.ts b/libs/common/src/tools/log/ecs-format/event.ts index 7b677496b9b..fac05df873c 100644 --- a/libs/common/src/tools/log/ecs-format/event.ts +++ b/libs/common/src/tools/log/ecs-format/event.ts @@ -4,26 +4,27 @@ import { EcsFormat } from "./core"; /** extends core event logs with additional information */ export type EventFormat = EcsFormat & { - - event: Partial & Partial & { - /** event severity as a number */ - severity?: LogLevelType, - }, -} + action?: string; + event: Partial & + Partial & { + /** event severity as a number */ + severity?: LogLevelType; + }; +}; export type ProcessEvent = { - start: Date, - duration: number, - end: Date, + start: Date; + duration: number; + end: Date; }; export type ApplicationEvent = { - /** source of the event; this is usually a client type or service name */ - provider: string, + /** source of the event; this is usually a client type or service name */ + provider: string; - /** reason why the event occurred, according to the source */ - reason: string, + /** reason why the event occurred, according to the source */ + reason: string; - /** reference URL for the event */ - reference: string, + /** reference URL for the event */ + reference: string; }; diff --git a/libs/common/src/tools/log/ecs-format/service.ts b/libs/common/src/tools/log/ecs-format/service.ts new file mode 100644 index 00000000000..24cf6972f06 --- /dev/null +++ b/libs/common/src/tools/log/ecs-format/service.ts @@ -0,0 +1,23 @@ +import { EcsFormat } from "./core"; + +export type ServiceFormat = EcsFormat & { + /** documents the program providing the log */ + service: { + /** Which kind of client is it? */ + name: "android" | "cli" | "desktop" | "extension" | "ios" | "web"; + + /** identifies the service as a type of client device */ + type: "client"; + + /** Information about the instance of the service providing the log */ + node: { + /** a unique identifier(s) for this client installation */ + name: string; + }; + /** The environment to which the client was connected */ + environment: "production" | "testing" | "development" | "local"; + + /** the unique identifier(s) for this client installation */ + version: "2025.3.1-innovation-sprint"; + }; +}; diff --git a/libs/common/src/tools/log/ecs-semantic-logger.ts b/libs/common/src/tools/log/ecs-semantic-logger.ts new file mode 100644 index 00000000000..bec1cc70d6d --- /dev/null +++ b/libs/common/src/tools/log/ecs-semantic-logger.ts @@ -0,0 +1,126 @@ +import { Subject } from "rxjs"; +import { Primitive } from "type-fest"; + +import { LogService } from "../../platform/abstractions/log.service"; +import { LogLevelType } from "../../platform/enums"; + +import { EcsFormat, ErrorFormat, EventFormat, LogFormat, UserFormat } from "./ecs-format"; +import { ServiceFormat } from "./ecs-format/service"; +import { SemanticLogger } from "./semantic-logger.abstraction"; + +type ClientInfo = ServiceFormat & { log: { logger: string } }; + +export class EcsLogger implements SemanticLogger { + constructor( + private logger: LogService, + private clientInfo: ClientInfo, + private now = () => Date.now(), + ) { + this.pipe = new Subject(); + } + + private pipe: Subject; + + log$() { + return this.pipe.asObservable(); + } + + event(info: EventFormat) {} + + debug(labels: Record | string, message?: string): void { + this.log(labels, LogLevelType.Debug, message); + } + + info(labels: Record | string, message?: string): void { + this.log(labels, LogLevelType.Info, message); + } + + warn(labels: Record | string, message?: string): void { + this.log(labels, LogLevelType.Warning, message); + } + + caught(error: Error, labels?: Record) { + const log: LogFormat & ErrorFormat & Partial = { + ...this.clientInfo, + error: { + message: error.message, + stack_trace: error.stack, + type: error.name, + }, + labels, + event: { + kind: "event", + category: "process", + type: "error", + ...this.clientInfo.event, + }, + log: { + level: stringifyLevel(LogLevelType.Error), + ...this.clientInfo.log, + }, + "@timestamp": this.now(), + }; + + this.write(log); + } + + error(labels: Record | string, message?: string): void { + this.log(labels, LogLevelType.Error, message); + } + + panic(labels: Record | string, message?: string): never { + const panicMessage = this.log(labels, LogLevelType.Error, message) ?? "a fatal error occurred"; + throw new Error(panicMessage); + } + + private log( + content: Record | string, + level: LogLevelType, + maybeMessage?: string, + ) { + const labels = typeof content === "string" ? { content } : content; + const message = + maybeMessage ?? (typeof content === "string" ? content : "message not provided"); + + const log: LogFormat & EventFormat = { + ...this.clientInfo, + message, + labels, + event: { + kind: "event", + category: "process", + type: level === LogLevelType.Error ? "error" : "info", + severity: level, + ...this.clientInfo.event, + }, + log: { + level: stringifyLevel(level), + ...this.clientInfo.log, + }, + "@timestamp": this.now(), + }; + + this.write(log); + + return log.message; + } + + private write(event: EcsFormat) { + this.pipe.next(event); + } +} + +function stringifyLevel(level: LogLevelType) { + switch (level) { + case LogLevelType.Debug: + return "debug"; + case LogLevelType.Info: + return "info"; + case LogLevelType.Warning: + return "warn"; + case LogLevelType.Error: + return "error"; + default: + return "info"; + } +}