# @mifi/logger Intentional, namespaced logging for TypeScript browser and Node.js applications. ```ts 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 ```bash pnpm add @mifi/logger ``` Requires Node.js `>=24`. Published to the private `@mifi` registry. ## Quick start ```ts 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: 1. Explicit `level` option on `createLogger` 2. Browser `sessionStorage.showLoggingFor` 3. Env vars `MIFI_LOG_LEVEL` / `MIFI_LOG_NAMESPACES` 4. Environment default (table above) ### Browser session override ```js 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](#environment-variables) and [`env.example`](./env.example). ```bash 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`](./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. ```ts 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`: ```ts 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. ```ts 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`). ```ts 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) | ```ts // 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.