---
title: "File System Adapter"
description: "Write wide events to the local file system as NDJSON for local debugging, AI agent integration, and production backup."
canonical_url: "https://www.evlog.dev/integrate/adapters/self-hosted/fs"
---
# File System Adapter

> Write wide events to the local file system as NDJSON for local debugging, AI agent integration, and production backup.

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

<prompt :actions="["copy","cursor","claude"]" description="Add the file system drain adapter" icon="i-lucide-hard-drive">

Add the file system drain adapter to write evlog wide events locally as NDJSON files.

1. Identify which framework I'm using and follow its evlog integration pattern
2. Install evlog if not already installed
3. Import createFsDrain from 'evlog/fs'
4. Wire createFsDrain() into my framework's drain configuration
5. Logs are written to .evlog/logs/ by default (one file per day, auto .gitignore)
6. Optionally configure dir, maxFiles, maxSizePerFile, or pretty options
7. Test by triggering a request and checking .evlog/logs/*.jsonl

Adapter docs: [https://www.evlog.dev/integrate/adapters/self-hosted/fs](https://www.evlog.dev/integrate/adapters/self-hosted/fs)
Framework setup: [https://www.evlog.dev/integrate/frameworks/overview](https://www.evlog.dev/integrate/frameworks/overview)

</prompt>

## Installation

The File System adapter comes bundled with evlog:

```typescript [src/index.ts]
import { createFsDrain } from 'evlog/fs'
```

## Quick Start

No credentials or environment variables needed. Just wire the drain to your framework:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-drain.ts
import { createFsDrain } from 'evlog/fs'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createFsDrain())
})
```

```typescript [Nitro]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createFsDrain } from 'evlog/fs'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createFsDrain())
})
```

```typescript [Next.js]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(),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createFsDrain } from 'evlog/fs'

export const { handle, handleError } = createEvlogHooks({ drain: createFsDrain() })
```

```typescript [TanStack Start]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createFsDrain } from 'evlog/fs'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createFsDrain())
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createFsDrain } from 'evlog/fs'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ drain: createFsDrain() }),
]
```

```typescript [Hono]src/index.ts
import { createFsDrain } from 'evlog/fs'

app.use(evlog({ drain: createFsDrain() }))
```

```typescript [Express]src/index.ts
import { createFsDrain } from 'evlog/fs'

app.use(evlog({ drain: createFsDrain() }))
```

```typescript [Fastify]src/index.ts
import { createFsDrain } from 'evlog/fs'

await app.register(evlog, { drain: createFsDrain() })
```

```typescript [Elysia]src/index.ts
import { createFsDrain } from 'evlog/fs'

app.use(evlog({ drain: createFsDrain() }))
```

```typescript [NestJS]src/app.module.ts
import { createFsDrain } from 'evlog/fs'

EvlogModule.forRoot({ drain: createFsDrain() })
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createFsDrain } from 'evlog/fs'

const handler = withEvlog(new RPCHandler(router), { drain: createFsDrain() })
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createFsDrain } from 'evlog/fs'

export default withEvlog(handler, { drain: createFsDrain() })
```

```typescript [Standalone]
import { createFsDrain } from 'evlog/fs'

initLogger({ drain: createFsDrain() })
```

</framework-tabs>

<callout color="info" icon="i-lucide-info">

The FS adapter requires Node.js (`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.

</callout>

Logs start appearing in `.evlog/logs/` immediately.

## Where the files land

```text [.evlog/logs directory layout]
.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.

<callout color="success" icon="i-lucide-git-branch">

A `.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.

</callout>

## Configuration

### Options

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        dir
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        '.evlog/logs'
      </code>
    </td>
    
    <td>
      Directory for log files
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        maxFiles
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        undefined
      </code>
    </td>
    
    <td>
      Max files to keep (auto-deletes oldest)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        maxSizePerFile
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        undefined
      </code>
    </td>
    
    <td>
      Max bytes per file before rotating
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        pretty
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Pretty-print JSON (multi-line, readable)
    </td>
  </tr>
</tbody>
</table>

### Examples

```typescript
// 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:

```text [Rotated log files]
.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:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog.ts
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)])
}

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', drain)
})
```

```typescript [Nitro]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
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)])
}

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', drain)
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
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)])
}
export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: drain,
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
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)])
}
export const { handle, handleError } = createEvlogHooks({ drain: drain })
```

```typescript [TanStack Start]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
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)])
}

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', drain)
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
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)])
}
export const middleware: Route.MiddlewareFunction[] = [
  evlog({ drain: drain }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
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)])
}
app.use(evlog({ drain: drain }))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
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)])
}
app.use(evlog({ drain: drain }))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
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)])
}
await app.register(evlog, { drain: drain })
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
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)])
}
app.use(evlog({ drain: drain }))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
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)])
}
EvlogModule.forRoot({ drain: drain })
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
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)])
}
const handler = withEvlog(new RPCHandler(router), { drain: drain })
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
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)])
}
export default withEvlog(handler, { drain: drain })
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'
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)])
}
initLogger({ drain: drain })
```

</framework-tabs>

## Read back what you wrote

### Stream in real-time

```bash [Terminal]
tail -f .evlog/logs/2026-03-14.jsonl
```

### Search with jq

```bash [Terminal]
# 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

```bash [Terminal]
# 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:

```typescript [src/index.ts]
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](https://www.evlog.dev/reference/agent-skills). 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](https://www.evlog.dev/reference/agent-skills) - Let AI analyze your logs
- [Axiom Adapter](https://www.evlog.dev/integrate/adapters/cloud/axiom) - Send logs to Axiom for querying and dashboards
- [Pipeline](https://www.evlog.dev/extend/drain-pipeline) - Add batching and retry to any drain
- [Custom Adapters](https://www.evlog.dev/extend/custom-drains) - Build your own adapter

---

- [NDJSON Format](https://github.com/ndjson/ndjson-spec)
- [Axiom Adapter](https://www.evlog.dev/integrate/adapters/cloud/axiom)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
