Traces, logs and metrics
Follow one slow request through every service.
Unhandled stores the spans, logs and metrics your services send over OpenTelemetry and ties them together by trace. Search for the request that went wrong, see where its time went span by span, and read what it logged on the way.
- Across services
- Spans join one trace through traceparent
- Self time
- The 5 slowest spans marked on the timeline
- Logs on the trace
- Each record links to its trace and span
- Any OTLP metric
- Gauges, sums, histograms and summaries
Find the request that explains it
Search spans with field filters and free text. A filter matches when any span in the trace matches, so db.system=postgresql keeps every request that ran a Postgres query.
- Compare durations with units, such as duration>=1.5s, or status codes with http.status_code>=500
- Chart count, P95 or average duration, split by service, route or any attribute
- Live tail streams new traces into the top of the list every 2 seconds
Name the work between the queries
The AdonisJS agent traces requests, queries, mail, queue jobs and Ace commands. Wrap your own steps with record and they show up as one block in the waterfall, with the queries they ran nested inside.
- Queries and HTTP calls made inside the callback become its children
- A throw records the exception, marks the span as failed and rethrows
- The span decorator traces a method without touching its body
import { record } from '@unhandled/agent-adonisjs/helpers'
export default class InvoiceService {
async issue(order: Order) {
return record('invoice.issue', async (span) => {
span.setAttribute('order.id', order.id)
span.setAttribute('invoice.total_cents', order.totalCents)
const invoice = await Invoice.createFromOrder(order)
await invoice.renderPdf()
return invoice
})
}
}Logs that know which request wrote them
The AdonisJS agent forwards the Pino logger, so a logger.info() call in a controller or job arrives with the trace and span of the request that made it. Any OpenTelemetry SDK that exports logs works the same way.
- Same trace lists the lines of one request, Same service the 10 lines either side
- Patterns group messages once IDs, numbers and quoted strings are masked
- The trace view lists up to 1,000 of the trace's log records under the waterfall
Chart any metric your services send
Every OpenTelemetry metric lands in a catalog grouped by namespace. Open one to chart it with the aggregations its type allows, split by any attribute.
- Histograms chart as percentiles, rate or sum, and cumulative counters as increases per bucket
- Group draws the 10 largest series, with their latest, average, min and max in a table
- Add to dashboard saves the chart with its aggregation, grouping and filters
A page for each kind of work
Requests, queries, external calls, jobs and cache each get a page built from the same spans. Calls are grouped into endpoints, query patterns, hosts or job names, each with its volume, errors and latency.
- Possible N+1 query shows when a pattern runs 5 or more times in one trace
- Drag a box on the latency heatmap to see what the slow calls have in common
- Callers lists the routes and jobs behind a query and their share of its time
Works with
Send it from what you already run
- AdonisJSHTTP, queries, mail, queues and Ace commands, plus logs and runtime metrics.
- OpenTelemetryAny SDK that exports traces, logs or metrics over OTLP.
- BullMQJobs traced from the queue that adds them to the worker that runs them.
- Drizzle and KyselyQueries with their SQL, operation and table.
- Service mapThe calls between your services, drawn from the same spans.
- MCP serverAgents list and open traces, search logs and read metrics.
Questions
Try it on your own stack.
Every feature is on every plan, priced by the telemetry you send.