Menu
Public repository / 3c855deb0cc56ae117c336ca1f5ffd8ab54c710e
akurai-log / README.md
Latest change 2577f04e8d3b30f5b729c10273a73b0d54351113 - feat: enforce tenant-safe log contract by Ólafur Búi Ólafsson
# akurai-log
Structured logging that ships to **AkurAI-Monitor** (`POST /api/ingest`),
categorized by host, process (source), module (target), pid, severity (level),
time, message, structured fields, and origin (server/client).
One shared home for three integration paths:
| Path | File | For |
|------|------|-----|
| Rust `tracing` layer | `src/lib.rs` (`akurai-log` crate) | standalone Rust apps (dns, idp, router, vpn-control, monitor) |
| Browser shipper | `js/monitor-client.js` | any frontend (server logs stay server-side via a proxy) |
| Node/Bun shipper + proxy | `js/monitor-server.js` | JS backends (mail, golfsetridak, drive) |
The Monitor `logs` contract carries `host_id, application_id, source, line,
level, target, pid, origin, fields (JSON), ts`, plus nullable top-level
`organization_id, workspace_id, installation_id, deployment_id, request_id`.
The correlation fields are opaque IDs and are never accepted inside free-form
`fields`, which prevents tenant-scoped query mixing. All fields except
`source` and `line` are optional; legacy payloads remain valid.
The shared privacy policy classifies fields as `correlation`, `metadata`,
`sensitive`, or `prohibited`. Field names are ASCII-only and camelCase is
normalized to the canonical snake_case names before classification. Only
bounded scalar metadata (at most 64 fields) is retained. Bearer variants,
encoded/JSON-embedded secrets, authorization material, emails, and note/content/file
fields are rejected or redacted before stdout, browser proxying, or Monitor
ingestion. Newlines are flattened to prevent log injection. Objects and arrays
are dropped instead of recursively logging unknown content. The Rust and
JavaScript implementations are checked against `fixtures/monitor-contract.json`.
## Rust (standalone, crates.io deps)
`Cargo.toml`:
```toml
akurai-log = { path = "../akurai-log" } # or git = "https://akurai-build.olibuijr.com/git/akurai-log.git"
```
`main.rs` — replace `tracing_subscriber::fmt().init()` with:
```rust
fn main() {
let context = akurai_log::CorrelationContext::new()
.with_organization_id("org_acme-01")
.with_workspace_id("ws_acme-01");
akurai_log::init_with_context("akurai-dns", context);
tracing::info!(request_id = "req-9f8e", component = "startup", "started");
}
```
`init("akurai-dns")` remains compatible for callers without tenant context.
Per-event correlation fields merge with the configured scope; conflicting IDs
are dropped to prevent a single record from mixing tenants.
Behaviour: keeps stdout (so journald still works) **and** ships every event at
`MONITOR_LOG_LEVEL` (default `debug`) to Monitor on a background thread. Shipping
never blocks the app (bounded channel, drop-on-full) and never loops
(`reqwest`/`hyper`/`rustls` targets are excluded).
Service env (set per app, ids from Monitor registration):
```
MONITOR_INGEST_URL=https://monitor.olibuijr.com
MONITOR_INGEST_TOKEN=<ingest token, PassVault>
MONITOR_HOST_ID=1
MONITOR_APP_ID=<application id>
MONITOR_ORGANIZATION_ID=org_acme-01
MONITOR_WORKSPACE_ID=ws_acme-01
MONITOR_INSTALLATION_ID=inst_123
MONITOR_DEPLOYMENT_ID=dep_20260718
MONITOR_REQUEST_ID=req-9f8e
MONITOR_LOG_LEVEL=debug
```
If URL/token are unset, only stdout logging is installed (safe no-op ship).
## Browser
```js
import { initClientLogging } from 'akurai-log/js/monitor-client.js';
initClientLogging({
app: 'akurai-notes',
organizationId: 'org_acme-01',
workspaceId: 'ws_acme-01',
}); // captures console.warn/error + window errors
```
Posts batches to the app's own `POST /api/client-logs` proxy — the ingest token
is NEVER exposed to the browser.
`js/monitor-client.js` and `js/monitor-contract.js` are the canonical sources.
Build a self-contained module for an app's static asset and sync it without a
cross-checkout import:
```sh
bash scripts/sync-monitor-client.sh /path/to/app/monitor-client.js
bash scripts/check-monitor-client-drift.sh /path/to/app/monitor-client.js
```
The sync command also writes a `.version` checksum marker. Consumer builds
should serve the generated module at their existing `/monitor-client.js` path
and keep the endpoint option pointed at their own `/api/client-logs` route.
## JS backend (Node/Bun)
```js
import { createLogger, clientProxy } from 'akurai-log/js/monitor-server.js';
const context = { organizationId: 'org_acme-01', workspaceId: 'ws_acme-01' };
const log = createLogger('akurai-mail-api', { correlation: context });
log.info('server started', { port: 3000, requestId: 'req-9f8e' });
// Mount the browser proxy on your framework's POST /api/client-logs:
const handle = clientProxy('akurai-mail-api', { correlation: context });
// e.g. (SvelteKit): export const POST = async ({ request }) =>
// json(await handle(await request.json()));
```
## Zero-touch alternative (journald)
Every Rust app already logs via `tracing` → stdout → journald. Monitor's
journald follower extracts `PRIORITY`→level, `_PID`→pid, unit→source,
origin=server. Adding a unit to `MONITOR_JOURNAL_UNITS` ships its server logs to
Monitor with **no code change or redeploy** — but without `target`/structured
`fields`. Use `akurai-log` when you want the rich structured data.
## Verify
```
cargo test # Rust layer
node --check js/*.js # JS modules
cargo run --example smoke # ships to a local Monitor (set MONITOR_INGEST_URL/TOKEN)
```