alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
241 lines (205 loc) • 7.38 kB
text/typescript
import {
$inject,
Alepha,
AlephaError,
type LoggerInterface,
type LogLevel,
} from "alepha";
import { DateTimeProvider } from "alepha/datetime";
import { CliFormatterProvider } from "../providers/CliFormatterProvider.ts";
import { JsonFormatterProvider } from "../providers/JsonFormatterProvider.ts";
import { LogDestinationProvider } from "../providers/LogDestinationProvider.ts";
import { LogFormatterProvider } from "../providers/LogFormatterProvider.ts";
import { PrettyFormatterProvider } from "../providers/PrettyFormatterProvider.ts";
import { RawFormatterProvider } from "../providers/RawFormatterProvider.ts";
import type { LogEntry } from "../schemas/logEntrySchema.ts";
export class Logger implements LoggerInterface {
protected readonly alepha = $inject(Alepha);
/**
* Formatter chosen at register time (from `LOG_FORMAT`, honoring any
* custom `LogFormatterProvider` substitution). Used unless a runtime
* `alepha.logger.format` override is set — see {@link formatter}.
*/
protected readonly defaultFormatter = $inject(LogFormatterProvider);
protected readonly jsonFormatter = $inject(JsonFormatterProvider);
protected readonly prettyFormatter = $inject(PrettyFormatterProvider);
protected readonly rawFormatter = $inject(RawFormatterProvider);
protected readonly cliFormatter = $inject(CliFormatterProvider);
protected readonly destination = $inject(LogDestinationProvider);
protected readonly dateTimeProvider = $inject(DateTimeProvider);
protected readonly levels: Record<string, number> = {
SILENT: -1,
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3,
TRACE: 4,
};
protected readonly service: string;
protected readonly module: string;
protected readonly app?: string;
protected appLogLevel: string = "INFO";
protected logLevel: LogLevel = "INFO";
constructor(service: string, module: string) {
this.service = service;
this.module = module;
this.app = this.alepha.env.APP_NAME;
}
public get context(): string | undefined {
return this.alepha.context.get<string>("context");
}
public get level(): string {
const stateLogLevel = this.alepha.store.get("alepha.logger.level");
if (stateLogLevel && stateLogLevel !== this.appLogLevel) {
this.appLogLevel = stateLogLevel;
this.logLevel = this.parseLevel(this.appLogLevel, this.module);
}
return this.logLevel;
}
/**
* Active formatter. Honors a runtime `alepha.logger.format` override
* (read live, like {@link level}); otherwise falls back to the
* register-time {@link defaultFormatter} so custom substitutions and the
* `LOG_FORMAT` default keep working.
*/
protected get formatter(): LogFormatterProvider {
switch (this.alepha.store.get("alepha.logger.format")) {
case "json":
return this.jsonFormatter;
case "pretty":
return this.prettyFormatter;
case "raw":
return this.rawFormatter;
case "cli":
return this.cliFormatter;
default:
return this.defaultFormatter;
}
}
public parseLevel(level: string, app: string): LogLevel {
const parts = level.toLowerCase().split(/[,;]/);
// First pass: check for module-specific configurations
for (const part of parts) {
const trimmedPart = part.trim();
if (!trimmedPart) continue; // Skip empty parts
if (trimmedPart.includes(":") || trimmedPart.includes("=")) {
const [modulePattern, levelValue] = trimmedPart.split(/[:=]/);
const trimmedModule = modulePattern.trim();
const trimmedLevel = levelValue?.trim();
if (!trimmedLevel) continue; // Skip if no level specified
if (this.matchesPattern(app, trimmedModule)) {
try {
return this.asLogLevel(trimmedLevel);
} catch (error) {
throw new AlephaError(
`Invalid log level '${levelValue?.trim()}' for module pattern '${trimmedModule}'`,
);
}
}
}
}
// Second pass: look for global level
for (const part of parts) {
const trimmedPart = part.trim();
if (!trimmedPart) continue; // Skip empty parts
if (!trimmedPart.includes(":") && !trimmedPart.includes("=")) {
try {
return this.asLogLevel(trimmedPart);
} catch (error) {
throw new AlephaError(`Invalid global log level "${trimmedPart}"`);
}
}
}
return "INFO";
}
protected matchesPattern(moduleName: string, pattern: string): boolean {
if (pattern.includes("*")) {
// Convert wildcard pattern to regex
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*");
return new RegExp(`^${regexPattern}`).test(moduleName);
}
// Exact prefix match (existing behavior)
return moduleName.startsWith(pattern);
}
public asLogLevel(something: string): LogLevel {
const level = something.trim().toUpperCase();
if (this.levels[level] !== undefined) {
return level as LogLevel;
}
throw new AlephaError(`Invalid log level: ${something}`);
}
/**
* Whether a message at `level` would actually be emitted given the current
* active level (read live from state). Mirrors the threshold used by
* {@link log}: a level is enabled when it sits at or above the active one.
*
* @example
* // active level = INFO
* log.isLevelEnabled("DEBUG"); // false
* log.isLevelEnabled("WARN"); // true
*/
public isLevelEnabled(level: LogLevel): boolean {
return this.levels[level] <= this.levels[this.level];
}
// -------------------------------------------------------------------------------------------------------------------
public error(message: string, data?: unknown): void {
this.log("ERROR", message, data);
}
public warn(message: string, data?: unknown): void {
this.log("WARN", message, data);
}
public info(message: string, data?: unknown): void {
this.log("INFO", message, data);
}
public debug(message: string, data?: unknown): void {
this.log("DEBUG", message, data);
}
public trace(message: string, data?: unknown): void {
this.log("TRACE", message, data);
}
protected log(level: LogLevel, message: string, data?: unknown): void {
let _message = "";
if (typeof message === "string") {
_message = message;
} else if (typeof data === "string") {
_message = data;
}
let _data: object | Error | undefined;
if (typeof data === "object" && data) {
_data = data;
} else if (typeof message === "object" && message) {
_data = message;
}
const logEntry: LogEntry = {
level,
message: _message,
data: _data,
context: this.context,
service: this.service,
module: this.module,
app: this.app,
timestamp: this.dateTimeProvider.nowMillis(),
};
if (this.levels[level] > this.levels[this.level]) {
this.emit(logEntry);
return;
}
const formatted = this.formatter.format(logEntry);
this.emit(logEntry, formatted);
this.destination.write(formatted, logEntry);
}
protected emit(entry: LogEntry, message?: string) {
this.alepha.events
.emit(
"log",
{
message,
entry,
},
{
catch: true,
},
)
.catch(() => null);
}
}