Skip to content

Instantly share code, notes, and snippets.

@sirkostya009
Last active April 21, 2025 10:41
Show Gist options
  • Save sirkostya009/3ac2780a8f0d087a0eb83546d568ccdc to your computer and use it in GitHub Desktop.
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…
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()),
};
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