- TypeScript 99.6%
- JavaScript 0.4%
- Serialize SQLite initialization, writes, and shutdown and add configurable journal modes. - Cache validated file paths and suppress repeated diagnostics while preserving automatic retries. - Normalize emitted log levels and warn about unknown appender configuration properties. - Enforce 100% coverage and document v2 migration requirements and performance gains. |
||
|---|---|---|
| .forgejo/workflows | ||
| lib | ||
| test | ||
| .editorconfig | ||
| .gitignore | ||
| .node-version | ||
| CHANGELOG.md | ||
| eslint.config.js | ||
| License | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.build.json | ||
| tsconfig.consumer.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
bit-log: Yet another logging library for TypeScript (and JavaScript)
A lightweight logging library with zero required runtime dependencies for TypeScript and JavaScript, in Node.js and the browser.
- Hierarchical loggers with dot-separated namespaces and level inheritance
- Pluggable appenders: Console, File (rolling) and SQLite included, easy to extend
- Dynamic reconfiguration at runtime, no restart needed
- Call-site capture with optional source map resolution for browser bundles
- Lazy evaluation: pass a function instead of a string to defer expensive computations
- Customizable formatting: override timestamp, log level or the entire prefix per appender
Migrating from v1
bit-log 2 introduces the following requirements and observable behaviour changes for existing applications:
- The runtime baseline changes to Node.js 24. See Requirements for supported Node.js releases.
- The package is distributed as native ESM with TypeScript declarations and explicit package exports.
- Unchanged appender configurations reuse their existing instances. Code must not rely on every
configureLogging()call constructing a fresh appender. - Replaced appenders are closed automatically. Custom
close()implementations must be idempotent and allow the appender to be used again afterwards. - Unknown appender configuration properties produce warnings. Existing misspellings or obsolete properties may therefore become visible during startup or reconfiguration.
- SQLite uses
WALwithNORMALsynchronization by default. This creates-waland-shmfiles next to the database. ConfigureDELETEfor NFS, SMB and other network file systems. - Applications can await
closeLogging()during shutdown to flush queued File- and SQLiteAppender operations.
See the changelog for the complete list of changes and fixes.
Requirements
bit-log 2.x requires Node.js 24 or newer. Projects using an older Node.js release should remain on bit-log 1.x.
Installation
pnpm add @mburchard/bit-log
npm install @mburchard/bit-log
The SQLite and browser source-map integrations use optional peer dependencies that are documented in their respective sections below.
Quick Start
import {useLog} from '@mburchard/bit-log';
const log = useLog('foo.bar');
log.info('Here we are, an info log');
log.warn('Here we are, a warning');
try {
// ...
} catch (error) {
log.error('error in method ...', error);
}
Lazy Evaluation
When a log argument is expensive to compute, pass a function instead. It is only evaluated when the log level is active, and the return value is treated as a single payload element:
log.debug(() => `User ${user.id}: ${JSON.stringify(expensiveData)}`);
Configuration
The logging system can be reconfigured at any time during execution. Repeated calls allow dynamic changes to the configuration.
Logging is configured as follows by default:
import {configureLogging, ConsoleAppender} from '@mburchard/bit-log';
configureLogging({
appender: {
CONSOLE: {
Class: ConsoleAppender,
},
},
root: {
appender: ['CONSOLE'],
level: 'INFO',
},
});
Appender instances are reused when all effective configuration values remain identical according to Object.is.
When reconfiguring logging repeatedly, define custom formatter functions once and reuse the same function reference.
Inline function expressions create a new reference and therefore replace the existing appender.
const formatTimestamp = (timestamp: Date): string => timestamp.toISOString();
configureLogging({
appender: {
CONSOLE: {
Class: ConsoleAppender,
formatTimestamp,
},
},
});
Additional Loggers
You can configure any number of additional hierarchical loggers.
configureLogging({
logger: {
'foo.bar': {
level: 'DEBUG',
},
},
});
After this configuration you have three loggers, all of which can be used as required.
const log = useLog(); // get the root logger
const fooLogger = useLog('foo');
const barLogger = useLog('foo.bar');
All three loggers are using the existing ConsoleAppender, which is registered on the root logger.
However, you do not have to preconfigure the loggers. You can get new hierarchical loggers at any time, which then take over the configuration from existing parents. If nothing else is available, then at the end from the root logger.
You can also change the level when accessing a logger. However, it is not recommended to do this, as this distributes
the configuration across the entire code base. Log levels should be configured centrally, in other words by calling
configureLogging.
It is of course also possible to completely overwrite the default configuration, i.e. to customize the root logger and
register a different appender than the ConsoleAppender.
Additional Appenders
Just like the loggers, you can also configure additional appender. These must then be registered on a logger. You can also register them on several loggers. If you use one of the logging methods of a logger, a LogEvent is created. This is bubbled up the hierarchy until an appender takes care of it. If this has happened, it is not passed up further.
You could add the SQLiteAppender (see below) to the root logger this way:
configureLogging({
appender: {
CONSOLE: {
Class: ConsoleAppender,
},
SQLITE: {
Class: SQLiteAppender,
level: 'WARN',
},
},
root: {
appender: ['CONSOLE', 'SQLITE'],
level: 'INFO',
},
});
Advanced Usage
The Call Site
In some environments one wants to know where the log event was created. Since this is a bit expensive, as the
stack trace must be analysed, it is not activated by default. To enable it, use the property includeCallSite.
configureLogging({
appender: {
CONSOLE: {
Class: ConsoleAppender,
},
},
root: {
appender: ['CONSOLE'],
includeCallSite: true,
level: 'DEBUG',
},
});
As always, it can be set at any time and for any Logger. The choice is yours.
Call Site Offset for Logger Wrappers
When you wrap the logger in a custom class or utility function, the call site points to the wrapper code instead
of the actual caller. Use callSiteOffset to skip additional stack frames:
configureLogging({
root: {
appender: ['CONSOLE'],
callSiteOffset: 1,
includeCallSite: true,
level: 'DEBUG',
},
});
The offset can also be set per logger. It is inherited from parent loggers, just like includeCallSite.
Note: The offset only applies to Tier 2 (captureStackTrace) and Tier 3 (fallback). When the log payload
contains a real Error, the call site always points to where the error was thrown, regardless of the offset.
Each log event then carries a callSite object with file, line and column properties. The resolution
strategy depends on the environment:
- If the log payload contains a real
Error, its stack trace is used (points to where the error was thrown). - In V8 (Chrome, Node.js, Electron) and WebKit/JSC (Safari, Tauri's WebView),
Error.captureStackTraceproduces a clean stack starting after the logger-internal frames. - As a fallback, a synthetic
Erroris created and logger-internal frames are skipped heuristically.
Source Map Resolution 
Wherever code is bundled or compiled (Vite, Webpack, esbuild, etc.), the line and column numbers from stack traces point into the compiled output rather than the original source. This affects all environments: browsers, Electron (both renderer and main process), Tauri (WebView), and Node.js alike.
To get accurate call-site positions in DOM environments (browsers, Electron renderer, Tauri's WebView), install
@jridgewell/trace-mapping
and configure the resolver explicitly:
pnpm add @jridgewell/trace-mapping
import {originalPositionFor, TraceMap} from '@jridgewell/trace-mapping';
import {configureSourceMapResolver} from '@mburchard/bit-log';
configureSourceMapResolver(TraceMap, originalPositionFor);
Call configureSourceMapResolver() before configureLogging() so that the resolver is active from the first log
event.
The explicit setup ensures that bundlers do not pull @jridgewell/trace-mapping into your production bundle
unless you actually use it.
Without this configuration, call sites still work but show compiled positions instead of the original source
locations. In Node.js and Electron main processes, the runtime can resolve source maps natively via the
--enable-source-maps flag, so configureSourceMapResolver() is typically not needed there.
Overwrite Formatting
Bit-Log is designed to be straightforward to use and extremely flexible. It is therefore possible to influence the formatting of the output for each appender.
configureLogging({
appender: {
CONSOLE: {
Class: ConsoleAppender,
formatLogLevel: (level: LogLevel, colored: boolean) => {
return 'whatever you want';
},
formatPrefix: (event: ILogEvent, colored: boolean) => {
return 'whatever you want';
},
formatTimestamp: (date: Date) => {
return 'whatever you want';
}
},
},
root: {
appender: ['CONSOLE'],
level: 'INFO',
},
});
The method names are almost self-explanatory except perhaps formatPrefix. Here is the default implementation
from AbstractBaseAppender showing how the formatting methods interlock:
function formatPrefix(event: ILogEvent, colored: boolean = false): string {
const levelStr = this.formatLogLevel(event.level, colored);
const paddedLevel = colored ? levelStr.padStart(13, ' ') : levelStr.padStart(5, ' ');
const name = truncateOrExtend(event.loggerName, 20);
const timestamp = this.formatTimestamp(event.timestamp);
let callSite = '';
if (event.callSite) {
const line = String(event.callSite.line).padStart(4, ' ');
const path = truncateOrExtendLeft(event.callSite.file, 50);
callSite = ` (${path}:${line})`;
}
return `${timestamp} ${paddedLevel} [${name}]${callSite}:`;
}
Handling Secrets
Sensitive values like API keys, tokens or passwords should never appear in plain text in log output. Since bit-log
converts log arguments to strings via toString(), you can create a simple wrapper class that masks itself
automatically:
class Secret {
constructor(private readonly value: string) {}
toString() {
return '********';
}
toJSON() {
return '********';
}
unwrap() {
return this.value;
}
}
Use it anywhere you would pass a sensitive string:
const apiKey = new Secret(process.env.API_KEY);
log.info('Connecting with key:', apiKey);
// Output: Connecting with key: ********
The masking works across all appenders, including ConsoleAppender, FileAppender and SQLiteAppender, because
they all rely on string conversion internally. Call unwrap() when you need the real value in your application
logic.
bit-log intentionally does not ship a Secret class, because masking requirements vary between projects (mask
length, partial reveal, different types of sensitive data). The pattern above is a starting point you can adapt
to your needs.
Appenders
ConsoleAppender
As the name states, this appender writes to the console.
It has three properties.
colored: boolean
Specifies whether logs should be formatted with colours. By default, this property is set to false.
pretty: boolean
Specifies whether objects to be output should be formatted nicely, i.e. with indents and breaks.
By default, this property is set to false.
useSpecificMethods: boolean
The JavaScript console has specific methods that match the log levels, such as console.info or console.error.
You can use these or console.log.
The specific methods may not appear in the browser console, such as console.debug.
By default, this property is set to false.
FileAppender
This appender, of course, writes to a file and cannot be used in the browser environment.
This implementation is rolling, as the name of the output file is calculated from the timestamp for each log event.
This means that the appender switches to a new file after midnight.
If you do not want this, you can overwrite the getTimestamp method as described above. You can also implement an
hourly rolling output in the same way.
Successful path validation is cached until the effective file configuration changes or a write fails. This roughly
doubled FileAppender throughput in local benchmarks while preserving recovery after filesystem errors.
The FileAppender has the following properties.
baseName: string
Specifies a base name for the output file. By default, this property is set to an empty string.
The baseName can be empty as long as the getTimestamp method does not return an empty string.
You can therefore combine both or use both individually.
combined: MyLog-2024-05-13.log
baseName only: MyLog.log
timestamp only: 2024-05-13.log
colored: boolean
Specifies whether logs should be formatted with colours. By default, this property is set to false.
extension: string
Specifies the file extension. By default, this property is set to log.
filePath: string
Specifies the file path. By default, this property is set to the OS default temp folder plus bit.log.
Attention: For security reasons, the FileAppender does not create directories.
pretty: boolean
Specifies whether objects to be output should be formatted nicely, i.e. with indents and breaks.
By default, this property is set to false.
SQLiteAppender 
This appender stores log events in an SQLite database. It can be used as-is or serve as a template for other database-backed appenders.
It requires better-sqlite3 as an optional peer dependency.
The module is loaded dynamically, so importing the appender will not fail when the dependency is absent. If you
want to use it, install the dependency yourself:
pnpm add better-sqlite3
And of course, this appender cannot be used in the browser either.
The SQLiteAppender has the following properties.
baseName: string
Specifies a base name for the database file. By default, this property is set to logging.
extension: string
Specifies the file extension. By default, this property is set to db.
filePath: string
Specifies the file path. By default, this property is set to the OS default temp folder plus bit.log.
journalMode: 'DELETE' | 'WAL'
Specifies the SQLite journal mode and defaults to WAL for substantially better write throughput. WAL creates temporary
-wal and -shm files next to the database and is not supported on network file systems. Configure DELETE when the
database is stored on NFS, SMB or another network file system.
synchronous: 'NORMAL' | 'FULL' | undefined
Controls when SQLite synchronizes committed data to persistent storage. When omitted, WAL uses NORMAL and DELETE
uses FULL. NORMAL is appropriate for regular application logging, but recently committed events can be lost after
a power failure or hard reset. Use FULL when maximum durability is more important than throughput.
The requested journal mode is verified when the database opens. Initialization fails with a diagnostic if the underlying file system cannot activate it. See the SQLite WAL documentation for further operational details.
Together with prepared statement reuse and serialized writes, the default WAL configuration improved
SQLiteAppender throughput by roughly 12× in local benchmarks.
The appender creates a Logs table with columns id, timestamp, level, loggerName and payload.
Call the close() method to flush queued events and cleanly release the database connection when you no longer need the
appender.
Shutdown
Call closeLogging() during application shutdown to close all registered appenders that expose a close() method:
import {closeLogging} from '@mburchard/bit-log';
await closeLogging();
Each appender instance is closed at most once, even when it is attached to multiple loggers. Closing does not clear the logging configuration, so logging can be used again afterwards. Errors from individual appenders are reported to the console without preventing the remaining appenders from closing. The promise resolves after asynchronous appender clean-up, including queued file writes and SQLite inserts, has completed.
Reporting Issues
Found a bug or have a feature request? Please open an issue on the GitHub issue tracker.