alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
599 lines (598 loc) • 18.8 kB
JavaScript
import { $context, $inject, $module, Alepha, AlephaError, KIND, z } from "alepha";
import { DateTimeProvider } from "alepha/datetime";
//#region ../../src/logger/providers/ConsoleColorProvider.ts
var ConsoleColorProvider = class ConsoleColorProvider {
static COLORS = {
RESET: "\x1B[0m",
BLACK: "\x1B[30m",
RED: "\x1B[31m",
GREEN: "\x1B[32m",
ORANGE: "\x1B[33m",
BLUE: "\x1B[34m",
PURPLE: "\x1B[35m",
CYAN: "\x1B[36m",
GREY_LIGHT: "\x1B[37m",
GREY_LIGHT_BOLD: "\x1B[1;37m",
GREY_DARK: "\x1B[90m",
GREY_DARK_BOLD: "\x1B[1;90m",
WHITE: "\x1B[97m",
WHITE_BOLD: "\x1B[1;97m",
DIM: "\x1B[2m",
BOLD: "\x1B[1m",
INVERSE: "\x1B[7m",
SILENT: "",
ERROR: "\x1B[31m",
WARN: "\x1B[33m",
INFO: "\x1B[32m",
DEBUG: "\x1B[34m",
TRACE: "\x1B[90m"
};
alepha = $inject(Alepha);
enabled = true;
constructor() {
this.enabled = this.isEnabled();
}
isEnabled() {
if (this.alepha.env.NO_COLOR) return false;
if (this.alepha.env.FORCE_COLOR) return true;
if (this.alepha.isBrowser() && !navigator.userAgent.includes("Chrome")) return false;
return !this.alepha.isProduction();
}
set(color, text, reset = ConsoleColorProvider.COLORS.RESET) {
if (!this.enabled) return text;
return `${ConsoleColorProvider.COLORS[color]}${text}${reset}`;
}
};
//#endregion
//#region ../../src/logger/providers/LogFormatterProvider.ts
var LogFormatterProvider = class {};
//#endregion
//#region ../../src/logger/providers/PrettyFormatterProvider.ts
var PrettyFormatterProvider = class extends LogFormatterProvider {
color = $inject(ConsoleColorProvider);
alepha = $inject(Alepha);
format(entry) {
const { data, timestamp } = entry;
let output = "";
let details = "";
const isError = data instanceof Error;
if (isError) details = this.formatError(data);
else if (data) {
let error = "";
let jsonData = data;
if ("error" in data && data.error instanceof Error) {
error = this.formatError(data.error);
const { error: _, ...rest } = data;
jsonData = rest;
}
if (Object.keys(jsonData).length > 0) try {
details = JSON.stringify(jsonData);
} catch {
details = "[Unserializable Object]";
}
if (error) details += `\n${error}`;
}
output += this.color.set("GREY_DARK", `[${this.formatTimestamp(timestamp)}]`);
output += " ";
output += this.color.set(entry.level, entry.level.toUpperCase());
output += " ";
if (entry.app) {
output += this.color.set("GREY_DARK", `${entry.app}`);
output += " ";
}
if (entry.context) {
output += this.color.set("GREY_DARK", `(${this.formatContext(entry.context)})`);
output += " ";
}
const module = this.color.set("GREY_LIGHT", `${entry.module}.`);
const service = this.color.set(this.alepha.isBrowser() ? "RESET" : "WHITE", entry.service);
output += `<${module}${service}>`;
if (entry.message) output += `: ${this.color.set("CYAN", entry.message)}`;
else output += ":";
if (details) if (isError) output += ` \n${details}`;
else output += ` ${this.color.set("GREY_DARK", details)}`;
return output;
}
formatTimestamp(timestamp) {
const d = new Date(timestamp);
const h = d.getHours();
const m = d.getMinutes();
const s = d.getSeconds();
const ms = d.getMilliseconds();
return `${this.pad2(h)}:${this.pad2(m)}:${this.pad2(s)}.${this.pad3(ms)}`;
}
pad2 = (n) => (n < 10 ? "0" : "") + n;
pad3 = (n) => n < 10 ? `00${n}` : n < 100 ? `0${n}` : `${n}`;
/**
* Avoid to display the whole UUID in development mode
*/
formatContext(context) {
if (this.alepha.isProduction()) return context;
return context.slice(0, 8);
}
formatError(error) {
if (this.alepha.isBrowser()) {
setTimeout(() => {
console.error(error);
});
return "";
}
const vite = this.alepha.store.get("alepha.vite.server");
vite?.ssrFixStacktrace(error);
let str = error.stack ?? error.message;
let currentCause = error.cause;
while (currentCause && currentCause instanceof Error) {
vite?.ssrFixStacktrace(currentCause);
str += `\nCaused by: ${currentCause.stack ?? currentCause.message}`;
currentCause = currentCause.cause;
}
return str;
}
};
//#endregion
//#region ../../src/logger/providers/CliFormatterProvider.ts
/**
* Compact formatter for CLI output.
*
* Renders `HH:MM:SS L message {json}` — a short clock time, a single-letter
* level colored by severity, the message, and any structured data appended
* dim. Deliberately drops the `<module.service>`, app name and context UUID
* that {@link PrettyFormatterProvider} prints: in an interactive CLI session
* those are noise. Use `pretty` (or `--verbose`) when you need them, `raw`
* when you want the bare message for piping, and `json` for aggregation.
*/
var CliFormatterProvider = class extends PrettyFormatterProvider {
format(entry) {
const { data, timestamp, level } = entry;
const isError = data instanceof Error;
let details = "";
if (isError) details = this.formatError(data);
else if (data) {
let error = "";
let jsonData = data;
if ("error" in data && data.error instanceof Error) {
error = this.formatError(data.error);
const { error: _, ...rest } = data;
jsonData = rest;
}
if (Object.keys(jsonData).length > 0) try {
details = JSON.stringify(jsonData);
} catch {
details = "[Unserializable Object]";
}
if (error) details += `\n${error}`;
}
let output = "";
output += this.color.set("GREY_DARK", this.formatClockTime(timestamp));
output += " ";
output += this.color.set(level, level.charAt(0).toUpperCase());
if (entry.message) output += ` ${entry.message}`;
if (details) if (isError) output += ` \n${details}`;
else output += ` ${this.color.set("GREY_DARK", details)}`;
return output;
}
/**
* Short clock time without milliseconds (HH:MM:SS).
*/
formatClockTime(timestamp) {
const d = new Date(timestamp);
return `${this.pad2(d.getHours())}:${this.pad2(d.getMinutes())}:${this.pad2(d.getSeconds())}`;
}
};
//#endregion
//#region ../../src/logger/providers/JsonFormatterProvider.ts
var JsonFormatterProvider = class extends LogFormatterProvider {
format(entry) {
const json = {
level: entry.level,
message: entry.message,
context: entry.context,
service: entry.service,
module: entry.module,
app: entry.app,
time: entry.timestamp
};
if (entry.data instanceof Error) json.error = this.formatJsonError(entry.data);
else json.data = entry.data;
return JSON.stringify(json);
}
formatJsonError(error) {
return {
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause instanceof Error ? this.formatJsonError(error.cause) : void 0
};
}
};
//#endregion
//#region ../../src/logger/providers/LogDestinationProvider.ts
var LogDestinationProvider = class {};
//#endregion
//#region ../../src/logger/providers/RawFormatterProvider.ts
var RawFormatterProvider = class extends LogFormatterProvider {
format(entry) {
let output = "";
output += `${entry.message}`;
if (entry.data instanceof Error) {
output += `\n${entry.data.message}`;
let cause = entry.data.cause;
while (cause instanceof Error) {
output += `\nCaused by: ${cause.message}`;
cause = cause.cause;
}
}
return output;
}
};
//#endregion
//#region ../../src/logger/services/Logger.ts
var Logger = class {
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}.
*/
defaultFormatter = $inject(LogFormatterProvider);
jsonFormatter = $inject(JsonFormatterProvider);
prettyFormatter = $inject(PrettyFormatterProvider);
rawFormatter = $inject(RawFormatterProvider);
cliFormatter = $inject(CliFormatterProvider);
destination = $inject(LogDestinationProvider);
dateTimeProvider = $inject(DateTimeProvider);
levels = {
SILENT: -1,
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3,
TRACE: 4
};
service;
module;
app;
appLogLevel = "INFO";
logLevel = "INFO";
constructor(service, module) {
this.service = service;
this.module = module;
this.app = this.alepha.env.APP_NAME;
}
get context() {
return this.alepha.context.get("context");
}
get level() {
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.
*/
get formatter() {
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;
}
}
parseLevel(level, app) {
const parts = level.toLowerCase().split(/[,;]/);
for (const part of parts) {
const trimmedPart = part.trim();
if (!trimmedPart) continue;
if (trimmedPart.includes(":") || trimmedPart.includes("=")) {
const [modulePattern, levelValue] = trimmedPart.split(/[:=]/);
const trimmedModule = modulePattern.trim();
const trimmedLevel = levelValue?.trim();
if (!trimmedLevel) continue;
if (this.matchesPattern(app, trimmedModule)) try {
return this.asLogLevel(trimmedLevel);
} catch (error) {
throw new AlephaError(`Invalid log level '${levelValue?.trim()}' for module pattern '${trimmedModule}'`);
}
}
}
for (const part of parts) {
const trimmedPart = part.trim();
if (!trimmedPart) continue;
if (!trimmedPart.includes(":") && !trimmedPart.includes("=")) try {
return this.asLogLevel(trimmedPart);
} catch (error) {
throw new AlephaError(`Invalid global log level "${trimmedPart}"`);
}
}
return "INFO";
}
matchesPattern(moduleName, pattern) {
if (pattern.includes("*")) {
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*");
return new RegExp(`^${regexPattern}`).test(moduleName);
}
return moduleName.startsWith(pattern);
}
asLogLevel(something) {
const level = something.trim().toUpperCase();
if (this.levels[level] !== void 0) return level;
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
*/
isLevelEnabled(level) {
return this.levels[level] <= this.levels[this.level];
}
error(message, data) {
this.log("ERROR", message, data);
}
warn(message, data) {
this.log("WARN", message, data);
}
info(message, data) {
this.log("INFO", message, data);
}
debug(message, data) {
this.log("DEBUG", message, data);
}
trace(message, data) {
this.log("TRACE", message, data);
}
log(level, message, data) {
let _message = "";
if (typeof message === "string") _message = message;
else if (typeof data === "string") _message = data;
let _data;
if (typeof data === "object" && data) _data = data;
else if (typeof message === "object" && message) _data = message;
const 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);
}
emit(entry, message) {
this.alepha.events.emit("log", {
message,
entry
}, { catch: true }).catch(() => null);
}
};
//#endregion
//#region ../../src/logger/primitives/$logger.ts
/**
* Create a logger.
*
* `name` is optional, by default it will use the name of the service.
*
* @example
* ```ts
* import { $logger } from "alepha";
*
* class MyService {
* log = $logger();
*
* constructor() {
* this.log.info("Service initialized");
* // print something like '[23:45:53.326] INFO <app.MyService>: Service initialized'
* }
* }
* ```
*/
const $logger = (options = {}) => {
const { alepha, service, module } = $context();
return $inject(Logger, {
lifetime: "transient",
args: [options.name ?? service?.name ?? "Func", module?.name ?? alepha.env.MODULE_NAME ?? "app"]
});
};
$logger[KIND] = Logger;
//#endregion
//#region ../../src/logger/providers/ConsoleDestinationProvider.ts
var ConsoleDestinationProvider = class extends LogDestinationProvider {
write(message) {
console.log(message);
}
};
//#endregion
//#region ../../src/logger/providers/MemoryDestinationProvider.ts
var MemoryDestinationProvider = class extends LogDestinationProvider {
entries = [];
options = { maxEntries: 1e4 };
write(formatted, entry) {
this.entries.push({
...entry,
formatted
});
if (this.entries.length > this.options.maxEntries) this.entries = this.entries.slice(-Math.floor(this.options.maxEntries * .8));
}
get logs() {
return [...this.entries];
}
clear() {
this.entries = [];
}
};
//#endregion
//#region ../../src/logger/schemas/logEntrySchema.ts
const logEntrySchema = z.object({
level: z.enum([
"SILENT",
"TRACE",
"DEBUG",
"INFO",
"WARN",
"ERROR"
]),
message: z.text({ size: "rich" }),
service: z.text(),
module: z.text(),
context: z.text().optional(),
app: z.text().optional(),
data: z.any().optional(),
timestamp: z.number()
});
//#endregion
//#region ../../src/logger/index.ts
/**
* Configurable logging with multiple outputs.
*
* **Features:**
* - Global logger access
* - JSON format
* - Pretty colored output
* - Compact CLI format
* - Raw text format
* - Console destination
* - Memory destination (for devtools)
* - Custom handlers
* - Configuration via `LOG_LEVEL`, `LOG_FORMAT`, and `DEBUG`
*
* @module alepha.logger
*/
const AlephaLogger = $module({
name: "alepha.logger",
primitives: [$logger],
services: [Logger, ConsoleColorProvider],
variants: [
ConsoleDestinationProvider,
MemoryDestinationProvider,
JsonFormatterProvider,
PrettyFormatterProvider,
RawFormatterProvider,
CliFormatterProvider
],
register: (alepha) => {
const env = alepha.parseEnv(envSchema);
let logLevel = env.LOG_LEVEL;
let logFormat = env.LOG_FORMAT;
if (env.DEBUG) {
if (env.DEBUG === "1" || env.DEBUG === "true") logLevel ??= "trace";
else {
const patterns = env.DEBUG.split(",").map((p) => p.trim().replaceAll(":", ".")).filter(Boolean);
logLevel ??= `${patterns.map((p) => `${p}:debug`).join(",")},info`;
}
logFormat ??= "pretty";
}
const getLogDestinationProvider = () => {
if (alepha.isTest() && !logLevel) {
const printOnError = (ev) => {
if (ev.task?.result?.state === "fail") {
const output = alepha.inject(MemoryDestinationProvider);
for (const log of output.logs) console.log(log.formatted);
}
};
try {
alepha.store.get("alepha.test.afterEach")?.(printOnError);
alepha.store.get("alepha.test.onTestFinished")?.(printOnError);
} catch {}
return MemoryDestinationProvider;
}
return ConsoleDestinationProvider;
};
const getLogFormatterProvider = () => {
if (logFormat) {
if (logFormat === "json") return JsonFormatterProvider;
if (logFormat === "raw") return RawFormatterProvider;
if (logFormat === "cli") return CliFormatterProvider;
return PrettyFormatterProvider;
}
if (alepha.isProduction() && !alepha.isBrowser()) return JsonFormatterProvider;
return PrettyFormatterProvider;
};
alepha.with({
optional: true,
provide: LogDestinationProvider,
use: getLogDestinationProvider()
});
alepha.with({
optional: true,
provide: LogFormatterProvider,
use: getLogFormatterProvider()
});
alepha.store.set("alepha.logger", alepha.inject(Logger, {
lifetime: "transient",
args: ["Alepha", "alepha.core"]
}));
alepha.store.set("alepha.logger.level", logLevel ?? (alepha.isTest() ? "trace" : alepha.isProduction() && alepha.isBrowser() ? "warn" : "info"));
}
});
const envSchema = z.object({
/**
* Enable debug logging for specific modules using the `debug` package convention.
*
* @example
* DEBUG=1 # Shorthand for LOG_LEVEL=trace LOG_FORMAT=pretty
* DEBUG=alepha:* # Enable debug logging for all alepha modules
* DEBUG=alepha:orm:* # Enable debug logging for alepha.orm modules
* DEBUG=* # Enable debug logging for all modules
*/
DEBUG: z.text({ description: "Enable debug logging for specific modules using the debug package convention. Example: DEBUG=alepha:*" }).optional(),
/**
* Default log level for the application.
*
* Default by environment:
* - dev = info
* - prod = info
* - test = error
*
* Levels are: "trace" | "debug" | "info" | "warn" | "error" | "silent"
*
* Level can be set for a specific module:
*
* @example
* LOG_LEVEL=my.module.name:debug,info # Set debug level for my.module.name and info for all other modules
* LOG_LEVEL=alepha:trace, info # Set trace level for all alepha modules and info for all other modules
*/
LOG_LEVEL: z.text({
description: `Application log level on startup.
Levels are: trace, debug, info, warn, error, silent
Level can be set for a specific module:
"my.module.name:debug,info" -> Set debug level for my.module.name and info for all other modules
"alepha:trace,info" -> Set trace level for all alepha modules and info for all other modules`,
lowercase: true
}).optional(),
/**
* Built-in log formats.
* - "json" - JSON format, useful for structured logging and log aggregation. {@link JsonFormatterProvider}
* - "pretty" - Full text format, human-readable, with colors and module/context. {@link PrettyFormatterProvider}
* - "cli" - Compact format for CLI sessions: time, level initial, message, json. {@link CliFormatterProvider}
* - "raw" - Raw format, no formatting, just the message (best for piping). {@link RawFormatterProvider}
*/
LOG_FORMAT: z.enum([
"json",
"pretty",
"raw",
"cli"
]).meta({ lowercase: true }).describe("Default log format for the application.").optional()
});
//#endregion
export { $logger, AlephaLogger, CliFormatterProvider, ConsoleColorProvider, ConsoleDestinationProvider, JsonFormatterProvider, LogDestinationProvider, LogFormatterProvider, Logger, MemoryDestinationProvider, PrettyFormatterProvider, RawFormatterProvider, logEntrySchema };
//# sourceMappingURL=index.js.map