@arki/log for agents

For @arki/[email protected] · source docs/agent-guide.md

The long form of llms.txt. When they disagree, src/core/index.ts wins.

The model

  1. A logger is getLogger('arki:kv'). Its category is :-separated. It obeys a configure() that runs later.
  2. A record has a template, fields, a level and a resource. Values stay fields. Formatters render the text.
  3. A channel is a named sink. A rule maps a category to a level and channels. The nearest configured ancestor wins and replaces.
  4. Processors (redact, enrich, dropWhen) change or drop a record before a sink sees it.
  5. Wrappers (fingersCrossed, buffer, deduplicate) sit around a sink.
  6. The core has no dependencies. @arki/log/node adds sinks and env. @arki/log/otel adds OpenTelemetry. @arki/log/testing adds fakeLog.

Task map

Log from a library

import { getLogger } from '@arki/log';
 
const log = getLogger('arki:kv'); // module scope is fine
 
export function connect(url: string): void {
  log.debug('connecting to {host}', { host: new URL(url).host });
}

Never call configure() in a library.

Configure logging in an app

Call configure() once at the entry file. See the README quick start.

Keep secrets out of records

Add redact(['password', 'headers.authorization']) to processors. Redaction runs before any formatter.

Test that code logs

import { test } from 'vitest';
 
import { getLogger } from '@arki/log';
import { fakeLog } from '@arki/log/testing';
 
test('logs a warning', () => {
  using log = fakeLog();
  getLogger('app:jobs').warn('job {id} is late', { id: 'job_1' });
  log.assertLogged({ level: 'warn', template: 'job {id} is late' });
});

Rules that bite

  • Before configure(), only warn and above are kept, and only the first 64 records.
  • A second configure() throws ARKI_LOG_E001. Use reconfigure() on the handle. App entries pass { owner: import.meta.url }, so a dev server's re-run of the entry replaces the setup instead of throwing.
  • withContext fields cross await only with asyncContext() from @arki/log/node.
  • A failure value logged as a field prints its cause chain, at most 64 links.
  • fatal never ends the process.
  • Importing @arki/log/node reads nothing. fromEnv() reads LOG_LEVEL, DEBUG and NO_COLOR when called.

Errors

CodeMeaningWhat to do
ARKI_LOG_E001configure() ran twiceCall it once; use reconfigure()
ARKI_LOG_E002A rule or to() names an undefined channelAdd it to configure({ channels })
ARKI_LOG_E003A level that does not existUse one of the six names or 1 to 24
ARKI_LOG_E004fakeLog() while another fake is activeRestore the first fake
ARKI_LOG_E005reconfigure() on a stale handleUse the latest handle

Verify

bun run test
bun run test:docs
bun run test:runtimes

All docs for @arki/[email protected]