# Stream logs to an OpenTelemetry collector

An OpenTelemetry log stream sends each request of a gateway environment as one trace to a collector or backend that accepts OTLP over HTTP, about 30 seconds after the request ends.

## Before you begin

- A collector with an OTLP/HTTP receiver, reachable from the internet over `https://`. Faustine does not send to private addresses. See [Collector configuration](https://opentelemetry.io/docs/collector/configuration/) in the OpenTelemetry docs.
- The headers the collector requires for authentication, if any.

The stream sends OTLP/HTTP with a JSON body. It does not send OTLP over gRPC.

## Add the stream

1. In the portal, go to [**Agent Gateway**](https://auth.faustine.ai/go?to=%2Fintegrations%2Fagent-gateway) and select your gateway.
2. Select the **Logs** tab, then **Log settings**.
3. Under **Log streams**, select **Add a stream**.
4. Under **Destination**, select **OpenTelemetry**.
5. Fill in the fields in the table.
6. Select **Send test event**. Faustine sends a trace of one request marked `test: true` and shows **Delivered** when the collector accepts it.
7. Select **Continue**.
8. Under **What to send**, choose **All requests** or **Errors and denials**, and turn on **Discovery calls** or **Payloads** if you want them. Select **Continue**.
9. Under **Review and create**, enter a **Name** and select **Create stream**.

| Field | Value |
| --- | --- |
| **OTLP/HTTP endpoint** | The address of the collector, such as `https://otel.example.com:4318`. Faustine adds `/v1/traces`. |
| **Headers** | Headers sent with every batch, such as `Authorization`. Optional, up to 10. The values are stored encrypted. |
| **Service name** | The `service.name` of the traces. Optional. Defaults to `faustine-gateway`. |

The stream's page opens. The first trace reaches the collector about 30 seconds after the next request ends.

## What the collector receives

Each batch is one OTLP/HTTP request in JSON, posted to `/v1/traces` on the endpoint. A request is one trace.

| Span | Kind | Name | Main attributes |
| --- | --- | --- | --- |
| The request | Server | `tools/call <tool>` over MCP, `REST <operation>` over REST | `gen_ai.tool.name`, `mcp.method.name`, `mcp.session.id`, `faustine.request.id`, `faustine.request.status`, `faustine.actor.name`, `faustine.client.name` |
| Each call | Internal | The operation key, such as `crm.deals.get` | `faustine.op.key`, `faustine.connection`, `faustine.call.status`, `faustine.upstream.status` |
| Each exchange with your API | Client | The HTTP method, or `<service>/<method>` for gRPC | `http.request.method`, `url.full`, `http.response.status_code`, `server.address`, or `rpc.system`, `rpc.service`, `rpc.method` |

The resource has `service.name`, `deployment.environment.name` (the environment's slug) and `faustine.gateway.id`, `faustine.gateway.slug` and `faustine.gateway.name`. A span that failed has the status `ERROR` and an `error.type` attribute. When the stream includes payloads, the request span has `gen_ai.tool.call.arguments` and `gen_ai.tool.call.result`, each cut at 8192 characters.

The span of each exchange with your API has the span ID the gateway sent to your API in the `traceparent` header. If your API sends its own traces to the same backend, its spans appear inside the gateway's trace.

The instrumentation scope is `faustine.gateway`. Each batch also has an `X-Faustine-Delivery` header that identifies it.

### Other attributes

| Span | Attributes |
| --- | --- |
| The request | `gen_ai.operation.name` (`execute_tool`), `jsonrpc.request.id`, `client.address`, `user_agent.original`, `faustine.request.kind`, `faustine.request.channel`, `faustine.deny_reason`, `faustine.op.key`, `faustine.actor.kind`, `faustine.actor.id`, `faustine.client.version`, `faustine.client.trace_id`, `faustine.client.request_id`, `faustine.run_execution.id` |
| Each call | `faustine.deny_reason`, `faustine.upstream.ms`, `faustine.recipe`, `faustine.replayed`, and with payloads `faustine.call.input` and `faustine.call.output` |
| Each exchange with your API | `server.port`, `rpc.grpc.status_code`, `faustine.attempt.purpose`, `faustine.attempt.number`, `faustine.upstream.request_id` |

An exchange that signed in to your API has ` (sign-in)` after its name. An attribute without a value is left out.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| The test shows "Log streams only reach public hosts." | The endpoint resolves to a private or reserved address. | Expose the collector on a public address with TLS. |
| The test shows "Nothing at that path" | The endpoint already has a path the collector does not serve. | Enter the base address. Faustine adds `/v1/traces` unless the URL already ends with it. |
| The test shows "The destination rejected the credentials" | The collector requires a header that is missing or wrong. | Add it under **Headers**. |

## Next steps

- [Log streams](https://docs.faustine.ai/agent-gateway/log-streams/): delivery, retries and the fields of a request.
- [Redact sensitive fields](https://docs.faustine.ai/agent-gateway/redact-sensitive-fields/): decide what a stream can carry.
- [Set recording and retention](https://docs.faustine.ai/agent-gateway/retention/): record the payloads a stream sends.
