Setup guide

Already sending OpenTelemetry somewhere else?

Run one collector as a gateway and fan out — your existing vendor keeps everything it gets today, and SightLift gets its own copy.

Who · Whoever runs your observability pipeline Time · ~30 min You'll need · A host that runs Docker
← Back to the dashboard

Is this the guide you want?

Yes if Datadog, Splunk, Grafana, Honeycomb or similar already consumes your Claude telemetry and you want SightLift too.

No if nothing consumes it yet — then skip the collector and use Connect Claude to SightLift instead.

Step 1: the shape

How this fits together

You run the gateway; SightLift is one destination on it, not the hub. Claude Code and Cowork each send to one place — your collector — which decides who gets a copy.

  Claude Code  ─┐
                │        ┌─────────────────────────┐      ┌──────────────────────┐
  Cowork       ─┼───────►│  YOUR collector         │─────►│  your existing vendor │
                │        │  (gateway — you own it) │      └──────────────────────┘
  anything else ┘        │                         │      ┌──────────────────────┐
                         │                         │─────►│  SightLift            │
                         └─────────────────────────┘      └──────────────────────┘

This is the Collector's gateway pattern, and fan-out is built in: a pipeline lists destinations, every record goes to all of them. One egress point to allowlist, and adding or dropping a vendor (us included) is one config block — not a settings rollout to every developer.

Why not let your vendor relay to us? It makes them a dependency for our data freshness and hands them all of your prompt content. We can't be your gateway either — our ingest writes to storage and forwards nowhere, by design.

Step 2: what to send

What SightLift needs from the stream

Every capability survives three hops: Claude emits it, your collector carries it, your collector sends it. This table is the contract.

What you get in SightLiftTurn on at the sourceTravels asPipeline
Activity, adoption, cost CLAUDE_CODE_ENABLE_TELEMETRY
OTEL_LOGS_EXPORTER
log records logs
Accurate topic classification OTEL_LOG_USER_PROMPTS log record body logs
Candidate automations (the command that ran) OTEL_LOG_TOOL_DETAILS tool_parameters attributes logs
Verified capabilities (proof it reproduces your results) CLAUDE_CODE_ENHANCED_TELEMETRY_BETA
OTEL_TRACES_EXPORTER
OTEL_LOG_TOOL_CONTENT
a tool.output event on a span traces
Cowork at all Cowork Monitoring fields
(content on by default)
log records + spans both
Send both signals — logs and traces. Tool output rides on spans. A logs-only pipeline looks healthy, exports without error, and silently costs you capability verification. This is the most common way this setup goes wrong.
RuleBecause
Enable content at the source Redaction only removes. Without OTEL_LOG_USER_PROMPTS every prompt arrives as the literal <REDACTED> — no pipeline change brings it back.
Don't filter toward us Our ingest drops what it can't use via a denylist, so anything unrecognized is kept. A filter on your side is an allowlist — it would drop what we can't re-request. Trim on the vendor pipeline.
Never send us metrics No metrics endpoint — we 405 every batch. Your vendor can use them, so enable Claude Code metrics and route them to the vendor only.

Step 3: credentials

Get your ingest key

Needs the Admin role in SightLift.

Navigate to
app.sightlift.ai Configuration Data & sources Anthropic Claude Live telemetry
  1. Set Your Anthropic plan, then open the Live telemetry tab.
  2. Generate your first key and copy it immediately — shown once, gone when you navigate away.
Take the key on its own — not the "OTLP headers" value. That field gives you Authorization=Bearer sl_… as one string, which is what Claude Code's env var wants. Collector YAML wants a header map, so pasting it there yields a header whose value starts Authorization=Bearer — a 401 that looks like a bad key. Here, SIGHTLIFT_INGEST_KEY is the key alone.

Have your vendor's ingest credential to hand too — both go in the same file in step 5.

Step 4: pick the collector

Which collector, and its config

Run this one

otelcol-contrib 0.157.0 — the OpenTelemetry project's own build, from the group that defines the protocol. Image otel/opentelemetry-collector-contrib:0.157.0; step 5 runs it as one container and you install nothing else. Your gateway shouldn't belong to any vendor it feeds, us included.

Contrib, not plain otelcol. A two-destination fan-out works on either, but the redaction processor in step 9 and the file_storage extension in step 10 are contrib-only — start on core and you'll be building a custom distribution to finish this page.

Already running a collector? Skip steps 4 and 5. Add the otlphttp/sightlift exporter below to your config and list it on your logs and traces pipelines. Check two things first: it runs as a gateway, not a per-host agent, and its processors aren't already stripping the content step 2 needs — plenty of pipelines drop log bodies by default.

The config

Make a directory for three small files. This is the first, and it's complete — one place in, two places out.

collector.yaml

# Receive OTLP from Claude Code, Cowork, and anything else you point here.
receivers:
  otlp:
    protocols:
      http:                      # 4318 — what Claude Code and Cowork use
        endpoint: 0.0.0.0:4318
      grpc:                      # 4317 — for anything else you send here
        endpoint: 0.0.0.0:4317

processors:
  # Keep the collector from eating the host if a burst arrives. Must run first.
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128
  batch: {}                     # group records before sending. Must run last.

exporters:
  # SightLift. The two signal-specific endpoints are used VERBATIM, so the
  # full path belongs here. Note there is deliberately no metrics endpoint.
  otlphttp/sightlift:
    logs_endpoint: https://app.sightlift.ai/v1/logs
    traces_endpoint: https://app.sightlift.ai/v1/traces
    headers:
      Authorization: "Bearer ${env:SIGHTLIFT_INGEST_KEY}"

  # Your existing vendor. Replace the endpoint and the auth header with
  # whatever they document. A BASE endpoint like this one gets /v1/logs and
  # /v1/traces appended automatically.
  otlphttp/vendor:
    endpoint: https://otlp.your-vendor.example
    headers:
      api-key: "${env:VENDOR_API_KEY}"

service:
  pipelines:
    # Listing two exporters IS the fan-out. Every record goes to both.
    logs:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/sightlift, otlphttp/vendor]

    # Do not skip this one: tool OUTPUT rides on spans, not log records.
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/sightlift, otlphttp/vendor]

    # Metrics go to your vendor ONLY. SightLift has no metrics endpoint and
    # will 405 every batch. Delete this block if you don't collect metrics.
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/vendor]

  telemetry:
    # The collector's own metrics — this is how you prove, in step 7, that
    # each destination is receiving. 0.0.0.0 so it's reachable outside the
    # container; the default 127.0.0.1 would only answer inside it.
    metrics:
      readers:
        - pull:
            exporter:
              prometheus:
                host: 0.0.0.0
                port: 8888

No routing component needed. A pipeline hands every record to every exporter it lists, and each keeps its own queue and retries independently — so a slow or dead vendor can't block the other. There's no "primary" to choose.

Step 5: run it

Add your keys and start the collector

Both credentials go in .env beside collector.yaml — out of the config file and your shell history.

.env

# The key on its own — no "Authorization=Bearer" prefix. See step 3.
SIGHTLIFT_INGEST_KEY=sl_your_key_here
VENDOR_API_KEY=your_other_vendor_key

Then the compose file. Pinned to an exact version, so upgrades are a choice.

compose.yaml

services:
  otelcol:
    image: otel/opentelemetry-collector-contrib:0.157.0
    restart: unless-stopped
    env_file: .env
    volumes:
      # The contrib image reads this exact path. See the warning below.
      - ./collector.yaml:/etc/otelcol-contrib/config.yaml:ro
    ports:
      - "4318:4318"                  # OTLP/HTTP — Claude Code and Cowork
      - "4317:4317"                  # OTLP/gRPC — anything else
      - "127.0.0.1:8888:8888"       # its own metrics, loopback only
Mount to /etc/otelcol-contrib/config.yaml — with the -contrib. Plain otelcol uses /etc/otelcol/config.yaml, and using that path here is the most common mistake with this image: the collector finds nothing, falls back to its baked-in default, and starts perfectly healthy — exporting nothing, anywhere, with no hint in the log.

Validate first — this parses the file, resolves the env vars, and exits without starting anything:

docker run --rm --env-file .env \
  -v "$PWD/collector.yaml:/etc/otelcol-contrib/config.yaml:ro" \
  otel/opentelemetry-collector-contrib:0.157.0 \
  validate --config=/etc/otelcol-contrib/config.yaml

No output means valid. Bring it up:

docker compose up -d
docker compose logs -f otelcol

Look for Everything is ready. Begin running and processing data. The startup log lists the pipelines it built — check both logs and traces are there.

Note: the image is FROM scratch with no shell, so docker compose exec otelcol sh fails. Expected — debug with validate above and the metrics in step 7.

Reachability

  • Hostname: stable, behind your TLS terminator — e.g. https://otel.example.com. Plain http:// on a private network works, but laptops off the VPN stop reporting.
  • Open 4318 inbound from wherever developers work — that's the port Claude Code and Cowork use.
  • Keep 8888 private. No telemetry content, but it reveals your traffic volumes.

Step 6: point Claude at it

Send Claude Code and Cowork to your collector

Same as Connect Claude to SightLift, with two differences: the endpoint is your collector, and there's no Authorization header — your collector holds the credentials now.

Navigate to
claude.ai Organization settings Claude Code Managed settings Manage

Replace otel.example.com with your hostname. If your org already has managed settings, merge the env block in rather than replacing the file.

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_TRACES_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.example.com:4318",
    "OTEL_LOG_USER_PROMPTS": "1",
    "OTEL_LOG_ASSISTANT_RESPONSES": "1",
    "OTEL_LOG_TOOL_DETAILS": "1",
    "OTEL_LOG_TOOL_CONTENT": "1"
  }
}

No path on that endpoint. OTEL_EXPORTER_OTLP_ENDPOINT is the generic form — the exporter appends /v1/logs and /v1/traces itself. (The other guide uses the signal-specific variables, which are taken verbatim.)

Those OTEL_LOG_* flags are the difference between a chart and an automation. Off by default, and this is the only place to switch them on — whatever is off here never reaches your collector, so it can never be forwarded. Enable per team as your security review allows. What each unlocks: data access & privacy.

Cowork

Under Organization settings → Cowork → Monitoring: OTLP endpoint https://otel.example.com:4318, protocol http/json, leave headers and resource attributes blank. Cowork allowlists your endpoint for its sandbox on save and says so under the field.

Step 7: prove it

Confirm both destinations are receiving

Run a Claude Code session on a machine that has the new settings — they land at sign-in, so sign out and back in first. Then ask the collector what it did:

curl -s localhost:8888/metrics | grep -E 'otelcol_(receiver_accepted|exporter_sent|exporter_send_failed)'

Read it in this order:

MetricWhat it tells you
otelcol_receiver_accepted_log_records
otelcol_receiver_accepted_spans
Claude is reaching the collector. Both should climb — spans as well as logs.
otelcol_exporter_sent_log_records
otelcol_exporter_sent_spans
Delivery, per exporter. Expect a separate rising series for otlphttp/sightlift and otlphttp/vendor.
otelcol_exporter_send_failed_log_records
otelcol_exporter_send_failed_spans
Should stay zero. The exporter label tells you whose credentials to check.

The per-exporter breakdown is the point: "SightLift is receiving" and "my vendor is still receiving" become two separate facts, so you never take the fan-out on faith or find out months later that one side stopped.

Once sent climbs for otlphttp/sightlift on both signals you're done — activity appears within the hour. No backfill; it starts from now.

Troubleshooting

Nothing showing up? Find the hop that broke

"No data in SightLift" looks identical whichever hop failed, so work down this table rather than changing things at random. The metrics from step 7 tell you which row you're in.

What you seeWhere it brokeWhat to check
receiver_accepted flat at zero Claude → collector Settings land at sign-in — sign out and in. Then: endpoint reachable from that machine, 4318 open, and no path left on OTEL_EXPORTER_OTLP_ENDPOINT.
Logs arriving, spans at zero source flags Needs CLAUDE_CODE_ENHANCED_TELEMETRY_BETA + OTEL_TRACES_EXPORTER set, and a traces pipeline in your config.
send_failed climbing on otlphttp/sightlift collector → SightLift Almost always the header — SIGHTLIFT_INGEST_KEY must be the key alone. If only metrics fail, remove us from that pipeline.
Everything healthy, still nothing in SightLift the key The key identifies your org — one from another tenant delivers successfully somewhere you can't see. Reissue from Live telemetry.
Data arriving, prompts read <REDACTED> source flags OTEL_LOG_USER_PROMPTS is off. No collector change fixes this.
Collector healthy, exporting nothing at all the config mount It's on its baked-in default config. Mount to /etc/otelcol-contrib/config.yaml — with the -contrib.

Still stuck? Send the step-7 curl output and your collector.yaml (keys removed) to support@sightlift.ai.

Optional

Send different things to different destinations

Each destination can get a different version of the same stream. The usual reason: we need prompt and tool content to classify work and verify automations; your observability vendor needs volumes and latencies, not your engineers' prompts.

No routing component needed — give each destination its own pipeline off the same receiver, and put the processors only where they belong:

collector.yaml — replacing the pipelines block

processors:
  # ... memory_limiter and batch as before ...

  # Strip conversation content. Applied ONLY to the vendor pipeline below.
  redaction/strip_content:
    allow_all_keys: true        # keep every other attribute as-is
    blocked_key_patterns:
      - ".*prompt.*"
      - ".*tool_parameters.*"

service:
  pipelines:
    # Full fidelity to SightLift.
    logs/sightlift:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/sightlift]

    # Same records, content stripped, to your vendor.
    logs/vendor:
      receivers: [otlp]
      processors: [memory_limiter, redaction/strip_content, batch]
      exporters: [otlphttp/vendor]

    # Traces split the same way. Keep BOTH — see step 2.
    traces/sightlift:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/sightlift]
    traces/vendor:
      receivers: [otlp]
      processors: [memory_limiter, redaction/strip_content, batch]
      exporters: [otlphttp/vendor]

One receiver can feed any number of pipelines, each getting its own copy. The logs/sightlift naming is just how the Collector tells two pipelines of the same signal apart.

Test your patterns. Point a copy at the debug exporter and read what comes out — attribute names differ between Claude Code and Cowork, and a pattern matching nothing fails silently. To cut whole records rather than edit fields, filter is simpler.

Redact toward your vendor, not toward us. On the SightLift pipelines these processors strip exactly what classification and capability verification run on — and with no backfill, work done while it was stripped is unrecoverable. If a review says content can't leave your network at all, talk to us: that's a conversation about metadata-only, not a config change.

Optional

Don't lose data when the collector restarts

Send queues live in memory by default, so a restart while a destination is down drops whatever was waiting — and with no backfill, it's gone. Put the queue on disk (this is what SightLift runs on its own ingest collector):

extensions:
  file_storage/queue:
    directory: /var/lib/otelcol/storage
    timeout: 10s

exporters:
  otlphttp/sightlift:
    # ... endpoints and headers as before ...
    sending_queue:
      enabled: true
      storage: file_storage/queue
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 300s

service:
  extensions: [file_storage/queue]      # declaring it is not enough — list it here
  # ... pipelines as before ...

Then give it somewhere to write, owned by the container's unprivileged user:

mkdir -p ./otelcol-queue
sudo chown 10001:10001 ./otelcol-queue

And mount it alongside the config:

    volumes:
      - ./collector.yaml:/etc/otelcol-contrib/config.yaml:ro
      - ./otelcol-queue:/var/lib/otelcol/storage
The chown 10001 is not optional. The image runs as UID 10001; a fresh directory is owned by root. Skip it and the collector fails to start on a permission error — loudly, unlike most traps here.

Reference

Notes and other environments

Kubernetes

The official Helm chart open-telemetry/opentelemetry-collector with mode: deployment is the same gateway pattern; put the collector.yaml above under the chart's config: key. Note the chart leaves image.repository empty, so set it to otel/opentelemetry-collector-contrib explicitly. Tell us if you'd like a hand — we can walk through your setup.

Upstream documentation