Self-Hosted
Store wide events in an in-memory ring buffer. Works in any runtime — including Cloudflare Workers (workerd) — where the file system is unavailable.

The Memory adapter stores wide events in a module-level ring buffer. Unlike the File System adapter, it has zero runtime dependencies and runs anywhere — including Cloudflare Workers (workerd), Deno Deploy, and other edge runtimes that don't expose Node's fs module.

The primary use case is local dev agent access: wire the drain during development, expose a lightweight HTTP endpoint, and let your AI agent fetch structured logs over HTTP without any external tooling.

Add the memory drain adapter

Installation

The Memory adapter comes bundled with evlog:

src/index.ts
import { createMemoryDrain, readMemoryLogs } from 'evlog/memory'

Quick Start

// src/index.ts
import { Hono } from 'hono'
import { evlog } from 'evlog/hono'
import { createMemoryDrain, readMemoryLogs } from 'evlog/memory'

const app = new Hono()

app.use(evlog({ drain: createMemoryDrain() }))

// Dev-only endpoint — restrict or remove in production
app.get('/_evlog/logs', (c) => {
  return c.json(readMemoryLogs())
})

Agent Access via HTTP

Expose a route so agents can retrieve structured logs during a local dev session. Use parseReadMemoryLogsQuery to let agents pass filter params directly as query strings:

src/index.ts (Hono)
import { readMemoryLogs, parseReadMemoryLogsQuery } from 'evlog/memory'

// Restrict to dev — agents hit this endpoint to retrieve logs
if (process.env.NODE_ENV !== 'production') {
  app.get('/_evlog/logs', (c) => {
    return c.json(readMemoryLogs(parseReadMemoryLogsQuery(c.req.query())))
  })
}

An agent can now call /_evlog/logs?level=error&limit=50&since=2026-01-01T00:00:00Z and the query params are coerced to the correct types before being passed to readMemoryLogs. Supported query params: store, since, until, level (comma-separated for multiple), limit.

The response is a JSON array of WideEvent objects — the same shape used by every other evlog adapter.

Configuration

Options

OptionTypeDefaultDescription
maxEventsnumber1000Maximum events to keep in the ring buffer (oldest are dropped)
storestring'default'Named buffer key — multiple drains sharing the same key share the same buffer
server/plugins/evlog-drain.ts
// Keep only the last 500 events
createMemoryDrain({ maxEvents: 500 })

// Use a named store for isolation
createMemoryDrain({ store: 'my-service' })

Named Stores

Use named stores to isolate events from different services or for testing:

src/index.ts
import { createMemoryDrain, readMemoryLogs, clearMemoryLogs } from 'evlog/memory'

// Two separate buffers
const authDrain = createMemoryDrain({ store: 'auth' })
const apiDrain = createMemoryDrain({ store: 'api' })

// Read from a specific store
const authErrors = readMemoryLogs({ store: 'auth', level: 'error' })

// Clear a store (useful in tests)
clearMemoryLogs('auth')

Querying

readMemoryLogs supports the same filtering options as readFsLogs:

src/index.ts
import { readMemoryLogs } from 'evlog/memory'

// All events
const all = readMemoryLogs()

// Errors only
const errors = readMemoryLogs({ level: 'error' })

// Last 10 minutes
const recent = readMemoryLogs({
  since: new Date(Date.now() - 10 * 60 * 1000),
})

// Custom predicate
const slow = readMemoryLogs({
  filter: e => typeof e.duration === 'string' && e.duration.endsWith('s'),
})

// Most recent 50 events
const latest = readMemoryLogs({ limit: 50 })

readMemoryLogs Options

OptionTypeDescription
storestringNamed store to read from (default: 'default')
sinceDate | stringOnly events with timestamp >= since
untilDate | stringOnly events with timestamp <= until
levelLogLevel | LogLevel[]Filter by level
filter(event) => booleanCustom predicate
limitnumberReturn at most N most-recent matching events

Combining with Network Drains

Use the memory adapter locally while sending to an observability platform in production:

server/plugins/evlog-drain.ts
import { createMemoryDrain } from 'evlog/memory'
import { createAxiomDrain } from 'evlog/axiom'

const memory = createMemoryDrain()
const axiom = createAxiomDrain()

const drain = async (ctx) => {
  if (process.env.NODE_ENV === 'development') {
    await memory(ctx)
  } else {
    await axiom(ctx)
  }
}

Ring Buffer Behaviour

The buffer is bounded: once it reaches maxEvents, the oldest events are discarded to make room for incoming ones. This means memory usage stays constant regardless of how long the service runs.

Ring buffer (maxEvents: 5)
Write events 1–5 → [1, 2, 3, 4, 5]
Write event  6   → [2, 3, 4, 5, 6]  (1 is dropped)
Write events 7–8 → [4, 5, 6, 7, 8]
The in-memory buffer is lost when the worker/process restarts. For persistent storage, use the File System adapter (Node-based runtimes) or NuxtHub.

Direct API Usage

For advanced use cases, call the underlying helpers directly:

src/index.ts
import { writeToMemory, readMemoryLogs, clearMemoryLogs, parseReadMemoryLogsQuery } from 'evlog/memory'

// Write events directly (skips the drain pipeline)
writeToMemory([event], { store: 'default', maxEvents: 1000 })

// Read the current buffer
const events = readMemoryLogs()

// Parse HTTP query params into ReadMemoryLogsOptions
const opts = parseReadMemoryLogsQuery({ level: 'error', limit: '50' })
// → { level: 'error', limit: 50 }

// Reset for tests
clearMemoryLogs()

parseReadMemoryLogsQuery coercion rules

Query paramType in ReadMemoryLogsOptionsNotes
storestringPassed through as-is
sincestringISO 8601 string — parsed by readMemoryLogs
untilstringISO 8601 string — parsed by readMemoryLogs
levelLogLevel | LogLevel[]Comma-separated (error,warn) or repeated array; invalid values are dropped
limitnumberparseInt; NaN → omitted

Next Steps