When an agent's tool call fails or feels slow, you have two questions: what happened, and where did the time go. Searchable per-project logs answer the first. The request trace, split into your code time versus external I/O time and summarized as p50, p95 and p99, answers the second. Neither needs instrumentation code. Both are recorded before the complaint arrives, so you debug from evidence instead of trying to reproduce a ghost.
Where do you start when a tool "sometimes fails"?
Start with the logs, because a failure has a message and the message is usually the whole story. Every DenkOps app has searchable logs scoped to the project, so you filter to the tool that misbehaved and read what it actually printed. Guessing comes later, if at all.
Here is the shape of a real session. A teammate says the sync_contact tool "sometimes fails and is always kind of slow" in their assistant. That is two separate problems wearing one complaint, and you can take them one at a time.
Open the project's logs and search for sync_contact. Most lines are clean. Scattered among them, a handful read sync_contact: upstream responded 502 with timestamps. So the intermittent failure is not your code throwing. It is an upstream CRM API returning a bad gateway now and then, and your handler is faithfully surfacing it. That is a five-minute finding, and it came from reading, not reproducing.
Is it your code or their API that is slow?
Now the "always slow" half. Feelings are not latency, so open the project's Traces page. Every request produced a trace, and the project keeps a rolling summary: request count, error rate (the share of 5xx responses), and p50, p95 and p99 latency, next to average code time and average I/O time.
The percentiles frame the problem before you open a single trace. If p50 is fine but p99 is ugly, one call in a hundred is bad, and that is the user who complained. Filter the trace list by minimum duration, say 200 ms, to jump straight to the slow ones.
Open a slow POST /mcp trace. Total 240 ms. Code: 5 ms. I/O: 235 ms. The waterfall underneath shows one span eating the request: the outbound call to the same CRM API. Your handler runs in single digits. The other 97 percent is waiting on someone else's server, the same server throwing the occasional 502.
That split is the argument-settler. It tells you the "slow" and the "fails" are one dependency, not two bugs, and that none of the fix belongs in your code. For why the code-versus-I/O distinction is the one that changes what you do next, see traces that split your code from their API.
What do you actually change?
The logs and the trace point at the same target, so the fixes are external-dependency fixes:
- Add a timeout and a retry around the CRM call, so a single 502 becomes a fast retry instead of a hung tool call.
- Cache stable lookups. The vendor's contact records rarely change within a few minutes, so a short-lived entry in the store turns most of those 235 ms waits into local reads.
- Take the numbers to the vendor. "p99 240 ms, intermittent 502s, here are timestamps and counts" is a support ticket they can act on. "It feels flaky" is not.
Then measure the fix the same way you found the bug. Ship the change, watch the project's error rate and p95 for the next window, and confirm the code side of the split stayed flat while I/O dropped. If a deploy makes things worse instead, roll back with one click and keep looking. Regressions become before-and-after measurements, not anecdotes.
The whole loop, search the logs, read the split, change the dependency, watch the numbers, works because the observability is already on. If you have not deployed a connector yet, start with deploying your first MCP server, then come back to this page the first time something feels slow.