cordis 4.0.0-rc.6, plugin-loader, -include, -group, -timer, -hmr, -logger-console, cosmokit 1.8.1, schemastery 3.18.0 — copied from the cordis-workspace checkout, flattened under vendor/, original npm names, private: true. vendor/README.md is the manifest: upstream repos + commit SHAs, local-modification log, sync procedure. Local modification: hmr's locale YAML imports and .i18n() call removed (avoids a runtime YAML import hook we don't vendor).
263 lines
8.1 KiB
TypeScript
263 lines
8.1 KiB
TypeScript
import { defineProperty, hyphenate } from 'cosmokit'
|
|
import { Context } from './context'
|
|
import { Fiber } from './fiber'
|
|
import { createCallable, joinPrototype, symbols, Tracker } from './utils'
|
|
|
|
declare module './context' {
|
|
interface Intercept {
|
|
logger: LoggerService.Intercept
|
|
}
|
|
}
|
|
|
|
/** Logger method name and severity category. */
|
|
export type LoggerType = 'error' | 'info' | 'warn' | 'debug'
|
|
|
|
/** Callable shape for one logger severity method. */
|
|
export type LoggerMethod = (format: any, ...param: any[]) => void
|
|
|
|
/** Formatter used to resolve a printf-style placeholder. */
|
|
export type Formatter = (value: any, exporter: Exporter, message: Message) => any
|
|
|
|
/** Numeric severity used when exporters decide whether to emit a message. */
|
|
export const enum LoggerLevel {
|
|
ERROR = 0,
|
|
INFO = 1,
|
|
WARN = 2,
|
|
DEBUG = 3,
|
|
}
|
|
|
|
/** Structured log record delivered to exporters. */
|
|
export interface Message {
|
|
sn: number
|
|
ts: number
|
|
name: string
|
|
type: LoggerType
|
|
level: number
|
|
args: any[]
|
|
fiber?: WeakRef<Fiber>
|
|
}
|
|
|
|
/** Sink that receives structured log messages. */
|
|
export interface Exporter {
|
|
colors?: number | false
|
|
maxLength?: number
|
|
levels?: Record<string, number>
|
|
formatters?: Record<string, Formatter>
|
|
export(message: Message): void
|
|
}
|
|
|
|
/** Built-in placeholder formatters used by `Logger.format()`. */
|
|
export const defaultFormatters: Record<string, Formatter> = {
|
|
s: (value) => String(value),
|
|
d: (value) => Math.trunc(Number(value)),
|
|
i: (value) => Math.trunc(Number(value)),
|
|
f: (value) => Number(value),
|
|
o: (value) => JSON.stringify(value),
|
|
O: (value) => JSON.stringify(value),
|
|
c: () => '',
|
|
C: (value, exporter, message) => {
|
|
return Logger.color(exporter, Logger.code(message.name, exporter.colors), value)
|
|
},
|
|
}
|
|
|
|
/** Options used when creating a named logger facade. */
|
|
export interface LoggerOptions {
|
|
name: string
|
|
meta?: Partial<Message>
|
|
level?: number
|
|
}
|
|
|
|
/** Logger facade identity, inherited message metadata, and optional minimum level. */
|
|
export interface Logger extends LoggerOptions {}
|
|
/** Logger facade severity methods. */
|
|
export interface Logger extends Record<LoggerType, LoggerMethod> {}
|
|
|
|
function isAggregateError(error: any): error is Error & { errors: Error[] } {
|
|
return error instanceof Error && Array.isArray(error['errors'])
|
|
}
|
|
|
|
/** Logger facade for one named subsystem. */
|
|
export class Logger {
|
|
static color(exporter: Exporter, code: number, value: any, decoration = '') {
|
|
if (!exporter.colors) return '' + value
|
|
return `\u001b[3${code < 8 ? code : '8;5;' + code}${exporter.colors >= 2 ? decoration : ''}m${value}\u001b[0m`
|
|
}
|
|
|
|
static code(name: string, level?: false | number) {
|
|
let hash = 0
|
|
for (let i = 0; i < name.length; i++) {
|
|
hash = ((hash << 3) - hash) + name.charCodeAt(i) + 13
|
|
hash |= 0
|
|
}
|
|
const colors = !level ? [] : level >= 2 ? c256 : c16
|
|
return colors[Math.abs(hash) % colors.length]
|
|
}
|
|
|
|
static format(exporter: Exporter, message: Message): string {
|
|
const args = message.args.slice()
|
|
if (args[0] instanceof Error) {
|
|
args[0] = args[0].stack || args[0].message
|
|
args.unshift('%s')
|
|
} else if (typeof args[0] !== 'string') {
|
|
args.unshift('%o')
|
|
}
|
|
|
|
let format: string = args.shift()
|
|
format = format.replace(/%([a-zA-Z%])/g, (match, char) => {
|
|
if (match === '%%') return '%'
|
|
const formatter = exporter.formatters?.[char] ?? defaultFormatters[char]
|
|
if (typeof formatter === 'function') {
|
|
const value = args.shift()
|
|
return formatter(value, exporter, message)
|
|
}
|
|
return match
|
|
})
|
|
|
|
const oFormatter = exporter.formatters?.o ?? defaultFormatters.o
|
|
for (let arg of args) {
|
|
if (typeof arg === 'object' && arg) {
|
|
arg = oFormatter(arg, exporter, message)
|
|
}
|
|
format += ' ' + arg
|
|
}
|
|
|
|
const { maxLength = 10240 } = exporter
|
|
return format.split(/\r?\n/g).map(line => {
|
|
return line.slice(0, maxLength) + (line.length > maxLength ? '...' : '')
|
|
}).join('\n')
|
|
}
|
|
|
|
constructor(options: LoggerOptions, private service: LoggerService) {
|
|
Object.assign(this, options)
|
|
this.error = this._method('error', LoggerLevel.ERROR)
|
|
this.info = this._method('info', LoggerLevel.INFO)
|
|
this.warn = this._method('warn', LoggerLevel.WARN)
|
|
this.debug = this._method('debug', LoggerLevel.DEBUG)
|
|
}
|
|
|
|
private _method(type: LoggerType, level: number): LoggerMethod {
|
|
return (...args: any[]) => {
|
|
if (args.length === 1 && args[0] instanceof Error) {
|
|
if (args[0].cause) {
|
|
this[type](args[0].cause)
|
|
} else if (isAggregateError(args[0])) {
|
|
args[0].errors.forEach(error => this[type](error))
|
|
return
|
|
}
|
|
}
|
|
|
|
const sn = ++this.service._snMessage
|
|
const ts = Date.now()
|
|
for (const exporter of this.service.exporters.values()) {
|
|
const targetLevel = exporter.levels?.[this.name] ?? exporter.levels?.default ?? this.level ?? LoggerLevel.INFO
|
|
if (targetLevel < level) continue
|
|
const message: Message = { sn, ts, type, level, name: this.name, ...this.meta, args }
|
|
exporter.export(message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** ANSI 16-color palette indexes used for logger name coloring. */
|
|
export const c16 = [6, 2, 3, 4, 5, 1]
|
|
/** ANSI 256-color palette indexes used for logger name coloring. */
|
|
export const c256 = [
|
|
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62,
|
|
63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113,
|
|
129, 134, 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168,
|
|
169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200,
|
|
201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
|
|
]
|
|
|
|
/** Logger service configuration merged from context intercepts. */
|
|
export namespace LoggerService {
|
|
export interface Intercept {
|
|
name?: string
|
|
level?: number
|
|
}
|
|
}
|
|
|
|
/** Callable `ctx.logger` service shape. */
|
|
export interface LoggerService extends Record<LoggerType, LoggerMethod> {
|
|
(name?: string): Logger
|
|
}
|
|
|
|
/**
|
|
* Built-in logging service.
|
|
*
|
|
* Call `ctx.logger()` to create a named logger, or call `ctx.logger.info()`
|
|
* directly to log with the current fiber-derived name.
|
|
*/
|
|
export class LoggerService {
|
|
bufferSize = 1000
|
|
buffer: Message[] = []
|
|
ctx!: Context
|
|
|
|
_snMessage = 0
|
|
_snExporter = 0
|
|
exporters = new Map<number, Exporter>()
|
|
|
|
constructor(ctx: Context) {
|
|
const tracker: Tracker = {
|
|
property: 'ctx',
|
|
noShadow: true,
|
|
}
|
|
const self = createCallable('logger', joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker) as unknown as LoggerService
|
|
Object.assign(self, this)
|
|
self.ctx = ctx
|
|
defineProperty(self, symbols.tracker, tracker)
|
|
|
|
self.exporter({
|
|
colors: 3,
|
|
export: (message) => {
|
|
self.buffer.push(message)
|
|
if (self.buffer.length > self.bufferSize) {
|
|
self.buffer = self.buffer.slice(-self.bufferSize)
|
|
}
|
|
},
|
|
})
|
|
|
|
return self
|
|
}
|
|
|
|
/** Register an exporter and dispose it with the current fiber. */
|
|
exporter(exporter: Exporter) {
|
|
return this.ctx.effect(() => {
|
|
this.exporters.set(++this._snExporter, exporter)
|
|
return () => this.exporters.delete(this._snExporter)
|
|
}, 'ctx.logger.exporter()')
|
|
}
|
|
|
|
private _resolveConfig(): LoggerService.Intercept {
|
|
let intercept = this.ctx[symbols.intercept]
|
|
const configs: LoggerService.Intercept[] = []
|
|
while ('logger' in intercept) {
|
|
if (Object.hasOwn(intercept, 'logger')) {
|
|
configs.unshift(intercept['logger'])
|
|
}
|
|
intercept = Object.getPrototypeOf(intercept)
|
|
}
|
|
return Object.assign({}, ...configs)
|
|
}
|
|
|
|
[symbols.invoke](name?: string): Logger {
|
|
const config = this._resolveConfig()
|
|
const fiber = ((this.ctx as any)[symbols.shadow] ?? this.ctx).fiber
|
|
name ??= config.name
|
|
name ??= hyphenate(fiber.name)
|
|
return new Logger({
|
|
name,
|
|
level: config.level,
|
|
meta: { fiber: new WeakRef(fiber) },
|
|
}, this)
|
|
}
|
|
|
|
static {
|
|
for (const type of ['error', 'info', 'warn', 'debug'] as const) {
|
|
;(LoggerService.prototype as any)[type] = function (this: LoggerService, ...args: any[]) {
|
|
return (this as any)()[type](...args)
|
|
}
|
|
}
|
|
}
|
|
}
|