From dac8100bc636a2c111af68d6002ad6ecd649961a Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Sun, 5 Oct 2025 15:24:17 -0700 Subject: [PATCH] add comments and test coverage to logging module --- libs/logging/src/index.ts | 37 +++++++++++++--- libs/logging/src/logging.spec.ts | 75 +++++++++++++++++++++++++++++--- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/libs/logging/src/index.ts b/libs/logging/src/index.ts index eccc178766b..a12c40ef075 100644 --- a/libs/logging/src/index.ts +++ b/libs/logging/src/index.ts @@ -7,10 +7,37 @@ export { LogLevel } from "./log-level"; export { ConsoleLogService } from "./console-log.service"; export { SemanticLogger } from "./semantic-logger.abstraction"; -/** Creates a semantic logger. - * @param context all logs emitted by the logger are extended with - * these fields. - * @remarks The `message`, `level`, `provider`, and `content` fields - * are reserved for use by the semantic logging system. +/** + * Creates a semantic logger with a fixed context that is included in all log messages. + * + * @param context - Contextual metadata that will be included in every log entry + * emitted by the returned logger. This is used to identify the source or scope + * of log messages (e.g., `{ type: "ImportService" }` or `{ accountId: "123" }`). + * + * @returns A SemanticLogger instance that includes the provided context in all log output. + * + * @remarks + * By convention, avoid using the following field names in the context object, as they + * may conflict with fields added by the semantic logging implementation: + * - `message` - The log message text + * - `level` - The log level (debug, info, warn, error, panic) + * - `provider` - The logging provider identifier + * - `content` - Additional data passed to individual log calls + * + * Note: These field names are not enforced at compile-time or runtime, but using them + * may result in unexpected behavior or field name collisions in log output. + * + * @example + * ```typescript + * // Create a logger for a service + * const log = logProvider({ type: "ImportService" }); + * + * // All logs from this logger will include { type: "ImportService" } + * log.debug("Starting import"); + * // Output: { type: "ImportService", level: "debug", message: "Starting import" } + * + * log.info({ itemCount: 42 }, "Import complete"); + * // Output: { type: "ImportService", level: "info", content: { itemCount: 42 }, message: "Import complete" } + * ``` */ export type LogProvider = (context: Jsonify) => SemanticLogger; diff --git a/libs/logging/src/logging.spec.ts b/libs/logging/src/logging.spec.ts index 04a057a156f..826ef9bf022 100644 --- a/libs/logging/src/logging.spec.ts +++ b/libs/logging/src/logging.spec.ts @@ -1,8 +1,73 @@ -import * as lib from "./index"; +import { mock } from "jest-mock-extended"; -describe("logging", () => { - // This test will fail until something is exported from index.ts - it("should work", () => { - expect(lib).toBeDefined(); +import * as lib from "./index"; +import { SemanticLogger } from "./index"; + +describe("logging module", () => { + describe("public API", () => { + it("should export LogService", () => { + expect(lib.LogService).toBeDefined(); + }); + + it("should export LogLevel", () => { + expect(lib.LogLevel).toBeDefined(); + }); + + it("should export ConsoleLogService", () => { + expect(lib.ConsoleLogService).toBeDefined(); + }); + }); + + describe("SemanticLogger", () => { + let logger: SemanticLogger; + + beforeEach(() => { + logger = mock(); + }); + + describe("logging methods", () => { + it("should accept a message string", () => { + logger.debug("debug message"); + logger.info("info message"); + logger.warn("warn message"); + logger.error("error message"); + + expect(logger.debug).toHaveBeenCalledWith("debug message"); + expect(logger.info).toHaveBeenCalledWith("info message"); + expect(logger.warn).toHaveBeenCalledWith("warn message"); + expect(logger.error).toHaveBeenCalledWith("error message"); + }); + + it("should accept content object and optional message", () => { + logger.debug({ step: 1 }, "processing step"); + logger.info({ count: 42 }, "items processed"); + logger.warn({ threshold: 100 }, "approaching limit"); + logger.error({ code: 500 }, "server error"); + + expect(logger.debug).toHaveBeenCalledWith({ step: 1 }, "processing step"); + expect(logger.info).toHaveBeenCalledWith({ count: 42 }, "items processed"); + expect(logger.warn).toHaveBeenCalledWith({ threshold: 100 }, "approaching limit"); + expect(logger.error).toHaveBeenCalledWith({ code: 500 }, "server error"); + }); + }); + + describe("panic", () => { + beforeEach(() => { + logger.panic = jest.fn((content: any, msg?: string) => { + const errorMsg = msg || (typeof content === "string" ? content : "panic"); + throw new Error(errorMsg); + }) as any; + }); + + it("should throw when called with a message", () => { + expect(() => logger.panic("critical error")).toThrow("critical error"); + }); + + it("should throw when called with content and message", () => { + expect(() => logger.panic({ reason: "invalid state" }, "system panic")).toThrow( + "system panic", + ); + }); + }); }); });