# OpenTelemetry SIEM Integration

Push Capsule detections, findings, and policy violations to any OpenTelemetry (OTLP) logs endpoint: your own OpenTelemetry Collector, or an observability or SIEM backend that accepts OTLP directly. Put a Collector in front to route events anywhere it can export.

## Overview

Capsule sends each event as an OTLP log record as it is produced, over OTLP/HTTP with JSON encoding, gzip-compressed.

| Event type | What gets exported |
|  --- | --- |
| **Detections** | Real-time detections raised on agent activity, with full event context |
| **Findings** | Posture and configuration findings across your AI inventory |
| **Policy violations** | Guardrail and policy breaches, with the full evidence the decision was made on |


```
Capsule event (detection / finding / policy violation)
  → OTLP log record, body = the full Capsule event envelope
  → POST <endpoint>/v1/logs, gzipped JSON, with your headers
  → your OpenTelemetry Collector or OTLP-compatible backend
```

> This is separate from **Settings → Notifications**. Notifications push individual alerts to a channel; the SIEM integration streams the full, verbose event record for ingestion and long-term correlation.


> **OTLP/HTTP JSON only.** Protobuf encoding and OTLP/gRPC are not supported. An endpoint that only accepts protobuf answers with `HTTP 415`. Put a Collector in front of it: the Collector receives JSON and re-exports in whatever protocol the backend needs.


## Prerequisites

- A **Capsule** account with admin access, and the **SIEM** tab visible under **Settings**
- An OTLP/HTTP logs endpoint reachable from the internet over **HTTPS**
- The credentials your endpoint expects, as one or more request headers


## Step 1: Prepare your endpoint

### Endpoint URL

Capsule accepts either shape:

- **A collector base URL**, the value you would put in `OTEL_EXPORTER_OTLP_ENDPOINT`. Capsule appends `/v1/logs`. For example `https://otel-collector.acme.com:4318` is posted to `https://otel-collector.acme.com:4318/v1/logs`.
- **The full logs URL**, the value you would put in `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. A URL that already ends in `/v1/logs` is used as is.


The endpoint must be `https`. Redirects are not followed, so point Capsule at the receiver itself rather than at a URL that forwards to it.

### Headers

Headers are sent with every request, and are usually how the endpoint authenticates Capsule. They are write-only: Capsule stores them encrypted and never shows the values again. You can add up to 20. `Content-Type`, `Content-Encoding`, `Content-Length`, `Host`, `Transfer-Encoding` and `Connection` are set by Capsule and cannot be overridden.

| Destination | Endpoint | Header |
|  --- | --- | --- |
| OpenTelemetry Collector with `bearertokenauth` | `https://<your-collector>:4318` | `Authorization: Bearer <token>` |
| Grafana Cloud OTLP | `https://otlp-gateway-<zone>.grafana.net/otlp` | `Authorization: Basic <base64 of instanceId:token>` |
| Honeycomb | `https://api.honeycomb.io` | `x-honeycomb-team: <api key>` |


For Grafana Cloud, the instance ID, zone, and a token with write access are on the **OpenTelemetry** tile of your Grafana Cloud stack. Encode the pair with `echo -n '<instanceId>:<token>' | base64`.

A collector that authenticates by network rather than by header, for example one behind an IP allow-list, needs no headers at all.

### Example: OpenTelemetry Collector

A minimal [OpenTelemetry Collector Contrib](https://github.com/open-telemetry/opentelemetry-collector-contrib) config that accepts Capsule's events with a bearer token and prints them. Replace the `debug` exporter with the exporter for your backend once you have seen events arrive.

```yaml
extensions:
  bearertokenauth:
    token: ${env:CAPSULE_OTLP_TOKEN}

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
        auth:
          authenticator: bearertokenauth
        tls:
          cert_file: /etc/otelcol/tls/server.crt
          key_file: /etc/otelcol/tls/server.key

exporters:
  debug:
    verbosity: detailed

service:
  extensions: [bearertokenauth]
  pipelines:
    logs:
      receivers: [otlp]
      exporters: [debug]
```

In Capsule, add the header `Authorization` with the value `Bearer <the CAPSULE_OTLP_TOKEN value>`. If TLS terminates at a load balancer in front of the Collector, drop the `tls` block and give Capsule the load balancer's `https` URL.

## Step 2: Create the SIEM integration in Capsule

1. In Capsule, go to **Settings → SIEM** and click **Add integration**.
2. Set **Destination** to **OpenTelemetry (OTLP)**.
3. Enable the event types you want: **Detections**, **Findings**, **Policy violations**.
4. Choose an **Event detail** level:
  - **Full** sends the whole envelope, matching the S3 and Splunk exports.
  - **Compact** omits activity message bodies. Use it where your backend bills by ingested volume and the transcript is not what you search on.
5. Paste the **Endpoint** from Step 1.
6. Under **Headers**, click **Add header** for each header your endpoint needs, and enter its name and value.
7. Click **Save**, then **enable** the integration. A new integration starts disabled and exports nothing until you turn it on.


When you edit the integration later, saved header values stay hidden. Leave **Headers** empty to keep them. Entering any header replaces the whole saved set, so enter every header the endpoint needs, not only the one you are changing.

## Step 3: Verify

Use the row menu's **Send test event**. Capsule posts a single connection-test log record to your endpoint and reports whether it was accepted, with the endpoint's error if it was not. The record is a `capsule.detection.created` event whose detection type is `CAPSULE_CONNECTION_TEST`, so it is easy to filter out. With the Collector example above, the record appears in the Collector's output.

The **Delivery** column on the SIEM list shows the outcome of the most recent attempt, and the details drawer shows the last error in full.

## What arrives at your endpoint

Each request is one OTLP `ExportLogsServiceRequest` carrying one resource, one scope, and a batch of log records.

### Log record mapping

| OTLP field | Value |
|  --- | --- |
| Resource attribute `service.name` | `capsule` |
| Resource attribute `capsule.tenant.id` | Your Capsule tenant ID |
| Scope `name` / `version` | `capsule.siem-export` / `2`, the envelope schema version |
| `timeUnixNano` | When the detection, finding, or policy violation was created |
| `observedTimeUnixNano` | When Capsule exported the event |
| `severityNumber`, `severityText` | Policy violations only, see below |
| Attribute `event.name` | `capsule.detection.created`, `capsule.finding.created`, or `capsule.policy_violation.created` |
| Attribute `capsule.event.id` | The ID of the detection, finding, or policy violation |
| Attribute `capsule.schema_version` | `2` |
| `body` | The full Capsule event envelope, as a structured map |


### Severity

Policy violations carry Capsule's severity. `severityNumber` maps it onto the OTLP severity ranges, and `severityText` carries Capsule's own value.

| Capsule severity | `severityNumber` | OTLP range |
|  --- | --- | --- |
| `critical` | 21 | FATAL |
| `high` | 17 | ERROR |
| `medium` | 13 | WARN |
| `low` | 9 | INFO |


Capsule does not rank detections or findings, so those records have no severity fields rather than an invented one.

### Body

The body is the same envelope the S3 and Splunk destinations receive, with the record under `event`:

```json
{ "schemaVersion": 2, "eventType": "policy_violation.created", "tenantId": "...", "exportedAt": "...", "event": { ... } }
```

It is sent as a structured OTLP value rather than a JSON string, so a backend can query its fields directly. Objects become `kvlistValue`, arrays become `arrayValue`, integers become `intValue`, other numbers become `doubleValue`, and `null` becomes an empty value.

Every event carries its anchor resource in full - id, name, type, external id, parent, and the environment and connector it was discovered through - so events correlate against the rest of your inventory without a lookup.

- **`policy_violation.created`** is self-contained: the violation, the policy that raised it including its conditions, and the complete finding, detection and activity records the decision was made on.
- **`detection.created`** carries the detection, its evidence, and the session activities it was raised on.
- **`finding.created`** carries the finding, its evidence, and its remediation steps.


Triage state is exported on all three. Secrets Capsule identified in evidence are masked to a short prefix before export, in evidence text and activity message bodies alike.

> **One violation, one event.** A violation is exported once, when it is first raised. If it is later resolved and the same condition re-triggers, Capsule reuses the existing violation and does not emit a second `policy_violation.created`.


## Delivery behaviour

- **Batching** - records are gzipped and split so each request stays under 1 MB compressed, a limit every common OTLP backend accepts.
- **Oversized events** - a single event that still exceeds the limit after compression is rejected and surfaced on the integration, never truncated. Switching **Event detail** to Compact is the fix.
- **Retries** - Capsule retries the statuses the OTLP/HTTP specification marks retryable (`429`, `502`, `503`, `504`), timeouts, and network errors, with exponential backoff, honouring `Retry-After`. Every other failure is a configuration error and is reported instead.
- **Timeouts** - each request has 10 seconds to complete.
- **Visibility** - the outcome of the most recent attempt is shown per integration in **Settings → SIEM**, with the full error in the details drawer.
- **Backfill** - export begins when the integration is enabled. Events raised before that are not sent.


## Troubleshooting

### `rejected the credentials in the configured headers` (HTTP 401 or 403)

The endpoint did not accept the headers. Check the header name your backend expects and the value's format, for example the `Bearer ` or `Basic ` prefix. Saved values are hidden, so enter every header again and save.

### `no logs receiver at this URL` (HTTP 404)

The path is wrong. Give Capsule the collector base URL and let it append `/v1/logs`, or the full logs URL. A base URL that already includes a different signal path, such as `/v1/traces`, will not work.

### `does not accept OTLP/HTTP JSON` (HTTP 415)

The endpoint only accepts protobuf. Capsule sends JSON only. Route the events through an OpenTelemetry Collector, which accepts JSON and exports protobuf or gRPC to the backend.

### `accepted the request but rejected N log record(s)`

The endpoint returned success but reported a partial success, dropping some records. Capsule shows this as a failed delivery with the endpoint's own message, since the dropped records were not stored. The message usually names the limit or field the backend objected to.

### Timeouts or nothing arrives

- Confirm the integration is **enabled** and at least one event type is turned on.
- Use **Send test event** - it reports the exact error from the endpoint.
- Confirm the endpoint is reachable from the internet over `https` on the port in the URL. If it sits behind an IP allow-list, Capsule's egress addresses need to be on it. Contact support for the current list.
- A request that takes longer than 10 seconds is retried and then reported as failed. Check the endpoint's load and anything between it and the internet, such as a proxy or load balancer.


## Support

For help with this integration, contact [support@capsule.security](mailto:support@capsule.security).