Last active
April 21, 2025 10:41
-
-
Save sirkostya009/3ac2780a8f0d087a0eb83546d568ccdc to your computer and use it in GitHub Desktop.
Structured, non-blocking logger for Node.js and Vite. Uses Vite for pretty-printing logs in dev mode, and serializing them as JSON in prod. slog and sdebug functions don't print anything in prod. Follows a similar naming pattern as console, prepended with s (for structural). Vite dependency may be dropped by replacing all `import.meta.env.DEV` w…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { inspect, type InspectOptions } from 'node:util'; | |
const inspectOptions: InspectOptions = { | |
colors: true, | |
depth: Infinity, | |
compact: true, | |
// numericSeparator: true, | |
// showHidden: true, | |
getters: true, | |
}; | |
function createStructure(args: any[], level: string): Record<string, any> { | |
const log: Record<string, any> = { timestamp: new Date(), level }; | |
if (args.length === 1 && typeof args[0] === 'object') { | |
Object.assign(log, args[0]); | |
} else if (args.length === 1) { | |
[log.message] = args; | |
} else if (args.length > 1) { | |
log.message = args; | |
} | |
return log; | |
} | |
function print(args: any[], printer: (message: any) => void) { | |
const log = createStructure(args, printer.name); | |
printer(import.meta.env.DEV ? inspect(log, inspectOptions) : JSON.stringify(log)); | |
} | |
function serror(args: any[]) { | |
const log = createStructure(args, 'error'); | |
if (args.length > 1 && args[0] instanceof Error) { | |
const { name, cause, stack, message } = args[0]; | |
log.name = name; | |
if (cause) log.cause = cause; | |
if (stack) log.stack = stack; | |
log.message.shift(); | |
log.message.unshift(message); | |
} | |
console.error(import.meta.env.DEV ? inspect(log, { ...inspectOptions, compact: !log.stack }) : JSON.stringify(log)); | |
} | |
const timers = new Map<string, bigint>(); | |
function stimeEnd(label: string | { label: string; [k: number | string | symbol]: any }, args: any[], now: bigint) { | |
const lbl = stimeLog(label, args, now); | |
if (lbl) timers.delete(lbl); | |
} | |
function stimeLog( | |
label: string | { label: string; [k: number | string | symbol]: any }, | |
args: any[], | |
now: bigint | |
): string | undefined { | |
let lbl = label as string; | |
if (typeof label === 'object') { | |
lbl = label.label; | |
// @ts-expect-error | |
delete label.label; | |
for (let _ in label) { | |
// add label to args as a message if there's any other key present in it | |
args.unshift(label); | |
break; | |
} | |
} | |
if (!timers.has(lbl)) return undefined; | |
const time = timers.get(lbl)!; | |
const diff = Number(now - time); | |
const s = createStructure(args, 'time'); | |
s.label = lbl; | |
if (diff > 60_000_000_000) s.time = diff / 60_000_000_000 + 'm'; // minutes | |
else if (diff > 1_000_000_000) s.time = diff / 1_000_000_000 + 's'; // seconds | |
else if (diff > 1_000_000) s.time = diff / 1_000_000 + 'ms'; // milliseconds | |
else if (diff > 1_000) s.time = diff / 1_000 + 'µs'; // microseconds | |
else s.time = diff + 'ns'; | |
process.stdout.write(import.meta.env.DEV ? inspect(s, inspectOptions) : JSON.stringify(s)); | |
process.stdout.write('\n'); | |
return lbl; | |
} | |
export default { | |
/** | |
* Structurely prints the message. Can be multiple arguments, if yes then the arguments are put into an array. | |
* | |
* The structure printed looks like the following: | |
* ```js | |
* { timestamp: Date, level: 'log', ...args[0] | message: args } | |
* ``` | |
*/ | |
slog: (...args: any[]) => import.meta.env.DEV && setTimeout(print, 0, args, console.log), | |
/** | |
* Structurely prints the message. Can be multiple arguments, if yes then the arguments are put into an array. | |
* | |
* The structure printed looks like the following: | |
* ```js | |
* { timestamp: Date, level: 'info', ...args[0] | message: args } | |
* ``` | |
*/ | |
sinfo: (...args: any[]) => setTimeout(print, 0, args, console.info), | |
/** | |
* Structurely prints the message. Can be multiple arguments, if yes then the arguments are put into an array. | |
* | |
* The structure printed looks like the following: | |
* ```js | |
* { timestamp: Date, level: 'warn', ...args[0] | message: args } | |
* ``` | |
*/ | |
swarn: (...args: any[]) => setTimeout(print, 0, args, console.warn), | |
/** | |
* Structurely prints the message. Can be multiple arguments, if yes then the arguments are put into an array. | |
* | |
* The structure printed looks like the following: | |
* ```js | |
* { timestamp: Date, level: 'debug', ...args[0] | message: args } | |
* ``` | |
*/ | |
sdebug: (...args: any[]) => import.meta.env.DEV && setTimeout(print, 0, args, console.debug), | |
/** | |
* Structurely prints the message. Can be multiple arguments, if yes then the arguments are put into an array. | |
* | |
* The structure printed looks like the following: | |
* ```js | |
* { timestamp: Date, level: 'error', message: args | Error.message, stack?: Error?.stack, cause: Error?.cause, name: Error?.name } | |
* ``` | |
*/ | |
serror: (...args: any[] | [Error, ...any]) => setTimeout(serror, 0, args), | |
/** | |
* Starts a timer with the given label. If the label is an object, it must have a `label` property. | |
* | |
* Does not print anything, merely sets the recorded time at invokation in a map. Resets the timer if the label already exists. | |
*/ | |
stime(label: string) { | |
timers.set(label, process.hrtime.bigint()); | |
}, | |
/** | |
* Ends a timer with the given label. If the label is an object, it must have a `label` property. | |
* | |
* Prints the time it took to execute the code block. | |
* | |
* ```js | |
* { timestamp: Date, level: 'time', time: diff, label, ...args[0] | message: args } | |
* ``` | |
*/ | |
stimeLog: (label: string | { label: string; [k: number | string | symbol]: any }, ...args: any[]) => | |
setTimeout(stimeLog, 0, label, args, process.hrtime.bigint()), | |
/** | |
* Ends a timer with the given label. If the label is an object, it must have a `label` property. | |
* | |
* Prints the time it took to execute the code block. | |
* | |
* Deletes the timer from the map. | |
* | |
* ```js | |
* { timestamp: Date, level: 'time', time: diff, label, ...args[0] | message: args } | |
* ``` | |
*/ | |
stimeEnd: (label: string | { label: string; [k: number | string | symbol]: any }, ...args: any[]) => | |
setTimeout(stimeEnd, 0, label, args, process.hrtime.bigint()), | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import logger from 'logger.ts'; | |
logger.stime('label'); | |
logger.stimeLog({ | |
label: 'label', // mandatory | |
stage: 1, | |
}); // prints { timestamp: ..., level: 'time', label: 'label', stage: 1, time: '...Xs' } | |
logger.stimeEnd('label'); // prints { timestamp: ..., level: 'time', label: 'label', time: '...Xs' } | |
logger.sinfo('foo'); // prints { timestamp: ..., level: 'info', message: 'foo' } | |
// and so on |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment