Repository avatar
AI Tools
v0.1.0
active

spanlog-mcp

io.github.chaugiakhang193/spanlog-mcp

Find the lines of one request, the newest failures and the slowest calls in spanlog JSON logs

Documentation

spanlog

Readable call-boundary logs for Node.js, Go and Python.

2026-11-01 01:49:38.070 +07:00 INFO  [shop] req=89e32c2e POST /orders | START
2026-11-01 01:49:38.071 +07:00 INFO  [shop] [OrderService.handleOrder] req=89e32c2e run | START
2026-11-01 01:49:38.071 +07:00 INFO  [shop] [OrderService.handleOrder] req=89e32c2e call InventoryService.reserve | START
2026-11-01 01:49:38.072 +07:00 DEBUG [shop] [InventoryService.reserve] req=89e32c2e query UPDATE inventory | START
2026-11-01 01:49:38.074 +07:00 DEBUG [shop] [InventoryService.reserve] req=89e32c2e query UPDATE inventory | DONE 2.5ms
2026-11-01 01:49:38.075 +07:00 ERROR [shop] [OrderService.handleOrder] req=89e32c2e call InventoryService.reserve | FAILED 3.6ms Error: stock not available
    at InventoryService.reserve (file:///app/server.js:43:13)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
    at async OrderService.handleOrder (file:///app/server.js:63:5)
    at async file:///app/server.js:110:17
2026-11-01 01:49:38.076 +07:00 INFO  [shop] [OrderService.handleOrder] req=89e32c2e run | FAILED 5.1ms Error: stock not available
2026-11-01 01:49:38.077 +07:00 WARN  [shop] req=89e32c2e POST /orders | FAILED 500 6.9ms

This is one request from examples/express-orders, without the lines of one internal call. Stack paths are shortened.

spanlog writes one line when a call starts and one line when it ends, with its duration. The lines go to stdout as readable text or JSON, or into the logger the application already uses: pino, slog or logging. There is no collector, dashboard or account to set up.

Install

npm install spanlog

For Go:

go get github.com/chaugiakhang193/spanlog/go

For Python:

pip install spanlog

Use

Express, once per application and once per service:

const log = createSpanlog({ service: 'shop' });
const orders = log.traced(new OrderService(), 'OrderService');
app.use(logRequests(log));

Go, once per application and once per method:

logger := spanlog.New(spanlog.Options{Service: "shop"})
http.ListenAndServe(address, logger.Middleware(mux))

func (s *OrderService) HandleOrder(ctx context.Context) (err error) {
	ctx, done := s.log.Start(ctx, "OrderService", "HandleOrder")
	defer done(&err)
	return s.inventory.Reserve(ctx)
}

FastAPI, once per application and once per service. Flask has the same two lines through spanlog.wsgi:

log = Spanlog(service="shop")
orders = log.traced(OrderService(), "OrderService")
app.add_middleware(LogRequests, log=log)

To keep the logger you already have, pass it as the sink. Its own level filters the lines too:

const log = createSpanlog({ service: 'shop', sink: pinoSink(pino()) });
logger := spanlog.New(spanlog.Options{Service: "shop", Sink: spanlog.SlogSink(slog.Default())})
log = Spanlog(service="shop", sink=logging_sink(logging.getLogger("shop")))

What it covers

Node.jsGoPython
Incoming requestsExpress 5net/httpASGI (FastAPI, Starlette), WSGI (Flask)
Callstraced wraps a whole object, call wraps one piece of workStart in each method, Call wraps one piece of worktraced wraps a whole object, call and acall wrap one piece of work
Database queriesTypeORM 1.1, with the operation and table namesdatabase/sql, any driver, with the operation and table namesSQLAlchemy 2, with the operation and table names
DestinationsReadable text, JSON, pinoReadable text, JSON, slogReadable text, JSON, logging

All implementations follow one specification and are tested against the same sample lines.

Safe defaults

  • Arguments, results and SQL text stay out of the logs unless passed as params or summary.
  • Those two are redacted by key. A sensitive value in a plain string is not seen.
  • Each error is logged once per request, with its stack.
  • An error's message is written as it is and can repeat a value, as a database error can repeat a SQL parameter. The errorMessage option replaces it.
  • The request line holds the real path without its query string. A path can hold personal data.

Search the logs from an AI assistant

@spanlog/mcp is an MCP server for log files written by the JSON writer. An AI assistant connected to it can ask for every line of one request, the newest failures or the slowest lines, and gets those lines back instead of the whole file.

npx -y @spanlog/mcp --file /var/log/shop/app.log

Other tools

spanlog is not a logger. It decides which lines to write and hands them to a sink, and pinoSink, SlogSink and logging_sink keep pino, slog or logging as the logger. Each tool below does more than spanlog in its own area. Pick spanlog when plain lines in your own logger are what you want.

ToolOutputSetupScope
spanlogLines in the application's loggerMiddleware, one wrapper per serviceExpress, net/http, ASGI and WSGI, TypeORM and SQLAlchemy queries
pino 10.4.0 with pino-http 11.0.0JSON lines, one per finished request with responseTimeA logger and a middlewareCalls and queries are logged by hand through req.log
@nestjs/observe 0.3.21Traces in a hosted dashboard, with an app key and secretOne module and one option in NestFactory.create. Every provider is instrumented by defaultNestJS, with queries of pg, mysql2, mssql, mongodb
OpenTelemetrySpans sent to the backend of your choiceSDK, instrumentations and an exporterMany languages and libraries. @opentelemetry/instrumentation-typeorm 0.24.0 records the statement text
nestlogged 3.7.3Lines through the NestJS loggerA decorator on each classNestJS

Status

Early development. Fields and options can change before 1.0. Each release is listed in docs/CHANGELOG.md.

License

MIT

NPM
@spanlog/mcp
Install Command
npm install @spanlog/mcp