Skip to main content
Glama
logger.test.js25.7 kB
/** * Tests for the centralized logging system */ import { vi } from "vitest"; // Import from source for proper coverage collection import { Logger, LoggerFactory, createLogger, createSiteLogger, createRequestLogger } from "../../src/utils/logger.ts"; // IMPORTANT: Import Config from the same path the logger uses (@/config/Config.js -> dist) // to ensure we're resetting the same singleton instance import { Config } from "../../dist/config/Config.js"; // Mock console methods const originalConsoleError = console.error; const originalConsoleLog = console.log; describe("Logger", () => { let consoleErrorSpy; let consoleLogSpy; beforeEach(() => { // Reset config singleton Config.reset(); // Mock console methods consoleErrorSpy = vi.fn(); consoleLogSpy = vi.fn(); console.error = consoleErrorSpy; console.log = consoleLogSpy; // Mock environment for testing process.env.NODE_ENV = "test"; process.env.DEBUG = "false"; process.env.LOG_LEVEL = "info"; }); afterEach(() => { // Restore console methods console.error = originalConsoleError; console.log = originalConsoleLog; // Reset environment delete process.env.NODE_ENV; delete process.env.DEBUG; delete process.env.LOG_LEVEL; // Reset config singleton Config.reset(); }); describe("Basic Logging", () => { it("should create a logger instance", () => { const logger = new Logger(); expect(logger).toBeInstanceOf(Logger); }); it("should log info messages", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = new Logger(); logger.info("Test message"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("INFO"); expect(logOutput).toContain("Test message"); }); it("should log with context", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = new Logger(); logger.info("Test message", { userId: 123, action: "test" }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("Test message"); expect(logOutput).toContain("userId"); expect(logOutput).toContain("123"); }); it("should respect log levels", () => { process.env.NODE_ENV = "development"; process.env.LOG_LEVEL = "warn"; Config.reset(); const logger = new Logger(); // Debug should not log logger.debug("Debug message"); expect(consoleErrorSpy).not.toHaveBeenCalled(); // Info should not log logger.info("Info message"); expect(consoleErrorSpy).not.toHaveBeenCalled(); // Warn should log logger.warn("Warning message"); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); // Error should log logger.error("Error message"); expect(consoleErrorSpy).toHaveBeenCalledTimes(2); }); }); describe("Child Loggers", () => { it("should create child logger with additional context", () => { process.env.NODE_ENV = "development"; Config.reset(); const parentLogger = new Logger({ component: "Parent" }); const childLogger = parentLogger.child({ component: "Child", siteId: "site1" }); childLogger.info("Child message"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[Child]"); expect(logOutput).toContain("{site:site1}"); }); it("should inherit parent context", () => { process.env.NODE_ENV = "development"; Config.reset(); const parentLogger = new Logger({ context: { parentKey: "parentValue" } }); const childLogger = parentLogger.child({ context: { childKey: "childValue" } }); childLogger.info("Test message"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("parentKey"); expect(logOutput).toContain("childKey"); }); }); describe("Error Handling", () => { it("should log Error objects properly", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = new Logger(); const error = new Error("Test error"); logger.error(error); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("ERROR"); expect(logOutput).toContain("Test error"); }); it("should log error with additional context", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = new Logger(); const error = new Error("Test error"); logger.error(error, { operation: "test-operation" }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("Test error"); expect(logOutput).toContain("operation"); }); }); describe("Sensitive Data Sanitization", () => { it("should redact passwords", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = new Logger(); logger.info("Login attempt", { username: "testuser", password: "secret123", appPassword: "app-secret", }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("username"); expect(logOutput).toContain("testuser"); expect(logOutput).toContain("[REDACTED:"); expect(logOutput).not.toContain("secret123"); expect(logOutput).not.toContain("app-secret"); }); it("should redact tokens and keys", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = new Logger(); logger.info("API call", { apiKey: "key123", token: "token456", secret: "secret789", credential: "cred000", }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).not.toContain("key123"); expect(logOutput).not.toContain("token456"); expect(logOutput).not.toContain("secret789"); expect(logOutput).not.toContain("cred000"); expect(logOutput).toContain("[REDACTED:"); }); }); describe("Environment-Specific Behavior", () => { it("should suppress most logs in test environment", () => { process.env.NODE_ENV = "test"; Config.reset(); const logger = new Logger(); logger.debug("Debug"); logger.info("Info"); logger.warn("Warn"); // In test mode, only errors are logged expect(consoleErrorSpy).not.toHaveBeenCalled(); logger.error("Error"); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); it("should suppress logs in DXT mode", () => { process.env.NODE_ENV = "dxt"; Config.reset(); const logger = new Logger(); logger.debug("Debug"); logger.info("Info"); // In DXT mode, only warnings and errors are logged expect(consoleErrorSpy).not.toHaveBeenCalled(); logger.warn("Warning"); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); }); describe("LoggerFactory", () => { it("should create API logger", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = LoggerFactory.api("site1"); logger.info("API request"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[API]"); expect(logOutput).toContain("{site:site1}"); }); it("should create cache logger", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = LoggerFactory.cache(); logger.info("Cache hit"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[CACHE]"); }); it("should create tool logger", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = LoggerFactory.tool("wp_posts", "site1"); logger.info("Tool executed"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[TOOL:wp_posts]"); expect(logOutput).toContain("{site:site1}"); }); it("should create server logger", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = LoggerFactory.server(); logger.info("Server started"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[SERVER]"); }); }); describe("Helper Functions", () => { it("should create component logger", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = createLogger("MyComponent"); logger.info("Component message"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[MyComponent]"); }); it("should create site-specific logger", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = createSiteLogger("site1", "Auth"); logger.info("Auth check"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[Auth]"); expect(logOutput).toContain("{site:site1}"); }); it("should create request-specific logger", () => { process.env.NODE_ENV = "development"; Config.reset(); const logger = createRequestLogger("req-123", "API", "site1"); logger.info("Request processed"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[API]"); expect(logOutput).toContain("{site:site1}"); expect(logOutput).toContain("{req:req-123"); }); }); describe("Timing Functions", () => { it("should time synchronous operations", () => { process.env.NODE_ENV = "development"; process.env.LOG_LEVEL = "debug"; Config.reset(); const logger = new Logger(); const result = logger.time("Test operation", () => { return "result"; }); expect(result).toBe("result"); expect(consoleErrorSpy).toHaveBeenCalledTimes(2); const startLog = consoleErrorSpy.mock.calls[0][0]; expect(startLog).toContain("Starting: Test operation"); const endLog = consoleErrorSpy.mock.calls[1][0]; expect(endLog).toContain("Completed: Test operation"); expect(endLog).toContain("duration"); }); it("should time async operations", async () => { process.env.NODE_ENV = "development"; process.env.LOG_LEVEL = "debug"; Config.reset(); const logger = new Logger(); const result = await logger.time("Async operation", async () => { await new Promise((resolve) => setTimeout(resolve, 10)); return "async-result"; }); expect(result).toBe("async-result"); expect(consoleErrorSpy).toHaveBeenCalledTimes(2); const endLog = consoleErrorSpy.mock.calls[1][0]; expect(endLog).toContain("Completed: Async operation"); expect(endLog).toContain("duration"); }); it("should log timing errors", async () => { process.env.NODE_ENV = "development"; process.env.LOG_LEVEL = "debug"; Config.reset(); const logger = new Logger(); await expect( logger.time("Failing operation", async () => { throw new Error("Operation failed"); }), ).rejects.toThrow("Operation failed"); expect(consoleErrorSpy).toHaveBeenCalledTimes(2); const errorLog = consoleErrorSpy.mock.calls[1][0]; expect(errorLog).toContain("Failed: Failing operation"); expect(errorLog).toContain("duration"); }); }); describe("Log Levels", () => { it("should support all log levels", () => { process.env.NODE_ENV = "development"; process.env.LOG_LEVEL = "trace"; Config.reset(); const logger = new Logger(); logger.trace("Trace message"); logger.debug("Debug message"); logger.info("Info message"); logger.warn("Warn message"); logger.error("Error message"); logger.fatal("Fatal message"); expect(consoleErrorSpy).toHaveBeenCalledTimes(6); const messages = consoleErrorSpy.mock.calls.map((call) => call[0]); expect(messages[0]).toContain("TRACE"); expect(messages[1]).toContain("DEBUG"); expect(messages[2]).toContain("INFO"); expect(messages[3]).toContain("WARN"); expect(messages[4]).toContain("ERROR"); expect(messages[5]).toContain("FATAL"); }); }); describe("LoggerFactory Comprehensive Tests", () => { beforeEach(() => { process.env.NODE_ENV = "development"; Config.reset(); }); it("should create auth logger", () => { const logger = LoggerFactory.auth("site1"); logger.info("Authentication check"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[AUTH]"); expect(logOutput).toContain("{site:site1}"); }); it("should create auth logger without site", () => { const logger = LoggerFactory.auth(); logger.info("Authentication check"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[AUTH]"); expect(logOutput).not.toContain("{site:"); }); it("should create config logger", () => { const logger = LoggerFactory.config(); logger.info("Configuration loaded"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[CONFIG]"); }); it("should create security logger", () => { const logger = LoggerFactory.security(); logger.warn("Security alert"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[SECURITY]"); }); it("should create performance logger", () => { const logger = LoggerFactory.performance(); logger.info("Performance metric"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[PERF]"); }); it("should create API logger without site", () => { const logger = LoggerFactory.api(); logger.info("API request"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[API]"); expect(logOutput).not.toContain("{site:"); }); it("should create cache logger with site", () => { const logger = LoggerFactory.cache("site2"); logger.info("Cache operation"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[CACHE]"); expect(logOutput).toContain("{site:site2}"); }); it("should create tool logger without site", () => { const logger = LoggerFactory.tool("wp_posts"); logger.info("Tool executed"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[TOOL:wp_posts]"); expect(logOutput).not.toContain("{site:"); }); }); describe("Advanced Error Handling", () => { beforeEach(() => { process.env.NODE_ENV = "development"; Config.reset(); }); it("should handle fatal error messages", () => { const logger = new Logger(); logger.fatal("Fatal system error"); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("FATAL"); expect(logOutput).toContain("Fatal system error"); }); it("should handle fatal Error objects", () => { const logger = new Logger(); const error = new Error("Critical failure"); error.stack = "Error: Critical failure\n at test"; logger.fatal(error); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("FATAL"); expect(logOutput).toContain("Critical failure"); expect(logOutput).toContain("errorName"); expect(logOutput).toContain("errorStack"); }); it("should handle fatal Error objects with context", () => { const logger = new Logger(); const error = new Error("Critical failure"); logger.fatal(error, { operation: "critical-task" }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("operation"); expect(logOutput).toContain("critical-task"); }); it("should handle fatal non-Error objects", () => { const logger = new Logger(); const nonErrorObj = { message: "Non-error fatal", code: 500 }; logger.fatal(nonErrorObj); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("FATAL"); expect(logOutput).toContain("[object Object]"); }); it("should handle fatal non-Error primitives", () => { const logger = new Logger(); logger.fatal(12345); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("FATAL"); expect(logOutput).toContain("12345"); }); it("should handle error Error objects with context", () => { const logger = new Logger(); const error = new Error("Regular error"); logger.error(error, { operation: "regular-task" }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("operation"); expect(logOutput).toContain("regular-task"); }); }); describe("Context Merging and Sanitization", () => { beforeEach(() => { process.env.NODE_ENV = "development"; Config.reset(); }); it("should merge logger context with message context", () => { const logger = new Logger({ context: { baseKey: "baseValue", shared: "fromLogger" }, }); logger.info("Test message", { messageKey: "messageValue", shared: "fromMessage" }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("baseKey"); expect(logOutput).toContain("messageKey"); expect(logOutput).toContain("fromMessage"); // Message context should override logger context }); it("should sanitize context with empty sensitive values", () => { const logger = new Logger(); logger.info("Test", { password: "", token: "", apiKey: "", secret: "", }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[EMPTY]"); // Password key is preserved but value is sanitized expect(logOutput).toContain("password"); expect(logOutput).not.toContain('""'); }); it("should preserve non-string sensitive values", () => { const logger = new Logger(); logger.info("Test", { password: 123, token: null, apiKey: undefined, secret: { nested: "value" }, }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("123"); expect(logOutput).toContain("null"); expect(logOutput).toContain("nested"); }); it("should sanitize sensitive fields containing array values", () => { const logger = new Logger(); logger.info("Test", { password: ["secret1", "secret2"], token: [123, 456], apiKey: [null, undefined], secret: [{ nested: "value" }, "anotherSecret"], }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; // Sensitive array values should be sanitized, not exposing their contents expect(logOutput).toContain("password"); expect(logOutput).not.toContain("secret1"); expect(logOutput).not.toContain("secret2"); expect(logOutput).not.toContain("anotherSecret"); expect(logOutput).not.toContain("nested"); expect(logOutput).toContain("token"); expect(logOutput).toContain("apiKey"); expect(logOutput).toContain("secret"); }); }); describe("Log Level Configuration", () => { it("should respect custom LOG_LEVEL environment variable", () => { process.env.NODE_ENV = "development"; process.env.LOG_LEVEL = "error"; Config.reset(); const logger = new Logger(); logger.debug("Debug message"); logger.info("Info message"); logger.warn("Warn message"); expect(consoleErrorSpy).not.toHaveBeenCalled(); logger.error("Error message"); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); it("should default to info level for invalid LOG_LEVEL", () => { process.env.NODE_ENV = "development"; process.env.LOG_LEVEL = "invalid-level"; Config.reset(); const logger = new Logger(); logger.debug("Debug message"); expect(consoleErrorSpy).not.toHaveBeenCalled(); logger.info("Info message"); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); it("should handle LOG_LEVEL case insensitivity", () => { process.env.NODE_ENV = "development"; process.env.LOG_LEVEL = "ERROR"; // Uppercase Config.reset(); const logger = new Logger(); logger.debug("Debug message"); logger.info("Info message"); logger.warn("Warning message"); expect(consoleErrorSpy).not.toHaveBeenCalled(); logger.error("Error message"); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); }); describe("Message Formatting Edge Cases", () => { beforeEach(() => { process.env.NODE_ENV = "development"; Config.reset(); }); it("should handle empty context objects", () => { const logger = new Logger(); logger.info("Test message", {}); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("Test message"); expect(logOutput).not.toContain("{}"); }); it("should handle deeply nested context objects", () => { const logger = new Logger(); const nestedContext = { user: { id: 123, profile: { name: "Alice", address: { city: "Wonderland", zip: "12345", details: { coordinates: { lat: 51.5, lng: -0.1 }, }, }, }, }, }; logger.info("Nested context test", nestedContext); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("Nested context test"); expect(logOutput).toContain("Alice"); expect(logOutput).toContain("Wonderland"); expect(logOutput).toContain("51.5"); expect(logOutput).toContain("-0.1"); }); it("should handle loggers with all options", () => { const logger = new Logger({ component: "TestComponent", siteId: "site123", requestId: "req-abcdef123456789", context: { baseContext: "value" }, }); logger.info("Full context message", { extraContext: "extra" }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[TestComponent]"); expect(logOutput).toContain("{site:site123}"); expect(logOutput).toContain("{req:req-abcd"); // Truncated to 8 chars expect(logOutput).toContain("baseContext"); expect(logOutput).toContain("extraContext"); }); it("should handle logger without context but with message context", () => { const logger = new Logger({ component: "NoContext" }); logger.info("Message", { messageKey: "messageValue" }); expect(consoleErrorSpy).toHaveBeenCalled(); const logOutput = consoleErrorSpy.mock.calls[0][0]; expect(logOutput).toContain("[NoContext]"); expect(logOutput).toContain("messageKey"); }); }); describe("Logger Edge Cases", () => { it("should not log when level is below threshold", () => { process.env.NODE_ENV = "development"; process.env.LOG_LEVEL = "warn"; Config.reset(); const logger = new Logger(); logger.log("debug", "Debug message"); logger.log("info", "Info message"); expect(consoleErrorSpy).not.toHaveBeenCalled(); logger.log("warn", "Warning message"); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server