# Telemetry Read API

The Telemetry Read API returns normalized OpenTelemetry logs and metric points for a project. Use
the [Trace Read API](./trace-api.md) for trace, span, conversation, and observed-user views. Use the
[OTLP/HTTP API](./otlp-api.md) to ingest all three signal types.

Base URL:

```text
https://api.provon.dev/v1
```

Public service clients use a project API key with `telemetry:read`:

```http
Authorization: Bearer <PROVON_API_KEY>
```

The key selects the project, so public routes do not require a project ID in the path.

## Endpoints

| Method | Public API path | Workbench path                 | Purpose      |
| ------ | --------------- | ------------------------------ | ------------ |
| `GET`  | `/logs`         | `/projects/:projectId/logs`    | List logs    |
| `GET`  | `/metrics`      | `/projects/:projectId/metrics` | List metrics |

The project-qualified forms are for signed-in Workbench calls and project-scoped clients that need
an explicit path. API integrations should normally use `/logs` and `/metrics`.

## Time Range

Every request must provide one time-range form:

| Form                 | Example                                     |
| -------------------- | ------------------------------------------- |
| Relative range       | `range=15m`, `range=6h`, or `range=7d`      |
| Absolute range       | `startMs=1760000000000&endMs=1760086400000` |
| Encoded custom range | `range=1760000000000-1760086400000`         |

Times are Unix milliseconds. When using `startMs`/`endMs`, both values are required and `startMs`
must not exceed `endMs`.

## List Logs

```http
GET /v1/logs?range=1h&limit=100
Authorization: Bearer <PROVON_API_KEY>
```

Query parameters:

| Parameter   | Type             | Purpose                                    |
| ----------- | ---------------- | ------------------------------------------ |
| `range`     | string           | Relative or encoded absolute range         |
| `startMs`   | Unix millisecond | Absolute range start                       |
| `endMs`     | Unix millisecond | Absolute range end                         |
| `traceId`   | string           | Return logs correlated to one trace        |
| `eventName` | string           | Return logs with one normalized event name |
| `limit`     | integer          | Page size from `1` to `500`; default `100` |
| `cursor`    | string           | Opaque cursor from the previous response   |

Response:

```json
{
  "logs": [
    {
      "trace_id": "4f3c2f...",
      "span_id": "9a41...",
      "time_ms": 1760086000000,
      "observed_time_ms": 1760086000042,
      "severity_text": "INFO",
      "body": {
        "message": "tool call completed"
      },
      "attributes": {
        "tool.name": "lookup_order"
      },
      "resource_attributes": {
        "service.name": "support-agent"
      },
      "scope_name": "agent-runtime",
      "event_name": "tool.result"
    }
  ],
  "pagination": {
    "limit": 100,
    "nextCursor": null
  }
}
```

`body` and attribute values are JSON values. `trace_id`, `span_id`, timestamps, severity, scope,
and event name can be `null` when the source did not provide them.

## List Metrics

```http
GET /v1/metrics?range=24h&metricNames=gen_ai.client.token.usage,gen_ai.client.operation.duration
Authorization: Bearer <PROVON_API_KEY>
```

Query parameters:

| Parameter     | Type             | Purpose                                    |
| ------------- | ---------------- | ------------------------------------------ |
| `range`       | string           | Relative or encoded absolute range         |
| `startMs`     | Unix millisecond | Absolute range start                       |
| `endMs`       | Unix millisecond | Absolute range end                         |
| `metricNames` | CSV string       | Exact metric names to include              |
| `limit`       | integer          | Page size from `1` to `500`; default `100` |
| `cursor`      | string           | Opaque cursor from the previous response   |

Response:

```json
{
  "metrics": [
    {
      "metric_name": "gen_ai.client.operation.duration",
      "description": "Model operation duration",
      "unit": "s",
      "type": "histogram",
      "start_time_ms": 1760085000000,
      "time_ms": 1760086000000,
      "attributes": {
        "gen_ai.operation.name": "chat",
        "gen_ai.provider.name": "openai"
      },
      "resource_attributes": {
        "service.name": "support-agent"
      },
      "scope_name": "agent-runtime",
      "value": null,
      "count": 24,
      "sum": 31.2,
      "min": 0.4,
      "max": 4.8,
      "bucket_counts": [3, 12, 8, 1],
      "explicit_bounds": [0.5, 1, 2],
      "exemplars": null
    }
  ],
  "pagination": {
    "limit": 100,
    "nextCursor": null
  }
}
```

Metric `type` is one of `gauge`, `sum`, `histogram`, `exponential_histogram`, or `summary`.
Fields that do not apply to that type are `null`.

## Pagination

Results use cursor pagination in descending telemetry time order. Pass `pagination.nextCursor`
unchanged:

```bash
curl --get "$PROVON_API_URL/logs" \
  -H "Authorization: Bearer $PROVON_API_KEY" \
  --data-urlencode "range=24h" \
  --data-urlencode "cursor=$NEXT_CURSOR"
```

Treat the cursor as opaque. Its encoding can change independently of record fields.

## Read Consistency

OTLP ingestion is asynchronous. A successful `POST /v1/logs` or `POST /v1/metrics` on the OTLP
surface means the payload was accepted for processing; it may not be immediately visible through
this API.

For verification:

1. keep the original signal timestamp inside the requested range;
2. poll with bounded backoff;
3. narrow by `traceId`, `eventName`, or `metricNames` where possible;
4. stop retrying on authentication or validation errors.

## Errors

| Status | Meaning                                        |
| ------ | ---------------------------------------------- |
| `400`  | Missing/invalid time range or malformed cursor |
| `401`  | Missing or invalid project API key             |
| `403`  | API key does not include `telemetry:read`      |
| `500`  | Query or storage failure                       |

Authentication failures use the shared structured API error. Some query-validation failures return
a compact `{ "error": "..." }` body; clients should use the HTTP status as the primary control
signal.

## Related Docs

- [Trace Read API](./trace-api.md)
- [OTLP/HTTP API](./otlp-api.md)
- [Tracing attribute reference](./tracing-attributes.md)
- [OpenTelemetry setup](./opentelemetry.md)
- [API reference](./api-reference.md)
