---
title: "FS reader"
description: "Replay and tail the local NDJSON drain with readFsLogs and tailFsLogs — works in-process or from any external Node tool, survives restarts."
canonical_url: "https://www.evlog.dev/extend/fs-reader"
---
# FS reader

> Replay and tail the local NDJSON drain with readFsLogs and tailFsLogs — works in-process or from any external Node tool, survives restarts.

The [filesystem drain](https://www.evlog.dev/integrate/adapters/self-hosted/fs) writes wide events as NDJSON to `.jsonl` files under `.evlog/logs/` (one file per day, e.g. `2026-05-08.jsonl`, plus rotation suffixes like `.1.jsonl` when size-based rotation is enabled). The `evlog/fs` module also ships **readers** that let any Node tool replay or follow that history without hooking into the running app.

<ndjson-tail>



</ndjson-tail>

<prompt :actions="["copy","cursor","claude"]" description="Read or tail evlog NDJSON logs from disk" icon="i-lucide-folder-search">

Build a script that consumes evlog's local NDJSON history (no app hook required).

- Confirm the filesystem drain is wired up (`evlog/fs` adapter writing NDJSON to `.evlog/logs/*.jsonl`)
- For replay: import `readFsLogs` from `evlog/fs` and iterate `for await (const event of readFsLogs({ since, until, level, filter }))`
- For follow mode: import `tailFsLogs` and iterate the same way. It watches for new lines, handles rotation, and accepts an `AbortSignal`
- Apply filters at read time (`level`, `since`, `until`, custom `filter` predicate) instead of post-processing
- Treat malformed lines as silently skipped (partial writes happen), and never crash the script on a bad line

Docs: [https://www.evlog.dev/extend/fs-reader](https://www.evlog.dev/extend/fs-reader)

</prompt>

## Replay history

```ts
import { readFsLogs } from 'evlog/fs'

for await (const event of readFsLogs({ since: '2026-03-01', level: 'error' })) {
  console.log(event.timestamp, event.action ?? event.message)
}
```

`readFsLogs(options)` walks the NDJSON files in chronological order, parses them line by line, and yields events that pass all filters. Files outside the date window are skipped entirely.

### Options

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

<tbody>
  <tr>
    <td>
      <code>
        dir
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Directory to read from. Default: <code>
        .evlog/logs
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        since
      </code>
    </td>
    
    <td>
      <code>
        Date | string
      </code>
    </td>
    
    <td>
      Yield events with <code>
        timestamp >= since
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        until
      </code>
    </td>
    
    <td>
      <code>
        Date | string
      </code>
    </td>
    
    <td>
      Yield events with <code>
        timestamp <= until
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        level
      </code>
    </td>
    
    <td>
      <code>
        LogLevel | LogLevel[]
      </code>
    </td>
    
    <td>
      Filter by event level.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        filter
      </code>
    </td>
    
    <td>
      <code>
        (event) => boolean
      </code>
    </td>
    
    <td>
      Custom predicate.
    </td>
  </tr>
</tbody>
</table>

Malformed lines (partial writes, manual edits) are silently skipped, so your script never crashes on a bad line.

## Live tail

```ts
import { tailFsLogs } from 'evlog/fs'

const ac = new AbortController()
process.on('SIGINT', () => ac.abort())

for await (const event of tailFsLogs({ signal: ac.signal })) {
  console.log('live:', event.action ?? event.message)
}
```

`tailFsLogs(options)` first yields existing events (unless `fromEnd: true`), then keeps yielding new ones as they're appended, including events written into newly created daily files. Partial writes split across polls are recombined transparently.

### Tail-specific options

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

<tbody>
  <tr>
    <td>
      <code>
        pollIntervalMs
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Polling interval. Default: 500ms (minimum 50ms).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fromEnd
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      Skip existing events; only yield future ones. Default: false.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        signal
      </code>
    </td>
    
    <td>
      <code>
        AbortSignal
      </code>
    </td>
    
    <td>
      Stop tailing when aborted.
    </td>
  </tr>
</tbody>
</table>

All [`readFsLogs`](#options) options also apply.

## Use cases

- A local Electron / Tauri dashboard reading `.evlog/logs/` from a target project directory
- A CI report aggregator that scans logs after a test run
- A `grep`-style CLI that pipes filtered events into `jq`
- Replaying historic events into a dashboard before switching to a live in-process subscription. See the [replay-then-live recipe](https://www.evlog.dev/extend/consumer-recipes#3-replay-history-then-go-live)


## Sitemap

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