Local-first log sink · searchable UI and API · agent-ready MCP

Your logs, readable by you and your agents.

One local process collects the logs your app routes to it. Full-text search, faceted filters and a live tail you can pause — plus an MCP endpoint, so your coding agent searches the same logs instead of asking you to paste your terminal.

localsink localhost:3000/?service=api-gateway,checkout,worker,postgres&level=error,warn
  • Service
  • all
  • api-gateway
  • checkout
  • worker
  • postgres
  • Severity
  • all
  • error
  • warn
  • info
  • debug
live tail tailing
service: api-gateway, checkout +2error, warn
error api-gateway upstream timeout after 30000ms route /v1/checkout status 504 +3
warn api-gateway retry budget 82% consumed for checkout attempt 3 +2
error checkout payment intent rejected: card_declined intent pi_3Qk… amount $42.00 +4
warn worker queue depth above threshold depth 1,284 queue emails +2
error postgres deadlock detected on relation orders pid 4471 +3
warn api-gateway slow request exceeded p99 budget dur 1.42s route /v1/cart +3
error worker job failed after 5 attempts, moving to dead letter job reconcile:8812 +2
warn checkout idempotency key reused within 60s window key ck_9f2… +4
error api-gateway upstream timeout after 30000ms route /v1/checkout status 504 +3
warn api-gateway retry budget 82% consumed for checkout attempt 3 +2
error checkout payment intent rejected: card_declined intent pi_3Qk… amount $42.00 +4
warn worker queue depth above threshold depth 1,284 queue emails +2
error postgres deadlock detected on relation orders pid 4471 +3

Interface preview — live tail, multi-select facets, structured attributes. Interface preview — live tail and structured log rows.

The problem

You find logs by filtering and searching — not by scrolling a mile back.

Three services, three terminal tabs, interleaved output, and Cmd-F pretending to be a search engine. Most teams eventually build a scrappy internal viewer for this. localsink is that tool, built properly.

Three tabs of scrollback
14:31:44 api {"level":30,"msg":"GET /v1/cart 200","dur":41,"req":"8f2c"}
14:31:44 worker reconcile:8812 attempt=2 queue=emails depth=1284
14:31:45 api {"level":40,"msg":"slow request","dur":1420,"route":"/v1/cart"}
14:31:45 checkout {"level":30,"msg":"cart validated","items":3}
14:31:46 worker reconcile:8812 attempt=3 queue=emails depth=1291
14:31:46 api {"level":30,"msg":"POST /v1/checkout","req":"8f2c"}
14:31:47 checkout {"level":50,"msg":"payment intent rejected","code":"card_declined"}
14:31:48 api {"level":50,"msg":"upstream timeout after 30000ms","route":"/v1/checkout"}
14:31:49 postgres {"level":50,"msg":"deadlock detected on relation orders","pid":4471}
14:31:49 api {"level":30,"msg":"GET /v1/cart 200","dur":38}
14:31:50 worker reconcile:8812 attempt=4 queue=emails depth=1302
One filtered view
error checkout payment intent rejected: card_declined
error api-gateway upstream timeout after 30000ms
error postgres deadlock detected on relation orders

For agents

Stop copy-pasting your terminal into a chat window.

Your agent writes the code and runs the app — then goes blind, because the output only exists in your terminal. localsink exposes its whole query surface over the Model Context Protocol at localhost:3000/mcp. Point Claude Code, Claude Desktop or any MCP client at it and the agent goes and finds the error itself: searchable and filterable, with the ability to poll forward for what's new.

The agent closes its own loop

Change code, re-run, search the logs, verify the fix — inside one turn, against what the app actually emitted rather than a guess.

Structured results, not screen scrapes

Levels, services, attributes and timestamps come back as data. Far less context burned than pasting a raw log file into the window.

Written for models, not just for humans

describe_logs tells the model to call it first to discover the service and level names that actually exist — so it filters on real values instead of inventing them.

One query surface, two front doors

The UI and the MCP tools share the same query layer, so they can’t drift apart. Everything the UI can do is a tool call.

Agent session localsink MCP
Checkout started failing after my last change. What's going on?
search_logs 3 rows
{ q: "checkout", level: ["error"], from: 1785076260000 }
error checkout payment intent rejected: card_declined svc checkout
error api-gateway upstream timeout after 30000ms svc api-gateway
error postgres deadlock detected on relation orders svc postgres
All three carry trace_id 8f2c…. The gateway timed out waiting on checkout, which was blocked by a deadlock on orders — from the transaction you widened in reserveStock(). Want me to narrow it back and re-run?
describe_logs

What services, levels and loggers exist, how many logs, what time range.

search_logs

The full filter and full-text query surface, same as the UI.

get_log_by_id

Drill into one row: attributes, error, stack, metadata.

Install

Two steps. Both of them tiny.

A single Node process and a SQLite database next to it — that's the whole deployment. No Docker Compose, no collector sidecar, no JVM, no cloud account, no API key.

Step 1 — run the sink
$ npx localsink

Starts on localhost:3000 with the web UI, the REST API and the MCP endpoint. Ctrl-C stops it.

Step 2 — point your logger at it
import pino from 'pino';

const logger = pino(
  pino.transport({
    target: '@localsink/pino',
    options: { serviceName: 'api' },
  }),
);

One import, one call. serviceName is the only required option; url defaults to http://localhost:3000.

Live tail

A tail you can stop and interrogate.

Terminal ordering — newest at the bottom, scroll up for history. Scroll up and the tail pauses itself, collecting arrivals into a pill instead of yanking you back down.

Auto-pause, then flush

Arrivals queue up as ↓ N new while you’re reading history. Scroll back down and they flush in; the view re-attaches to the tail.

No gaps, no duplicates

Close your laptop, restart the sink, lose your network — the watermark doesn’t move while you’re stopped, so the next poll backfills exactly what you missed.

Connection state always in view

Connected, reconnecting, offline — derived from the tail itself rather than a separate health ping.

Not virtualized, on purpose

A bounded scrollback buffer keeps native Cmd-F, real text selection and expandable variable-height rows working — the way terminal scrollback does.

Scrolled up — reading history arrivals collecting
warn worker queue depth above threshold
info api-gateway POST /v1/checkout 202
error checkout payment intent rejected: card_declined
warn checkout idempotency key reused within 60s window
debug postgres acquired advisory lock 4471
↓ 12 new
  • connected
  • reconnecting
  • offline

Zero infrastructure

One process. One database. Light enough to leave running.

No container to build, no search cluster to babysit, no daemon fleet idling on your laptop. Your logs never leave your machine — it works offline, on a plane, with no account and no API key.

What this usually costs you
services:
  collector:
    image: otel/opentelemetry-collector:latest
    volumes: ["./collector.yaml:/etc/config.yaml"]
  search:
    image: search-engine:8.13.0
    environment: [discovery.type=single-node, JAVA_OPTS=-Xms2g]
    ulimits: { memlock: -1 }
  viewer:
    image: log-viewer:9.2
    depends_on: [search]
    ports: ["5601:5601"]
volumes:
  search-data:
What localsink costs you
$ npx localsink
one process, started instantly
one localsink.db next to it
UI, REST API and MCP on one port

Details that matter

Built to disappear into your dev loop.

  • 01

    It can’t take your app down

    Every transport swallows network errors and non-2xx responses, times out after three seconds, and never throws or blocks. localsink being closed is a non-event for your app.

  • 02

    Nothing is suppressed

    The console.* transport patches every method — log, error, warn, info, debug, trace — and still calls the original method. Your terminal output is unchanged.

  • 03

    Structured logs stay structured

    Click a row for pretty-printed attributes, a formatted error with its stack, and key/value metadata. trace_id and span_id are first-class indexed columns.

  • 04

    Levels are yours, not ours

    Common severity names across pino, winston, syslog and console are colored consistently, anything else gets a stable palette slot — and your raw label always shows. No "unknown".

  • 05

    Dark-first, with a real light theme

    Terminal-adjacent, monospace, dense and calm. The light theme is a designed counterpart, not an inverted afterthought.

  • 06

    Typed contract, tested against the wire

    The wire schemas are published and shared, so the REST API, the MCP tools and every transport can’t drift. A Vitest harness boots a real server on a random port — no mocks.

Licensing

Source-available, not open core.

No artificial feature limits, no crippled community edition, no enterprise-only fork. Every user gets the whole product — commercial use is what's paid for.

Community Edition

Personal & non-commercial

Personal projects, hobbyists, students, academic work, and contributing to open source.

  • The complete product — nothing held back
  • Full source available to read and build from
  • No account, no limits, no cost
Install it
Licensed Edition

Companies & paid work

Internal tooling at a company, work on a paid product, or a client deliverable — including solo consultants and freelancers.

  • Identical product and full source
  • Same UI, same API, same MCP tools
  • One seat covers every machine that developer uses
See pricing

Coming soon

localsink isn't released yet.

Follow along on GitHub, or get in touch about commercial licensing.