dandv/timestamp-logger

Human-readable logger with clean timestamped output

โ˜… 3Forks 0TypeScriptGitHub โ†—Compare
deno-modulejsr-packageloggerlogginglogging-library

README

timestamp-logger โ€” Logger with human-readable, clean timestamped output

JSR

Gitmoji

Configurable logger outputting to the console and optionally to a file, with cleaned error stack traces, and timestamps in the YYYY-MM-DD HH:MM:SS[.mmm] format (RFC3339, or ISO8601).

screenshot

Features

  • prefixes each line with the local time in RFC3339 YYYY-MM-DD HH:MM:SS format (which is ISO8601 with a more readable between the date and the time instead of the T)

    [2025-04-23 17:00:00] It's tea time
    
  • outputs Error objects to file (via serialize-error)

  • cleans up Error stack traces (via clean-stack)

  • makes absolute error paths relative to the home directory

  • uses the native Node console with colorization, plus yellow for WARNs and red for ERRORs

    • the downside is that objects beyond 3 levels deep will be displayed as [Object]. Refer to the same timestamp in the log file to see the full JSON dump.
  • exposes a lazily-created writable .stream for integration with other libraries

  • uses four standard log levels: debug, info, WARN, ERROR.

  • option to prefix messages with the same unique id per Logger instance, to distinguish them in parallel processing contexts

  • you can use the familiar variable-arity console format, with arguments of any type:

    logger.warn('Got', results.length, 'results, but also an error:', results, new Error('oops'));
  • arrays are logged in JSON format, with newlines for readability

    logger.error([error1, error2]);  // smart indented display

Overall, the package aims to format messages logged to a file as close as possible to the console having been redirected to that file (e.g. by adding newlines for readability), while including more information than what was logged to the console (e.g. by fully dumping objects beyond the first 3 levels of nesting).

Install

This is a JSR package.

# Deno (optional if you don't prefix the import with 'jsr:'), current pnpm or yarn
deno add jsr:@dandv/timestamp-logger
pnpm add jsr:@dandv/timestamp-logger
yarn add jsr:@dandv/timestamp-logger

# NPM, bun, and older versions of yarn or pnpm
npx jsr add @dandv/timestamp-logger
bunx jsr add @dandv/timestamp-logger
yarn dlx jsr add @dandv/timestamp-logger
pnpm dlx jsr add @dandv/timestamp-logger

Examples

import { Logger } from '@dandv/timestamp-logger';
const logger = new Logger({ filename: 'file.log' });

// Timestamped log messages in the YYYY-MM-DDTHH:MM:SS format and the local timezone
logger.debug('Greyed out timestamp to de-emphasize');
logger.info('Variable number of arguments, not just', 1);
logger.warn('Yellow for warnings');
logger.error('Error with clean stack trace', new Error('Oops'));
// No need to close โ€” log methods append atomically, no file handle is held open

Passing .stream to another library

If you need a persistent WriteStream (e.g. to hand off to Mongoose or another library), access the .stream getter. This lazily opens a file handle, so you must close the Logger when done โ€” via await using, using, or an explicit .close() call:

import { Logger } from '@dandv/timestamp-logger';
import mongoose from 'mongoose';

await using logger = new Logger({ filename: 'file.log' });
mongoose.set('debug', logger.stream);  // creates a persistent WriteStream
// ... logger.stream is automatically closed at end of scope by `await using`

Or with an explicit .close():

const logger = new Logger({ filename: 'file.log' });
externalLib.setOutput(logger.stream);
// ... when done:
await logger.close();

For more examples, see examples.ts.

Permissions

If you're using Deno, you may need to run Deno with the following access flags:

Closing the logger

Normal log methods (.debug(), .info(), .warn(), .error()) append to the file atomically โ€” no persistent file handle is held open, so the Logger can simply go out of scope without leaking resources.

You only need to close the Logger if you accessed the .stream getter (e.g. to pass it to another library). Three options, in order of preference:

  1. await using โ€” automatically flushes and closes when the variable goes out of scope:
    {
        await using logger = new Logger({ filename: 'app.log' });
        externalLib.setOutput(logger.stream);
        // ...
    }
    // stream is closed here
  2. using โ€” synchronous disposal; the stream may not be fully flushed before you can read the file:
    {
        using logger = new Logger({ filename: 'app.log' });
        externalLib.setOutput(logger.stream);
    }
  3. await logger.close() โ€” explicit async close:
    const logger = new Logger({ filename: 'app.log' });
    externalLib.setOutput(logger.stream);
    // ...
    await logger.close();

A FinalizationRegistry safety net will close a forgotten stream when the Logger is garbage-collected, but you should not rely on it โ€” GC timing is non-deterministic.

Known issues

  1. Logging something right before calling Deno/process.exit() won't flush the output to the file. This is a problem with all loggers (e.g. Winston, Bristol). As a workaround, try delaying the exit:

    setTimeout(() => Deno.exit(1),  1);
  2. Stack traces don't produce proper URLs. This is an issue with clean-stack.

  3. Somewhat ironically, Date objects logged to the console will be output in UTC, while in the log file they're output in the local timezone (i.e. passed through .localISOdt). This is done to preserve console colorization, and may be improved in a future version. In the meantime, you can pass Date objects to .localISOdt if desired.

Author

Dan Dascalescu

License

MIT

Contributors

dandv

Issues