- Add documentation for recently released changes - Update some exports to include enums
@mifi/logger
Intentional, namespaced logging for TypeScript browser and Node.js applications.
import { createLogger } from '@mifi/logger';
const logger = createLogger({ environment: 'production', namespace: 'WIDGET' });
logger.child('Button').debug('Rendered', () => ({ expensive: 'only evaluated when shown' }));
logger.debug('response', async () => ({ data: await fetchBody() })); // settled before sinks
Install
pnpm add @mifi/logger
Requires Node.js >=24. Published to the private @mifi registry.
Quick start
import { createLogger } from '@mifi/logger';
const logger = createLogger({
environment: 'development', // "development" | "staging" | "production"
namespace: 'API', // optional; string or string[] joined with ":"
});
logger.trace('very detailed');
logger.debug('diagnostic', () => ({ snapshot: heavyWork() }));
logger.debug('response', async () => ({ data: await res.json(), url: res.url }));
logger.info('ready');
logger.warn('slow response', { ms: 1200 });
logger.error('request failed', error);
logger.assert(userId, 'missing user id');
const auth = logger.child('auth'); // namespace → "API:auth"
auth.info('token refreshed');
Console utility methods (group, groupCollapsed, table, time, count, dir, …) are available and gated by the same level policy as info.
Runtime policy
Default levels by environment:
| Environment | Default level | Console shows |
|---|---|---|
development |
trace |
all levels |
staging |
warn |
warn, error |
production |
error |
error only |
Levels in order (most → least verbose): trace → debug → info → warn → error → silent.
Override precedence
Highest wins:
- Explicit
leveloption oncreateLogger - Browser
sessionStorage.showLoggingFor - Env vars
MIFI_LOG_LEVEL/MIFI_LOG_NAMESPACES - Environment default (table above)
Browser session override
sessionStorage.setItem('showLoggingFor', 'debug');
// or restrict to namespaces (exact or descendants):
sessionStorage.setItem('showLoggingFor', 'debug:WIDGET,API');
WIDGET matches WIDGET and WIDGET:Button.
Container / Node env overrides
See Environment variables and env.example.
MIFI_LOG_LEVEL=debug
MIFI_LOG_NAMESPACES=WIDGET,API
Environment variables
These are read from process.env (or from options.env when testing/injecting):
| Variable | Description |
|---|---|
MIFI_LOG_LEVEL |
Override level: trace, debug, info, warn, error, or silent. |
MIFI_LOG_NAMESPACES |
Optional comma-separated namespace filters applied with the level override. |
When only MIFI_LOG_LEVEL is set, all namespaces are enabled at that level. When MIFI_LOG_NAMESPACES is also set, only matching namespaces (and their children) emit at that level.
Copy env.example as a starting point for local or container config.
Sentry
The Sentry adapter has no SDK dependency. Pass your official @sentry/node, @sentry/browser, or @sentry/nextjs module directly to createSentrySink — no consumer adapter or cast is required.
Development never sends to Sentry. Staging and production attach the sink when sentry is configured.
Destination policy when sentry is configured and sinks is not:
| Runtime + environment | Console | Sentry |
|---|---|---|
| Production browser | Off | Issues for errors; optional Logs |
| Production Node | On (stderr for errors) | Issues for errors; optional Logs |
| Staging | On (default warn+) |
Issues for errors; optional Logs |
| Development | On | Nothing |
Issues vs Logs
- Errors → Sentry Issues (
captureException/captureMessage). Not also written to Logs. - Warnings and lower → Sentry Logs (
sentry.logger.*) only when{ logs: true }on the sink (or forced per call).
Default Sentry Logs thresholds when logs is enabled:
| Environment | Logs threshold |
|---|---|
| Staging | info |
| Production | warn |
Override with logLevel on createSentrySink. Console policy stays independent — production browsers can stay quiet while warnings still appear in Sentry Logs.
Per-call options
trace / debug / log / info / warn / error (and assert) accept a trailing options object. Console utilities (group, time, …) do not.
import * as Sentry from '@sentry/nextjs';
import { createLogger } from '@mifi/logger';
import { createSentrySink } from '@mifi/logger/sentry';
const logger = createLogger({
environment: 'production',
namespace: 'API',
sentry: createSentrySink(Sentry, { logs: true }),
});
logger.error('Request failed', error, { requestId: 'req_1' });
logger.warn('slow', { ms: 1200 }); // → Sentry Logs in production
logger.info('investigating', { orderId }, { sentry: true }); // force Logs
logger.error('expected', err, { suppressSentry: true }); // console only (when enabled)
| Option | Effect |
|---|---|
sentry: true |
Force this event to Sentry Logs (ignores Logs threshold; not for errors) |
suppressSentry: true |
Skip creating a Sentry Issue for an error |
A last argument is treated as options only when every key is sentry or suppressSentry. Prefer the third-argument form when you also pass data.
Logs payload: string message + flat primitive attributes (including logger.namespace). Nested objects are not unfurled. When emit was deferred for thenables, the payload also includes async: true.
Issue severities use Sentry’s union (warning, not logger warn). The sink maps warn → warning and trace → debug for Issues only; Logs keep logger level names (sentry.logger.warn).
Custom sinks
Replace the default destinations entirely with sinks:
import { createLogger, createConsoleSink, type LogSink } from '@mifi/logger';
const analyticsSink: LogSink = {
emit(event) {
if (event.sendToConsole) analytics.track('log', event);
// event.async === true when args were settled asynchronously
},
};
const logger = createLogger({
environment: 'production',
sinks: [createConsoleSink(), analyticsSink],
});
When sinks is provided, the sentry option is not used for default sink selection.
API overview
| Export | From | Purpose |
|---|---|---|
createLogger |
@mifi/logger |
Build a namespaced, Console-shaped logger |
createConsoleSink |
@mifi/logger |
Default styled console destination |
parseLoggingOverride |
@mifi/logger |
Parse debug / debug:NS1,NS2 override strings |
createSentrySink |
@mifi/logger/sentry |
Adapter for any Sentry-like SDK |
Types: Logger, LoggerOptions, LogLevel, LoggerEnvironment, LogEvent, LogSink, LogData, LogCallOptions, and (from /sentry) SentryLike, SentryScopeLike, SentrySeverityLevel, SentrySink, SentrySinkOptions.
Optional destinations live under src/sinks/ (console, Sentry today; New Relic / Datadog-style adapters can land beside them later). Public import paths stay @mifi/logger and @mifi/logger/sentry.
All public APIs include JSDoc with parameter and example documentation in the TypeScript sources.
Lazy arguments
Any function passed after the first argument is invoked only if the event will actually be emitted (console and/or Sentry). That keeps expensive snapshots cheap when the level is suppressed.
logger.debug('state', () => buildHugeSnapshot()); // skipped when debug is suppressed
logger.error('failed', error, () => ({ body: pendingBody }));
Async factories and Promises
Factories may be async, or you may pass a top-level Promise as an argument. Thenables are settled before sinks run; the log method stays fire-and-forget (void).
logger.debug('response', async () => ({
data: await res.json(),
url: res.url,
}));
logger.error('failed', { id }, responsePromise);
| Behavior | Detail |
|---|---|
| Unwrap scope | Shallow — only top-level thenables in the argument list |
| Nested promises | Not walked; compose with await / Promise.all inside the factory |
| Call return type | Still void (no await logger.debug(...)) |
| Timestamp | Captured at call time (before settle), so async logs correlate with the failure moment |
LogEvent.async |
true when emit was deferred; omitted for fully sync emits |
| Console | Label gains an [async] suffix, e.g. [API][async] |
| Sentry Logs | Attribute async: true when set |
| Ordering | Async emits may appear after later sync logs |
| Rejections | Rejection reason replaces that argument slot; other args still emit (Promise.allSettled) |
| Console utilities | group / time / etc. do not settle thenables (emit path only) |
// Nested promises — compose inside the factory:
logger.debug('route', async () => {
const [body, next] = await Promise.all([res.json(), nextRoute]);
return { body, next };
});
Releases
Woodpecker verifies pull requests and main, reports CI to Mattermost, then automatically releases from Conventional Commits merged to main. It updates package.json and CHANGELOG.md, creates a vX.Y.Z tag, and publishes to the private @mifi registry.
Use fix: for a patch, feat: for a minor release, and feat! / fix! (or a BREAKING CHANGE: footer) for a major release. Commits such as docs:, test:, and chore: do not release a package.
The release workflow uses the existing global gitea_package_token, gitea_registry_username, gitea_release_token, mattermost_bot_access_token, mattermost_tests_channel_id, mattermost_pushes_channel_id, and mattermost_post_api_url secrets.