Ideas for a TypeScript structured logger, focused on the structured bit and not so much on all the other stuff like transports and formatting and whatnot. Has the tools you need to log JSON messages about things happening in parallel bound to specific context, such as request and account IDs.
logger.set({
fields
});
logger.log('Message goes here with {id} values interpolated', {
id: 123
});
logger.log({
fields
});
logger.log(); // if everything has already been bound, log a message
logger.message('').log();
logger.prefix(''); // like message but bind a message prefix, so .message() will be appended
// TODO how to allow both mutating a logger instance you already have, and creating child instances?
.set() mutates
.add() adds new fields; throws if any are already set. Use to avoid accidental overwrites
.child() creates a sub-logger
.child({jobId}) to create a sub-logger bound to a job ID so you can log a bunch of stuff happening in parallel
.child('jobUpdate', {jobId}) to create a sub-logger where fields go onto a sub-context object
.child().prefix('Updating {jobId}: ', {jobId})
logger.declare<>(); // Declare fields in the type system to be bound later, without any runtime behavior.
// Can be useful to create suggested naming conventions at the root of a project, which will tab-complete when using the logger elsewhere
logger.settings(); // setup stuff like auto stack trace capturing
// At the root of your project, you can create a single "root" logger like this:
const rootLogger = createBlankLogger().settings({
captureSourcePosition: true
}).set({
app: 'app-name',
env: 'prod'
}).declare<{
requestId: string;
accountId?: string;
userId?: string;
}>();
// Within a request handler, you can create a sub-logger:
const logger = rootLogger.child({
requestId: request.id
});
// .log(), .info(), .error() imply setting a severity field and writing to stdout
// If you want the JSON object without sending it anywhere
logger.build()
logger.buildInfo()
logger.buildError()
// enrich() or postprocess() accept callbacks which augment the structured log message before logging
// Can be used to set timestamps, capture stack traces
// Can return false; filter out log messages. Could be useful for tracing?
logger.enrich(log => {
log.timestamp = +new Date,
});
// Encapsulate hard-coded fields alongside a message, then reuse it in logging.
const message = createMessage({code: 'JOB_RELATED_MESSAGE'}, 'message about {job_id}');
logger.log(message, {job_id: 123});
tracing: how to group trace events, how to determine if tracing is enabled for a given log message / trace?
Every child logger is a tracing span() Every logged message is an "event" within the span
can tracing be as simple as calling .trace()
on a logger; .trace()
calls are filtered / grouped based on set logger values: userId
, accountId
, requestId
, etc.
wrap fields in context object? Or use flat JSON object? How does Sumo / DD do it?
Worth it to allow pairing a message with field types? something like
const message = logMessage<{duration: number}>(`Took too long, took {duration} seconds`);
logger.message(message, {
duration: // known type: number
});
Define MVP: enable logging of today, ready for tracing of tomorrow
Add typedocs for semantic conventions https://opentracing.io/specification/conventions/
Linking to other libs I'm looking at: https://www.npmjs.com/package/tslog https://github.com/gajus/roarr