- Add documentation for recently released changes - Update some exports to include enums
246 lines
11 KiB
Markdown
246 lines
11 KiB
Markdown
# @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.
|