---
title: "Trace ingest diagnostics for one monitor"
method: GET
path: "/api/v1/checks/{id}/trace-diagnostics"
tags: ["Monitors"]
---

# Trace ingest diagnostics for one monitor

`GET /api/v1/checks/{id}/trace-diagnostics`

## Request

- Base URL: `https://app.lastping.dev`
- URL: `https://app.lastping.dev/api/v1/checks/{id}/trace-diagnostics`
- Auth: API key in header `X-Api-Key`

Why traces, metrics or logs sent to this monitor did or did not
arrive: the newest 20 ingest attempts (refusals and routine drops
with a reason code, and accepted batches; kept 7 days), the newest
accepted attempt, and
a summary of the monitor's newest traced run.

Call it after sending a test span, or whenever an exporter is
configured and nothing shows up. Two failures leave no attempt at
all, because neither reaches a point where LastPing knows which
project the request belongs to: an exporter using gRPC against the
OTLP gRPC port, and a missing or wrong key. An empty `attempts` list
means check those two first.

This is what the MCP `get_trace_diagnostics` tool returns. 404 if the
monitor does not exist in the authenticated project (no existence
oracle).

## Response `200`

The diagnostics.

- TraceDiagnostics: The body of `GET /api/v1/checks/{id}/trace-diagnostics`.
  - `attempts` IngestAttempt[], required: The newest 20 attempts, newest first.
    - `at` string, date-time, required
    - `outcome` 'accepted' | 'refused' | 'dropped', required: accepted: the batch was stored (reason is empty). refused: the export was rejected, or some or all of it was not kept for a reason worth fixing; see reason. dropped: the request was answered 202 and kept nothing, for a routine reason; nothing to fix.
    - `reason` string, required: A code, empty when accepted. A refused request, answered with a 4xx or 5xx: `unsupported_media_type`, `body_too_large`, `too_many_spans` (traces), `too_many_records` (more than 500 metric data points or log records), `unknown_monitor`, `expired_key`, `wrong_scope`, `wrong_project`, `monitor_mismatch`, `over_budget` (the shared daily budget), `over_log_budget` (the daily log event cap, logs only), `rate_limited`, `busy` (every OTLP slot was taken, answered 429 `OTLP_BUSY`) or `malformed`. A traces request answered 202 that kept nothing, with the outcome refused (the sending machine's clock needs fixing): `future_start` (every span started more than an hour after ingest received it). A metrics or logs request answered 202 that kept nothing, one row per reason, with the outcome dropped (routine, nothing to fix): `unknown_event` (a log event LastPing does not keep), `unknown_metric` (a metric family LastPing does not keep), `cumulative_temporality` (a cumulative data point; only delta is summed) or `invalid_point` (a data point with no usable value or token type, or past the 16 model series one request may add). A request that kept anything writes none of these. `too_many_series` is different, and its outcome is refused: it is written later, when the metrics are stored, after ingest has already answered 202. The owner (the agent, or the monitor) already had 200 new model series that day, so the data points of any further new series were not kept. It is written whenever that happens, even when the same request stored its other data points, at most once a minute per monitor. A model series the owner recorded in the seven days before is not new and is kept.
    - `span_count` integer, required: On an accepted row, how many spans, metric data points or log records were stored. On a row for a reason that discards items (`unknown_event`, `unknown_metric`, `cumulative_temporality`, `invalid_point`, `future_start`, `too_many_series`), how many items of the request were discarded for that reason. 0 on every other refusal.
    - `bytes` integer, required: The request body's size. On an accepted row, and on a refusal made after the body was read, it is the size LastPing read, after gzip is undone. On a refusal made before the body was read (a key, scope, rate or budget refusal), it is the request's Content-Length header, which is the compressed size when the body is gzip; 0 when the header was absent. A large request can be recorded as several accepted entries; the first carries the request's bytes, protocol and user agent and the rest carry 0 and empty strings, so a sum over entries is normally the bytes received. A part that stored nothing new (every span or log event already stored, every metric point of a new model series past the daily limit, or every metric sample dropped for an invalid day, value, metric or label) records no accepted entry, and when that part is the first, the request's bytes, protocol and user agent go unrecorded.
    - `protocol` string, required: http/protobuf or http/json, from the request's Content-Type; empty when it was neither.
    - `user_agent` string, required: The request's User-Agent header, with secrets redacted, cut to 200 bytes.
    - `signal` 'traces' | 'metrics' | 'logs', required
  - `last_accepted_at` string, date-time, nullable, required: The newest accepted attempt in the last 7 days, which may be older than every listed attempt; null when none.
  - `summary` TraceSummary, required: What the monitor's newest traced run contained.
    - `rid` string, required
    - `span_count` integer, required
    - `model` string, required: The model most of its calls used, empty when none named one.
    - `model_calls` integer, required
    - `tool_calls` integer, required
    - `tokens_in` integer, required
    - `tokens_out` integer, required
    - `cost_usd` string, nullable, required
    - `cost_source` string, required: What `cost_usd` is made of. A plain string, not an enum, so a new value never breaks a client: `client` when every priced call's cost was reported by the client (the exporter's own figure, or Claude Code's per-call cost), `estimated` when LastPing priced every call at API list prices, `mixed` when the sum holds both, empty when `cost_usd` is null.
    - `last_span_at` string, date-time, nullable, required
    - `is_test` boolean, required: True when this run is the set-up's test span (every span carried `lastping.test=true`), as `is_test` on `GET /api/v1/runs`. A set-up is working once one arrives; the tool's own first run follows it.

## Other responses

- `400`: The id is not a UUID.
- `401`: Missing or invalid API key.
- `403`: The API key authenticated, but its scope does not reach this operation. The problem body carries `required_scope`: the tier the operation needs. Re-mint the key at that scope (Settings, `POST /api/v1/api-keys`, or the `create_api_key` MCP tool) rather than retrying — the refusal is deterministic and a retry will be refused identically.
- `404`: Monitor not found in this project.
- `429`: Too many requests. The per-principal fixed-window rate limit has been exceeded. Retry after the number of seconds indicated by the `Retry-After` response header. The problem body's `code` is `RATE_LIMITED`. It names this limiter specifically, so a client can tell it apart from the application-level throttles that also answer 429 with their own codes and their own, much longer, `Retry-After` values (`TEST_PING_RATE_LIMITED`, `TOO_MANY_ATTEMPTS`). Back off globally on `RATE_LIMITED`; back off on the one operation for the others. The principal is the API key for a key-authenticated request and the USER for a browser session, so every tab one person has open shares one budget.

---

[API](https://skmtc.dev/lastping/apis/lastping-management-api.md) · [All operations](https://skmtc.dev/lastping/apis/lastping-management-api/llms.txt) · [OpenAPI document](https://skmtc.dev/lastping/apis/lastping-management-api/revisions/bbc6eda91c46?raw)
