# Write useful, safe logs

> Record what happened with enough context to investigate it, without copying secrets into your logs.

- Package: `@db3.ai/app/logging`
- Canonical page: [https://db3.ai/docs/logging](https://db3.ai/docs/logging)
- Markdown: [https://db3.ai/docs/logging.md](https://db3.ai/docs/logging.md)
- Framework source of truth: `packages/app/src/logging/README.md`

<a id="setup"></a>

## Capture a note workflow

Complete Installation and copy this lab. It uses the real Pino driver and a writable stream, with devtools delivery explicitly disabled. No running log server is required.

Application code normally uses `app().log`. The example owns its logger so capture, flush and shutdown are testable in isolation.

- [Installation](https://db3.ai/docs/installation.md)

### Copy the shipped example

```bash
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/logging/examples/. examples/
```

<a id="run"></a>

## Run and inspect three records

Expect `Note saved` at level 30, `Summary failed` at level 50 and `Diagnostics enabled` at level 20. The initial debug record is filtered. Password, token and the custom provider key are absent.

The output removes timestamps, process metadata and error stacks for readability. Real production records retain that diagnostic metadata. Only synthetic test secrets are used.

### Run the lab

```bash
npx tsx examples/runNoteLogs.ts
```

<a id="records"></a>

## Use fields and a short message

Pass a structured object first and a short message second: `log.info({ noteId }, "Note saved")`. Use a child logger for stable request/job/owner context shared by several records.

Pass an Error under `err` to retain its type, message, stack and cause. A failure log does not repair the operation or change its HTTP response; the application still owns recovery.

### examples/runNoteLogs.ts

```typescript
import { Writable } from 'node:stream';
import { pathToFileURL } from 'node:url';
import { Log, PinoLoggerDriver } from '@db3.ai/app/logging';

/**
 * Captures real structured logs and demonstrates child context and redaction.
 *
 * @returns Parsed records without unstable process/time fields for readable output.
 */
export async function runNoteLogs() {
	const chunks: string[] = [];
	const destination = new Writable({
		/** Captures complete Pino output using a real writable stream. */
		write(chunk, _encoding, callback) { chunks.push(chunk.toString()); callback(); },
	});
	const log = new Log({ driver: new PinoLoggerDriver({ level: 'info', environment: 'test', source: 'notes', console: false, devtools: false, redact: ['integration.apiKey'] }, destination) });
	try {
		const request = log.child({ requestId: 'request-1', ownerId: 'ada' });
		request.debug('Filtered diagnostic');
		request.info({ noteId: 'one', password: 'test-password', integration: { apiKey: 'test-provider-key' } }, 'Note saved');
		request.error({ err: new Error('Summary unavailable'), token: 'test-token' }, 'Summary failed');
		log.level = 'debug';
		log.debug({ component: 'notes' }, 'Diagnostics enabled');
		log.level = 'silent';
		log.error('Filtered after disabling');
		await log.flush();
		return chunks.join('').trim().split('\n').map(line => {
			const { time, pid, hostname, ...record } = JSON.parse(line);
			if (record.err) delete record.err.stack;
			return record;
		});
	} finally {
		await log.close();
		destination.end();
	}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) console.log(JSON.stringify(await runNoteLogs(), null, 2));
```

<a id="levels"></a>

## Control volume

Levels are `trace`, `debug`, `info`, `warn`, `error`, `fatal` and `silent`. Default output starts at info, or silent in tests. Supply an explicit level for deterministic tests.

`log.level` reads/changes the root threshold. Use `enabled: false` to disable creation-time logging and `console: false` to disable stdout. A child shares destinations but is not a separately owned lifecycle resource.

<a id="configuration"></a>

## Configure the app logger

Pass `log: { level: "info", source: "notes-api", devtools: false, bindings: { release }, redact: ["integration.apiKey"] }` in App options. These are `AppOptions.log`, not arbitrary values under `config.log`.

Existing environment names remain `PLATFORM_LOG_LEVEL`, `PLATFORM_LOG_SOURCE`, `PLATFORM_LOG_DEVTOOLS`, `PLATFORM_DEVTOOLS_EVENTS_URL`, `PLATFORM_DEVTOOLS_HOST` and `PLATFORM_DEVTOOLS_API_PORT`. The public package name does not rename environment variables.

The default destination is newline-delimited JSON on stdout. Interactive development can automatically enable devtools; explicitly disable it when a script/test must not make HTTP requests.

- [Logging and devtools options](https://db3.ai/docs/logging-api.md#options)

<a id="redaction"></a>

## Redaction is a backstop

Defaults remove common password, secret, token, authorization and cookie paths. Custom `redact` paths extend those defaults; `redact: false` disables them.

This is path-based removal, not a scanner for arbitrary sensitive text. A key inside a message string, deep unfamiliar field, URL query or Error message may still be logged. Select safe fields, avoid request bodies and test your own paths.

Do not send production credentials or customer data to a development ingestion endpoint. Logging is not an audit ledger or secure secret store.

<a id="delivery"></a>

## Flush and close at shutdown

`flush()` waits for accepted records to reach the driver destination; `close()` flushes and ends an owned worker transport. `App.close()` owns this for the app logger. Avoid `process.exit()` immediately after logging.

Devtools delivery is bounded and best-effort: by default 25 records per batch, 100 ms partial-batch delay and up to 1,000 pending events. Failed batches can be discarded; later delivery pauses. Flush is not proof of durable remote ingestion.

A manually supplied stream belongs to its creator. This example closes Log then ends the stream. Do not close a shared app logger at the end of each request.

<a id="testing"></a>

## Testing

Create a `tests/logging` directory and save the test below as `runNoteLogs.test.ts`. It imports the example you copied into `examples/`. Keep the same folder layout so the relative import resolves.

Run from the application root with the development dependencies from Installation. These are consumer tests, not commands that assume a framework checkout.

### tests/logging/runNoteLogs.test.ts

```typescript
import { expect, it } from 'vitest';
import { runNoteLogs } from '../../examples/runNoteLogs';

it('preserves structured context and errors while filtering levels and removing selected secrets', async () => {
	const records = await runNoteLogs();
	expect(records).toHaveLength(3);
	expect(records[0]).toMatchObject({ source: 'notes', environment: 'test', requestId: 'request-1', ownerId: 'ada', noteId: 'one', msg: 'Note saved', level: 30, integration: {} });
	expect(records[1]).toMatchObject({ msg: 'Summary failed', level: 50, err: { type: 'Error', message: 'Summary unavailable' } });
	expect(records[2]).toMatchObject({ msg: 'Diagnostics enabled', level: 20 });
	expect(JSON.stringify(records)).not.toMatch(/test-password|test-provider-key|test-token|Filtered diagnostic|Filtered after disabling/);
});
```

<a id="run-tests"></a>

## Assert records, not console formatting

The copied test asserts context, levels, error fields and the absence of three synthetic secrets using the real driver. Add a sensitive field used by your own app and prove its path is removed.

The example writes a controlled failure record then enables diagnostics. It does not claim to recover a failed provider request or prove remote devtools delivery.

### Run your copied test and check types

```bash
npx vitest run tests/logging/runNoteLogs.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts
```

<a id="coverage"></a>

## Coverage and limits

Taught/tested: structured records, child context, errors, threshold filtering/change, default/custom redaction, flush and direct-stream close. App shutdown and controlled devtools batching/failure have separate service tests.

Remote ingestion, retention, indexing, alerting and compliance/audit policy are application/deployment responsibilities. Logs must not trigger business behavior.

- [Full Logging API](https://db3.ai/docs/logging-api.md)
- [Application events](https://db3.ai/docs/events.md)

## Behavioural verification
Captures real Pino records, tests child context and level filtering, and checks default/custom secret redaction.
- Behaviour test: `packages/app/src/logging/tests/examples/runNoteLogs.test.ts`
- Repository test command (framework checkout only): `npm run test:service --workspace @platform/app -- logging --maxWorkers=1`
- In an installed application, use the walkthrough commands instead of this repository test.
- Expected outcome: The guide test passes against the real framework components.
- Environment: Node.js 24. Real writable stream and Pino; development HTTP delivery disabled.

## Related documentation
- [Logging API reference](https://db3.ai/docs/logging-api.md): Current emitted signatures and options for @db3.ai/app/logging.
- [App](https://db3.ai/docs/app.md): Create one application at boot. Configure its services, use request-local state and close the resources you own.
- [React to a saved note](https://db3.ai/docs/events.md): Use typed, in-process events for small reactions while keeping delivery and failure behavior visible.
- [Encrypt a secret you need to read later](https://db3.ai/docs/security.md): Keep reversible secrets encrypted with an application-owned key and explicit ownership context.

## Framework-owned source: `packages/app/src/logging/README.md`

This is the exact source document captured by the documentation build. Use it for detailed API and workflow guidance, subject to the public package exports and behavioural evidence identified above.

````markdown
# Logging

`@db3.ai/app/logging` provides the application logger exposed through
`app().log`. Pino is the default driver, while the framework owns service discovery,
configuration, redaction, transport selection, and shutdown.

## Run a structured logging lab

From your independent app after [Installation](https://db3.ai/docs/installation):

```sh
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/logging/examples/. examples/
npx tsx examples/runNoteLogs.ts
```

This captures the real Pino driver's output in a writable stream, with devtools HTTP delivery disabled. Expect three records: `Note saved`, `Summary failed`, and `Diagnostics enabled`. The first debug message is filtered, then changing `log.level` enables debug output. Changing it to silent suppresses later errors. The output omits unstable metadata and stack text only for readability.

The [Logging guide](https://db3.ai/docs/logging#testing) contains the exact consumer test. Save it as `tests/logging/runNoteLogs.test.ts` and run:

```sh
npx vitest run tests/logging/runNoteLogs.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts
```

The test asserts structured context, error serialization, level changes and the absence of synthetic password/token/provider-key values. It uses the real logger, not a framework mock. This does not prove remote log ingestion or retention.

## Writing Logs

Prefer a short message plus structured fields:

```ts
app().log.info({
	websiteId,
	pageCount,
}, 'Website crawl completed');
```

Pass errors under `err` so Pino retains their name, message, stack, and cause:

```ts
app().log.error({
	err: error,
	websiteId,
}, 'Website crawl failed');
```

Use child loggers for context shared by several records:

```ts
const log = app().log.child({
	component: 'crawler',
	websiteId,
});

log.info('Crawl started');
log.debug({ url }, 'Page discovered');
```

The standard levels are `trace`, `debug`, `info`, `warn`, `error`, `fatal`,
and `silent`. The default level is `info`; tests default to `silent`.

## Application Configuration

Configure logging through `AppOptions.log`:

```ts
const app = new App({
	log: {
		level: 'debug',
		source: 'example-api',
		bindings: {
			release: process.env.RELEASE_SHA,
		},
		devtools: true,
	},
});
```

Supported environment values:

- `PLATFORM_LOG_LEVEL`: minimum standard log level.
- `PLATFORM_LOG_SOURCE`: source name copied to every record.
- `PLATFORM_LOG_DEVTOOLS`: explicitly enables or disables devtools delivery.
- `PLATFORM_DEVTOOLS_EVENTS_URL`: complete development ingestion endpoint.
- `PLATFORM_DEVTOOLS_HOST`: development service host when no URL is supplied.
- `PLATFORM_DEVTOOLS_API_PORT`: development service port, defaulting to `9998`.

Newline-delimited JSON is written to standard output by default. Interactive
development also enables the framework devtools transport automatically. Tests
do not enable network delivery unless explicitly configured.

## Development Log Stream

The Pino transport runs in a worker thread and sends bounded, best-effort HTTP
batches to the same generic event endpoint used by database and queue
instrumentation:

```text
app().log
	-> Pino worker transport
	-> POST /api/events
	-> development event store
	-> WebSocket
	-> Logs panel
```

The default batch contains at most 25 records and a partial batch waits at most
100 milliseconds. At most 1,000 unsent events are retained; the oldest event is
discarded when that bound is exceeded. A failed batch is discarded and later
delivery pauses for one second. Logging therefore remains observability rather
than an application dependency.

`App.close()` flushes the logger and closes its worker transport. Application
entrypoints should always use the framework shutdown lifecycle rather than
calling `process.exit()` directly after writing a log.

## Security

The default Pino driver removes common password, secret, token, authorization,
and cookie paths before records reach stdout or devtools. Applications can add
more redaction paths through `AppOptions.log.redact`.

Redaction is a safeguard, not permission to log request bodies, credentials,
payment details, or personal data. Prefer explicit safe fields over logging
large application objects.

Redaction removes configured paths, not arbitrary secret substrings. Error messages, URL query strings and unfamiliar nested properties may still contain sensitive data. Test your application paths and never embed a credential in the log message itself. `redact: false` disables the protection.

`Log.level` changes take effect on subsequent method lookups; avoid capturing a log function and then expecting it to be replaced when the level changes. `App.close()` owns its logger; a manually supplied output stream remains the creator's resource to end. The [full API](https://db3.ai/docs/logging-api) includes all current logger, driver, options and HTTP exchange contracts.

## Framework Boundaries

Logs describe operations for people and observability tools. They are not a
framework event bus and must not trigger application behavior.

Database query events, queue lifecycle events, and durable flow events retain
their own typed contracts. They can be correlated with logs through fields such
as `requestId`, `jobId`, `websiteId`, `flowId`, and `component`.
````

## Guidance for AI tools
Use the documented public import `@db3.ai/app/logging` and its exported types. Prefer the source-backed examples and behavioural outcomes above over invented APIs or source-relative internal imports.
