/*! * Copyright (c) Microsoft Corporation and contributors. All rights reserved. * Licensed under the MIT License. */ import safeStringify from "json-stringify-safe"; import { v4 as uuid } from "uuid"; import { LumberEventName } from "./lumberEventNames"; import { LogLevel, LumberType, ILumberjackEngine, ILumberjackSchemaValidator, handleError, ILumberFormatter, } from "./resources"; const performanceNow = () => globalThis.performance.now(); // Lumber represents the telemetry data being captured, and it uses a list of // ILumberjackEngine to emit the data according to the engine implementation. // Lumber should be created through Lumberjack. Additional properties can be set through // setProperty(). Once the telemetry event is complete, the user must call either success() // or error() on Lumber to emit the data. /** * @internal */ export class Lumber { private readonly _startTime = performanceNow(); private _properties = new Map(); private _durationInMs?: number; private _successful?: boolean; private _message?: string; private _exception?: Error; private _logLevel?: LogLevel; private _completed = false; private _timestamp = Date.now(); public get timestamp(): number { return this._timestamp; } public readonly id = uuid(); public get properties(): Map { return this._properties; } public get durationInMs(): number | undefined { if (this.type === LumberType.Log) { return undefined; } return this._durationInMs; } public get successful(): boolean | undefined { if (this.type === LumberType.Log) { return undefined; } return this._successful; } public get message(): string | undefined { return this._message; } public get exception(): Error | undefined { return this._exception; } public get logLevel(): LogLevel | undefined { return this._logLevel; } constructor( public readonly eventName: T, public readonly type: LumberType, private readonly _engineList: ILumberjackEngine[], private readonly _schemaValidators?: ILumberjackSchemaValidator[], properties?: Map | Record, private readonly _formatters?: ILumberFormatter[], ) { if (properties) { this.setProperties(properties); } } public setProperty(key: string, value: any): this { this._properties.set(key, value); return this; } public setProperties(properties: Map | Record): this { if (properties instanceof Map) { if (this._properties.size === 0) { this._properties = new Map(properties); } else { properties.forEach((value: any, key: string) => { this.setProperty(key, value); }); } } else { Object.entries(properties).forEach((entry) => { const [key, value] = entry; this.setProperty(key, value); }); } return this; } /** * Overrides the timestamp of the telemetry event. * @param msSinceEpoch - The timestamp in milliseconds since the epoch (1970-01-01T00:00:00Z), i.e. `Date.now()` * @remarks * This is useful when a Metric's start time needs to be set retroactively, such as when an event's duration is * tracked across several service instances, then logged in a single instance where the event may not have started. */ public overrideTimestamp(msSinceEpoch: number): void { this._timestamp = msSinceEpoch; } public success(message: string, logLevel: LogLevel = LogLevel.Info) { this.emit(message, logLevel, true, undefined); } public error(message: string, exception?: any, logLevel: LogLevel = LogLevel.Error) { this.emit(message, logLevel, false, exception); } public isCompleted(): boolean { return this._completed; } private emit( message: string, logLevel: LogLevel, successful: boolean, exception: any | undefined, ) { if (this._completed) { handleError( LumberEventName.LumberjackError, `Trying to complete a Lumber telemetry operation that has alredy been completed.\ [eventName: ${this.eventName}][id: ${this.id}]`, this._engineList, ); return; } if (this._schemaValidators) { for (const schemaValidator of this._schemaValidators) { const validation = schemaValidator.validate(this.properties); if (!validation.validationPassed) { handleError( LumberEventName.LumberjackSchemaValidationFailure, `Schema validation failed for props: ${validation.validationFailedForProperties.toString()}.\ [eventName: ${this.eventName}][id: ${this.id}]`, this._engineList, ); } } } this._message = message; this._logLevel = logLevel; this._successful = successful; if (exception instanceof Error) { this._exception = exception; } else if (exception !== undefined) { // We want to log the exception even if its value is `false` this._exception = new Error(safeStringify(exception)); } const durationOverwrite = parseFloat(this.properties.get("durationInMs")); this._durationInMs = isNaN(durationOverwrite) ? performanceNow() - this._startTime : durationOverwrite; if (this._formatters) { this._formatters.forEach((formatter) => formatter.transform(this)); } this._engineList.forEach((engine) => engine.emit(this)); this._completed = true; } }