File System Adapter
The File System adapter writes your wide events to local NDJSON files (one JSON object per line, one file per day). This enables:
- AI agent integration - point a skill to
.evlog/logs/to parse structured logs for debugging and pattern analysis - Local dev debugging - persistent log history without scrolling the terminal (
tail -f .evlog/logs/2026-03-14.jsonl) - Production backup - combine with a network drain (Axiom, OTLP) for local fallback
Add the file system drain adapter
Installation
The File System adapter comes bundled with evlog:
import { createFsDrain } from 'evlog/fs'
Quick Start
No credentials or environment variables needed. Just wire the drain to your framework:
// server/plugins/evlog-drain.ts
import { createFsDrain } from 'evlog/fs'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createFsDrain())
})
// lib/evlog.ts — Node.js routes only; keep evlog/fs out of root instrumentation.ts
import { createEvlog } from 'evlog/next'
import { createFsDrain } from 'evlog/fs'
export const { withEvlog, useLogger, log, createError } = createEvlog({
service: 'my-app',
drain: createFsDrain(),
})
node:fs). On the Edge runtime, or when its directory is not writable, it logs a one-time [evlog/fs] warning and skips writes, so attaching it on a serverless host is safe but pointless, since only the temp directory is writable there and it does not outlive the instance. Use evlog/memory or an HTTP adapter for those.import { createFsDrain } from 'evlog/fs'
app.use(evlog({ drain: createFsDrain() }))
import { createFsDrain } from 'evlog/fs'
app.use(evlog({ drain: createFsDrain() }))
import { createFsDrain } from 'evlog/fs'
await app.register(evlog, { drain: createFsDrain() })
import { createFsDrain } from 'evlog/fs'
app.use(evlog({ drain: createFsDrain() }))
import { createFsDrain } from 'evlog/fs'
EvlogModule.forRoot({ drain: createFsDrain() })
import { createFsDrain } from 'evlog/fs'
initLogger({ drain: createFsDrain() })
Logs start appearing in .evlog/logs/ immediately.
Where the files land
.evlog/
logs/
2026-03-14.jsonl ← one file per day
2026-03-13.jsonl
2026-03-12.jsonl
Each .jsonl file contains one JSON object per line (NDJSON format), making it easy to parse, grep, and stream.
.gitignore is automatically created on first write, inside the .evlog/ ancestor directory when present or in the configured dir otherwise. Log files are never committed to version control.Configuration
Options
| Option | Type | Default | Description |
|---|---|---|---|
dir | string | '.evlog/logs' | Directory for log files |
maxFiles | number | undefined | Max files to keep (auto-deletes oldest) |
maxSizePerFile | number | undefined | Max bytes per file before rotating |
pretty | boolean | false | Pretty-print JSON (multi-line, readable) |
Examples
// Keep only the last 7 days of logs
createFsDrain({ maxFiles: 7 })
// Rotate files at 10MB, keep 30 files
createFsDrain({
maxSizePerFile: 10 * 1024 * 1024,
maxFiles: 30,
})
// Pretty-print for human reading
createFsDrain({ pretty: true })
// Custom directory
createFsDrain({ dir: '/var/log/myapp' })
Rotate by size or by day
By default, a new file is created each day (2026-03-14.jsonl). When maxSizePerFile is set, the adapter creates suffixed files when the current file exceeds the limit:
.evlog/logs/
2026-03-14.jsonl ← base file (full)
2026-03-14.1.jsonl ← first rotation
2026-03-14.2.jsonl ← second rotation
Delete old files on a schedule
When maxFiles is set, the adapter automatically deletes the oldest .jsonl files after each write, keeping only the most recent files.
Write to disk and ship to a service
Use the FS adapter alongside a network drain for local backup:
import { createFsDrain } from 'evlog/fs'
import { createAxiomDrain } from 'evlog/axiom'
const fs = createFsDrain({ maxFiles: 7 })
const axiom = createAxiomDrain()
const drain = async (ctx) => {
await Promise.allSettled([fs(ctx), axiom(ctx)])
}
Read back what you wrote
Stream in real-time
tail -f .evlog/logs/2026-03-14.jsonl
Search with jq
# Find errors
cat .evlog/logs/2026-03-14.jsonl | jq 'select(.level == "error")'
# Slow requests (over 1s)
cat .evlog/logs/2026-03-14.jsonl | jq 'select(.durationMs > 1000)'
# Requests by path
cat .evlog/logs/2026-03-14.jsonl | jq 'select(.path == "/api/checkout")'
Search with grep
# Find all errors
grep '"level":"error"' .evlog/logs/2026-03-14.jsonl
# Find by request ID
grep 'req_abc123' .evlog/logs/*.jsonl
Call the drain without a framework
For advanced use cases, use the lower-level write functions:
import { writeToFs, writeBatchToFs } from 'evlog/fs'
await writeToFs(event, {
dir: '.evlog/logs',
pretty: false,
})
await writeBatchToFs(events, {
dir: '.evlog/logs',
pretty: false,
})
Hand the file to an agent
The file system drain pairs with the analyze-logs agent skill. When installed, your AI assistant can read the NDJSON logs directly to debug errors, trace requests, and investigate performance without any external tools.
Next Steps
- Agent Skills - Let AI analyze your logs
- Axiom Adapter - Send logs to Axiom for querying and dashboards
- Pipeline - Add batching and retry to any drain
- Custom Adapters - Build your own adapter