alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
400 lines (347 loc) • 13.9 kB
text/typescript
import { Alepha } from "alepha";
import { describe, it } from "vitest";
import {
JsonFormatterProvider,
LogDestinationProvider,
LogFormatterProvider,
MemoryDestinationProvider,
PrettyFormatterProvider,
RawFormatterProvider,
} from "../index.ts";
import { Logger } from "../services/Logger.ts";
/**
* Exposes the protected `formatter` getter so the runtime format override
* can be asserted by provider identity.
*/
class TestLogger extends Logger {
public get activeFormatter() {
return this.formatter;
}
}
describe("Logger", () => {
const createLogger = (service = "TestService", module = "test.module") => {
const alepha = Alepha.create({
env: { LOG_LEVEL: "info" },
})
.with({
provide: LogDestinationProvider,
use: MemoryDestinationProvider,
})
.with({
provide: LogFormatterProvider,
use: PrettyFormatterProvider,
});
return alepha.inject(Logger, {
lifetime: "transient",
args: [service, module],
});
};
describe("parseLevel", () => {
it("should return global level when no matching module found", ({
expect,
}) => {
const logger = createLogger();
// When no module-specific config matches, it falls back to global level
expect(logger.parseLevel("debug", "non.matching.module")).toBe("DEBUG");
expect(
logger.parseLevel("specific.module:trace,warn", "non.matching.module"),
).toBe("WARN");
});
it("should parse simple global level", ({ expect }) => {
const logger = createLogger();
expect(logger.parseLevel("debug", "any.module")).toBe("DEBUG");
expect(logger.parseLevel("trace", "any.module")).toBe("TRACE");
expect(logger.parseLevel("warn", "any.module")).toBe("WARN");
expect(logger.parseLevel("error", "any.module")).toBe("ERROR");
expect(logger.parseLevel("silent", "any.module")).toBe("SILENT");
});
it("should parse module-specific level with colon separator", ({
expect,
}) => {
const logger = createLogger();
expect(logger.parseLevel("test:debug,info", "test.module")).toBe("DEBUG");
expect(logger.parseLevel("alepha:trace,warn", "alepha.core")).toBe(
"TRACE",
);
expect(logger.parseLevel("my.app:error,info", "my.app.service")).toBe(
"ERROR",
);
});
it("should parse module-specific level with equals separator", ({
expect,
}) => {
const logger = createLogger();
expect(logger.parseLevel("test=debug,info", "test.module")).toBe("DEBUG");
expect(logger.parseLevel("alepha=TRACE,warn", "alepha.core")).toBe(
"TRACE",
);
expect(logger.parseLevel("my.app=error,info", "my.app.service")).toBe(
"ERROR",
);
});
it("should handle multiple module configurations", ({ expect }) => {
const logger = createLogger();
const config = "alepha.core:trace,alepha.server:debug,my.app:error,info";
expect(logger.parseLevel(config, "alepha.core")).toBe("TRACE");
expect(logger.parseLevel(config, "alepha.server")).toBe("DEBUG");
expect(logger.parseLevel(config, "my.app")).toBe("ERROR");
expect(logger.parseLevel(config, "other.module")).toBe("INFO");
});
it("should handle semicolon separators", ({ expect }) => {
const logger = createLogger();
const config = "alepha:trace;my.app:error;info";
expect(logger.parseLevel(config, "alepha.core")).toBe("TRACE");
expect(logger.parseLevel(config, "my.app")).toBe("ERROR");
expect(logger.parseLevel(config, "other.module")).toBe("INFO");
});
it("should use startsWith matching for modules", ({ expect }) => {
const logger = createLogger();
const config = "alepha:debug,info";
expect(logger.parseLevel(config, "alepha")).toBe("DEBUG");
expect(logger.parseLevel(config, "alepha.core")).toBe("DEBUG");
expect(logger.parseLevel(config, "alepha.server.cache")).toBe("DEBUG");
expect(logger.parseLevel(config, "other.alepha")).toBe("INFO");
});
it("should prioritize first matching module", ({ expect }) => {
const logger = createLogger();
const config = "alepha:debug,alepha.core:trace,info";
// alepha.core matches both "alepha" and "alepha.core", but first match wins
expect(logger.parseLevel(config, "alepha.core")).toBe("DEBUG");
});
it("should handle case insensitive levels", ({ expect }) => {
const logger = createLogger();
expect(logger.parseLevel("DEBUG", "any.module")).toBe("DEBUG");
expect(logger.parseLevel("Test:TRACE,INFO", "test.module")).toBe("TRACE");
});
it("should handle whitespace around separators", ({ expect }) => {
const logger = createLogger();
const config = " alepha : debug , my.app : error , info ";
expect(logger.parseLevel(config, "alepha.core")).toBe("DEBUG");
expect(logger.parseLevel(config, "my.app")).toBe("ERROR");
expect(logger.parseLevel(config, "other")).toBe("INFO");
});
it("should fall back to global level when no module match", ({
expect,
}) => {
const logger = createLogger();
const config = "specific.module:debug,trace";
expect(logger.parseLevel(config, "other.module")).toBe("TRACE");
});
it("should handle empty parts gracefully", ({ expect }) => {
const logger = createLogger();
// Empty parts are now skipped gracefully
expect(logger.parseLevel(",,debug,,", "any.module")).toBe("DEBUG");
expect(logger.parseLevel("alepha:trace,,info", "alepha.core")).toBe(
"TRACE",
);
expect(logger.parseLevel("alepha:trace,,", "other.module")).toBe("INFO");
expect(logger.parseLevel(" , , debug , ", "any.module")).toBe(
"DEBUG",
);
});
it("should provide better error messages", ({ expect }) => {
const logger = createLogger();
expect(() =>
logger.parseLevel("alepha:invalid,info", "alepha.core"),
).toThrow("Invalid log level 'invalid' for module pattern 'alepha'");
expect(() => logger.parseLevel("badlevel", "any.module")).toThrow(
'Invalid global log level "badlevel"',
);
});
it("should support wildcard patterns", ({ expect }) => {
const logger = createLogger();
// Basic wildcard matching
expect(logger.parseLevel("alepha.*:debug,info", "alepha.core")).toBe(
"DEBUG",
);
expect(logger.parseLevel("alepha.*:debug,info", "alepha.server")).toBe(
"DEBUG",
);
expect(logger.parseLevel("alepha.*:debug,info", "other.module")).toBe(
"INFO",
);
// More specific patterns
expect(
logger.parseLevel("*.test:silent,*.core:trace,info", "alepha.test"),
).toBe("SILENT");
expect(
logger.parseLevel("*.test:silent,*.core:trace,info", "my.core"),
).toBe("TRACE");
expect(
logger.parseLevel("*.test:silent,*.core:trace,info", "other.module"),
).toBe("INFO");
// Exact prefix match still works (existing behavior)
expect(logger.parseLevel("alepha.core:debug,info", "alepha.core")).toBe(
"DEBUG",
);
expect(
logger.parseLevel("alepha.core:debug,info", "alepha.core.service"),
).toBe("DEBUG"); // startsWith behavior
});
it("should prioritize more specific wildcard matches", ({ expect }) => {
const logger = createLogger();
// First match wins, so order matters
const config = "alepha.*:debug,alepha.core.*:trace,info";
expect(logger.parseLevel(config, "alepha.core.service")).toBe("DEBUG"); // matches alepha.* first
// Reverse order
const config2 = "alepha.core.*:trace,alepha.*:debug,info";
expect(logger.parseLevel(config2, "alepha.core.service")).toBe("TRACE"); // matches alepha.core.* first
});
});
describe("asLogLevel", () => {
it("should return valid log levels", ({ expect }) => {
const logger = createLogger();
expect(logger.asLogLevel("trace")).toBe("TRACE");
expect(logger.asLogLevel("debug")).toBe("DEBUG");
expect(logger.asLogLevel("info")).toBe("INFO");
expect(logger.asLogLevel("warn")).toBe("WARN");
expect(logger.asLogLevel("error")).toBe("ERROR");
expect(logger.asLogLevel("silent")).toBe("SILENT");
});
it("should handle whitespace", ({ expect }) => {
const logger = createLogger();
expect(logger.asLogLevel(" debug ")).toBe("DEBUG");
expect(logger.asLogLevel(" trace ")).toBe("TRACE");
});
it("should throw error for invalid levels", ({ expect }) => {
const logger = createLogger();
expect(() => logger.asLogLevel("invalid")).toThrow(
"Invalid log level: invalid",
);
expect(() => logger.asLogLevel("")).toThrow("Invalid log level: ");
expect(() => logger.asLogLevel("DEBUqG")).toThrow(
"Invalid log level: DEBUqG",
);
});
});
describe("level getter", () => {
it("should update logLevel when state changes", ({ expect }) => {
const alepha = Alepha.create({
env: { LOG_LEVEL: "info" },
})
.with({
provide: LogDestinationProvider,
use: MemoryDestinationProvider,
})
.with({
provide: LogFormatterProvider,
use: PrettyFormatterProvider,
});
const logger = alepha.inject(Logger, {
lifetime: "transient",
args: ["TestService", "test.module"],
});
// Initial state
expect(logger.level).toBe("INFO");
// Change state
alepha.store.set("alepha.logger.level", "debug");
expect(logger.level).toBe("DEBUG");
// Change to module-specific config
alepha.store.set("alepha.logger.level", "test:trace,warn");
expect(logger.level).toBe("TRACE");
});
it("should cache parsed level until state changes", ({ expect }) => {
const alepha = Alepha.create({
env: { LOG_LEVEL: "info" },
})
.with({
provide: LogDestinationProvider,
use: MemoryDestinationProvider,
})
.with({
provide: LogFormatterProvider,
use: PrettyFormatterProvider,
});
const logger = alepha.inject(Logger, {
lifetime: "transient",
args: ["TestService", "test.module"],
});
alepha.store.set("alepha.logger.level", "test:debug,info");
const level1 = logger.level;
const level2 = logger.level;
expect(level1).toBe("DEBUG");
expect(level2).toBe("DEBUG");
expect(level1).toBe(level2); // Should be same instance/value
});
});
describe("format getter", () => {
const make = () => {
const alepha = Alepha.create({ env: { LOG_LEVEL: "info" } })
.with({
provide: LogDestinationProvider,
use: MemoryDestinationProvider,
})
.with({
provide: LogFormatterProvider,
use: PrettyFormatterProvider,
});
const logger = alepha.inject(TestLogger, {
lifetime: "transient",
args: ["TestService", "test.module"],
});
return { alepha, logger };
};
it("falls back to the register-time formatter when unset", ({ expect }) => {
const { logger } = make();
expect(logger.activeFormatter).toBeInstanceOf(PrettyFormatterProvider);
});
it("honors the alepha.logger.format state override (read live)", ({
expect,
}) => {
const { alepha, logger } = make();
alepha.store.set("alepha.logger.format", "json");
expect(logger.activeFormatter).toBeInstanceOf(JsonFormatterProvider);
alepha.store.set("alepha.logger.format", "raw");
expect(logger.activeFormatter).toBeInstanceOf(RawFormatterProvider);
alepha.store.set("alepha.logger.format", "pretty");
expect(logger.activeFormatter).toBeInstanceOf(PrettyFormatterProvider);
// Unknown / cleared → back to the register-time default.
alepha.store.set("alepha.logger.format", undefined);
expect(logger.activeFormatter).toBeInstanceOf(PrettyFormatterProvider);
});
});
describe("isLevelEnabled", () => {
const at = (level: string) => {
const alepha = Alepha.create().with({
provide: LogDestinationProvider,
use: MemoryDestinationProvider,
});
const logger = alepha.inject(Logger, {
lifetime: "transient",
args: ["TestService", "test.module"],
});
// Set after inject: the logger module's register defaults the level to
// "trace" in test mode, so a pre-inject set would be overwritten.
alepha.store.set("alepha.logger.level", level);
return { alepha, logger };
};
it("enables a level only when it is at or above the active level", ({
expect,
}) => {
const { logger } = at("info");
expect(logger.isLevelEnabled("ERROR")).toBe(true);
expect(logger.isLevelEnabled("WARN")).toBe(true);
expect(logger.isLevelEnabled("INFO")).toBe(true);
expect(logger.isLevelEnabled("DEBUG")).toBe(false);
expect(logger.isLevelEnabled("TRACE")).toBe(false);
});
it("enables DEBUG once the active level is debug, but not TRACE", ({
expect,
}) => {
const { logger } = at("debug");
expect(logger.isLevelEnabled("DEBUG")).toBe(true);
expect(logger.isLevelEnabled("INFO")).toBe(true);
expect(logger.isLevelEnabled("TRACE")).toBe(false);
});
it("enables everything at trace and reacts to live state changes", ({
expect,
}) => {
const { alepha, logger } = at("trace");
expect(logger.isLevelEnabled("TRACE")).toBe(true);
expect(logger.isLevelEnabled("DEBUG")).toBe(true);
alepha.store.set("alepha.logger.level", "warn");
expect(logger.isLevelEnabled("DEBUG")).toBe(false);
expect(logger.isLevelEnabled("WARN")).toBe(true);
});
});
});