UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

599 lines (598 loc) 18.8 kB
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