- `logger.debug('message', async () => ({ asyncReturn: await asyncFn() }))` is now supported
- NOTE: this may result in logging occurring out of band for these calls if other events fire befor they settle
Update CI labels to direct jobs to correct servers
213 lines
9.2 KiB
Markdown
213 lines
9.2 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" }));
|
||
```
|
||
|
||
## 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.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.
|
||
|
||
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);
|
||
},
|
||
};
|
||
|
||
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 is emitted:
|
||
|
||
```ts
|
||
logger.debug("state", () => buildHugeSnapshot()); // skipped when debug is suppressed
|
||
logger.debug("response", async () => ({ data: await res.json(), url }));
|
||
```
|
||
|
||
Top-level thenables (including Promises returned from lazy factories, or a Promise passed directly as an argument) are settled before sinks run. The log call stays fire-and-forget (`void`). Settled events set `LogEvent.async`, and the console sink renders an `[async]` label suffix. Nested promises inside plain objects are not walked — compose them inside the async factory. Async emits may appear out of order relative to sync logs.
|
||
|
||
Rejection reasons replace rejected thenables in that argument slot; other arguments still emit.
|
||
|
||
## 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.
|