> For the complete documentation index, see [llms.txt](https://docs.ionos.com/cloud/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ionos.com/cloud/tutorials/observability/monitoring-service/monitor-claude-code-with-monitoring-service.md).

# Monitor Claude Code Usage and Cost with the Monitoring Service

## Overview

Claude Code is a command-line coding agent from Anthropic. It can export OpenTelemetry data about its own activity, including how many tokens each model consumed, what each session cost, how long developers spent working with it, and how many lines of code it changed.

Since OpenTelemetry Protocol (OTLP) ingestion became available in August 2026, the <code class="expression">space.vars.ionos\_cloud</code> **Monitoring Service** accepts this data directly. OpenTelemetry Collector and translation layers are not required. Connect Claude Code's exporter to your pipeline to start receiving metrics.

Claude Code emits each OpenTelemetry signal separately, so each one can be sent to a different <code class="expression">space.vars.ionos\_cloud</code> observability service:

| **Signal** | **Service**        | **What it gives you**                                                             |
| ---------- | ------------------ | --------------------------------------------------------------------------------- |
| Metrics    | Monitoring Service | Token usage, cost per model, sessions, and active time.                           |
| Traces     | Tracing Service    | A span for each interaction, showing every model request and tool call within it. |

Metrics aggregate usage over time, while traces record individual interactions, so the two answer different questions. Steps 1 to 7 cover metrics, including a dashboard you can import. Steps 8 to 10 cover traces and are optional.

## Target audience

Platform and DevOps engineers, and engineering leads who need visibility over AI coding assistant usage and cost, and who want that data stored on European infrastructure they control.

## What you will learn

You will learn how to:

* Configure Claude Code's OpenTelemetry exporter for the <code class="expression">space.vars.ionos\_cloud</code> Monitoring Service.
* Confirm that metrics arrived, and diagnose it when they have not.
* Import a Grafana dashboard for token usage and cost.
* Control label cardinality, which drives what you are billed for.
* Handle the personal data that Claude Code attaches to every metric.

## Before you begin

Ensure you have:

* An active <code class="expression">space.vars.ionos\_cloud</code> account. Contract owners and administrators already have the necessary access. Any other user needs the **Access and manage Monitoring** privilege. For more information, see [<mark style="color:blue;">Set User Privileges</mark>](https://docs.ionos.com/cloud/observability/monitoring-service/dcd-how-tos/set-privileges-monitoring-service).
* A monitoring pipeline, together with its HTTP endpoint and API key. For more information, see [<mark style="color:blue;">Monitoring Service User Guide</mark>](https://docs.ionos.com/cloud/observability/monitoring-service).
* Access to the Grafana endpoint of the respective pipeline. Monitoring privileges govern pipeline access only and do not grant Grafana access.
* Claude Code 2.x installed on macOS or Linux. For more information, see [<mark style="color:blue;">Claude Code Documentation</mark>](https://code.claude.com/docs).

## Cost considerations

This tutorial uses an existing <code class="expression">space.vars.ionos\_cloud</code> Monitoring Service subscription. The Monitoring Service bills ingested samples and stored data, so sending Claude Code metrics adds to your usage.

Claude Code emits eight metrics. At the default 60-second export interval, a single active developer generates tens of thousands of samples daily, meaning telemetry costs scale linearly with your team size.

Two settings affect the volume more than the number of developers does:

* **Label cardinality:** Each distinct session identifier creates a new time series. Step 6 shows how to switch the label off.
* **Export interval:** Halving `OTEL_METRIC_EXPORT_INTERVAL` doubles the number of samples.

For pricing information, see [<mark style="color:blue;">Monitoring Service Billing</mark>](https://docs.ionos.com/cloud/observability/monitoring-service/billing) and [<mark style="color:blue;">IONOS CLOUD Prices</mark>](https://cloud.ionos.com/prices).

## Architecture

Claude Code on your machine exports two OpenTelemetry signals directly, without an intermediate collector. Metrics travel over OTLP to the <code class="expression">space.vars.ionos\_cloud</code> Monitoring pipeline and into the Monitoring Service, which stores them in Grafana Mimir. Traces travel over OTLP to the <code class="expression">space.vars.ionos\_cloud</code> Tracing pipeline and into the Tracing Service, which stores them in Grafana Tempo. Both paths converge on Grafana, which queries the Monitoring Service with PromQL and the Tracing Service with TraceQL.

![Architecture of Claude Code exporting metrics and traces](https://3040852435-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FEpuEvuLJIyhyeRGhmrv1%2Fuploads%2Fgit-blob-2979f9985cef7c1021c1d0820e1c765d8fbff43e%2Ftutorial-monitor-claude-code-architecture.png?alt=media)

Each OpenTelemetry signal is exported independently, with its own endpoint, protocol, and authentication setting. That is what allows one Claude Code installation to send each signal to a different <code class="expression">space.vars.ionos\_cloud</code> service.

## Procedure

{% stepper %}
{% step %}

### Collect the pipeline endpoint

In the Data Center Designer (DCD), open your monitoring pipeline and note the HTTP endpoint. The OTLP metrics path is appended to it:

```
https://<pipeline-id>-metrics.<tenant>.monitoring.<region>.ionos.com/otlp/v1/metrics
```

{% hint style="warning" %}
**Important:** The metrics path is `/otlp/v1/metrics`, not `/v1/metrics`. Because it differs from the OpenTelemetry default, you must set the metrics-specific endpoint variable in step 3. The generic `OTEL_EXPORTER_OTLP_ENDPOINT` variable appends `/v1/metrics` and does not reach the Monitoring Service.
{% endhint %}
{% endstep %}

{% step %}

### Store the API key

1\. Copy the pipeline API key and export it, so that it never appears in a settings file or in your shell history:

```bash
export IONOS_MONITORING_APIKEY="$(pbpaste | tr -d '\n\r')"
```

On Linux, use `xclip -o` in place of `pbpaste`.

2\. Confirm the value arrived without printing it:

```bash
echo "key length: ${#IONOS_MONITORING_APIKEY}"
```

{% hint style="warning" %}
**Warning:** Handle the pipeline key like a password. The Monitoring Service returns it only once, when the pipeline is created. Store it in a password manager, and do not paste it into files, chats, tickets, or version control. If it is exposed, rotate the pipeline key to generate a new one.
{% endhint %}
{% endstep %}

{% step %}

### Configure Claude Code to export metrics

Set these variables in the terminal that you will start Claude Code from:

```bash
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=none
export OTEL_TRACES_EXPORTER=none

export OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="https://<pipeline-id>-metrics.<tenant>.monitoring.<region>.ionos.com/otlp/v1/metrics"
export OTEL_EXPORTER_OTLP_METRICS_HEADERS="apikey=$IONOS_MONITORING_APIKEY"
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative
export OTEL_METRIC_EXPORT_INTERVAL=60000
export OTEL_RESOURCE_ATTRIBUTES="service.name=claude-code,team=platform"
```

Two of these settings are often overlooked, and misconfiguring either one will prevent any data from arriving:

| **Setting**                                                    | **Why it is required**                                                                                                                                   |
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf`            | OpenTelemetry defaults to gRPC Remote Procedure Call (gRPC). The Monitoring Service endpoint is an HTTPS path, so gRPC fails without reporting an error. |
| `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative` | Claude Code sends delta values by default. Grafana Mimir expects cumulative counters.                                                                    |
| {% endstep %}                                                  |                                                                                                                                                          |

{% step %}

### Start Claude Code and generate activity

1\. Start Claude Code from the same terminal:

```bash
claude
```

Claude Code reads environment variables only at startup, so ensure the exports above are set before starting it.

2\. Send a few prompts to generate some reportable activity.

{% hint style="info" %}
**Note:** To avoid re-exporting the non-secret variables in every terminal, move them into an `env` block in `~/.claude/settings.json`. Values must be strings, and Claude Code must be restarted after you edit the file. Do not store `OTEL_EXPORTER_OTLP_METRICS_HEADERS` in this file, as it contains your API key and is saved in plain text. In team environments, use the `otelHeadersHelper` hook to fetch the credential at runtime instead.
{% endhint %}
{% endstep %}

{% step %}

### Confirm the metrics arrived

Open the Grafana endpoint of your pipeline, go to **Explore**, select the Mimir data source, switch to **Code** mode, select **Instant**, and run:

```
group by (__name__) ({__name__=~"claude_code.*"})
```

The following metric names appear:

```
claude_code_session_count
claude_code_token_usage
claude_code_cost_usage
claude_code_active_time_total
```

Metric names are rewritten on ingestion, so that dots become underscores. Query `claude_code_token_usage` rather than `claude_code.token.usage`.

Four further metrics (`claude_code_lines_of_code_count`, `claude_code_commit_count`, `claude_code_pull_request_count`, and `claude_code_code_edit_tool_decision`) appear only once a session edits files, commits, or asks you to approve a tool. Their absence before that point is expected.
{% endstep %}

{% step %}

### Review the labels

To review the labels received, set **Format** to `Table` and query the metric name on its own:

```
claude_code_token_usage
```

{% hint style="warning" %}
**Warning:** While you are signed in, Claude Code attaches `user.email`, `organization.id`, and other account identifiers to every metric series. Because these become labels in Grafana Mimir, your metric storage will contain Personally Identifiable Information (PII). Currently, no environment variable can remove the `user.email` label.

Check your organization's policies before using this beyond a single machine. Deleting individual series from a metrics store is operationally difficult, so decide before you ingest rather than afterwards. If you need the data without the identifier, route it through an OpenTelemetry Collector and drop or hash the attribute there. For more information, see \[[<mark style="color:blue;">Next steps</mark>](#next-steps).
{% endhint %}

{% hint style="info" %}
**Note:** Session and account identifiers are included by default, creating a new time series for every session and user. While manageable on a single machine, this metric volume grows uncontrollably across a team, driving up costs since the Monitoring Service bills per ingested sample. To reduce it, set the following environment variables and restart Claude Code:

```bash
export OTEL_METRICS_INCLUDE_SESSION_ID=false
export OTEL_METRICS_INCLUDE_ACCOUNT_UUID=false
```

To check the current series count, run `count(count by (session_id) (claude_code_token_usage))`.
{% endhint %}
{% endstep %}

{% step %}

### Import the dashboard

In Grafana, go to **Dashboards** > **New** > **Import**, paste the following definition, and select your Mimir data source when prompted.

<details>

<summary><strong>Dashboard definition</strong></summary>

```json
{
  "__inputs": [
    {
      "name": "DS_MONITORING",
      "label": "Monitoring Service",
      "description": "The Mimir data source of your monitoring pipeline",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "title": "Claude Code usage and cost",
  "description": "Token usage, cost per model, sessions, and active time reported by Claude Code",
  "tags": [
    "claude-code",
    "opentelemetry"
  ],
  "schemaVersion": 39,
  "editable": true,
  "time": {
    "from": "now-7d",
    "to": "now"
  },
  "refresh": "5m",
  "panels": [
    {
      "type": "stat",
      "title": "Total cost",
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_MONITORING}"
      },
      "gridPos": {
        "h": 5,
        "w": 6,
        "x": 0,
        "y": 0
      },
      "fieldConfig": {
        "defaults": {
          "unit": "currencyUSD",
          "decimals": 2
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "expr": "sum(max_over_time(claude_code_cost_usage[$__range]))",
          "instant": true
        }
      ]
    },
    {
      "type": "stat",
      "title": "Sessions",
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_MONITORING}"
      },
      "gridPos": {
        "h": 5,
        "w": 6,
        "x": 6,
        "y": 0
      },
      "fieldConfig": {
        "defaults": {
          "unit": "short",
          "decimals": 0
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "expr": "count(count by (session_id) (claude_code_session_count))",
          "instant": true
        }
      ],
      "description": "Distinct sessions in the selected range. Requires the session identifier label."
    },
    {
      "type": "stat",
      "title": "Active time",
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_MONITORING}"
      },
      "gridPos": {
        "h": 5,
        "w": 6,
        "x": 12,
        "y": 0
      },
      "fieldConfig": {
        "defaults": {
          "unit": "s",
          "decimals": 0
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "expr": "sum(max_over_time(claude_code_active_time_total[$__range]))",
          "instant": true
        }
      ]
    },
    {
      "type": "gauge",
      "title": "Reused input token share",
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_MONITORING}"
      },
      "description": "Share of input tokens reused instead of sent again. Higher is cheaper.",
      "gridPos": {
        "h": 5,
        "w": 6,
        "x": 18,
        "y": 0
      },
      "fieldConfig": {
        "defaults": {
          "unit": "percentunit",
          "min": 0,
          "max": 1,
          "decimals": 2
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "instant": true,
          "expr": "sum(max_over_time(claude_code_token_usage{type=\"cacheRead\"}[$__range])) / clamp_min(sum(max_over_time(claude_code_token_usage[$__range])), 1)"
        }
      ]
    },
    {
      "type": "timeseries",
      "title": "Cost by model",
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_MONITORING}"
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 5
      },
      "fieldConfig": {
        "defaults": {
          "unit": "currencyUSD",
          "custom": {
            "fillOpacity": 20,
            "stacking": {
              "mode": "normal"
            }
          }
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "expr": "sum by (model) (increase(claude_code_cost_usage[$__rate_interval]))",
          "legendFormat": "{{model}}"
        }
      ]
    },
    {
      "type": "timeseries",
      "title": "Tokens by type",
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_MONITORING}"
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 12,
        "y": 5
      },
      "fieldConfig": {
        "defaults": {
          "unit": "short",
          "custom": {
            "fillOpacity": 20,
            "stacking": {
              "mode": "normal"
            }
          }
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "expr": "sum by (type) (increase(claude_code_token_usage[$__rate_interval]))",
          "legendFormat": "{{type}}"
        }
      ]
    },
    {
      "type": "timeseries",
      "title": "Tokens by model",
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_MONITORING}"
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 13
      },
      "fieldConfig": {
        "defaults": {
          "unit": "short",
          "custom": {
            "fillOpacity": 20,
            "stacking": {
              "mode": "normal"
            }
          }
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "expr": "sum by (model) (increase(claude_code_token_usage[$__rate_interval]))",
          "legendFormat": "{{model}}"
        }
      ]
    },
    {
      "type": "bargauge",
      "title": "Total cost by model",
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_MONITORING}"
      },
      "gridPos": {
        "h": 8,
        "w": 6,
        "x": 12,
        "y": 13
      },
      "options": {
        "displayMode": "gradient",
        "orientation": "horizontal"
      },
      "fieldConfig": {
        "defaults": {
          "unit": "currencyUSD",
          "decimals": 2
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "expr": "sum by (model) (max_over_time(claude_code_cost_usage[$__range]))",
          "legendFormat": "{{model}}",
          "instant": true
        }
      ]
    },
    {
      "type": "stat",
      "title": "Cost per session",
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_MONITORING}"
      },
      "gridPos": {
        "h": 8,
        "w": 6,
        "x": 18,
        "y": 13
      },
      "fieldConfig": {
        "defaults": {
          "unit": "currencyUSD",
          "decimals": 3
        },
        "overrides": []
      },
      "targets": [
        {
          "refId": "A",
          "expr": "sum(max_over_time(claude_code_cost_usage[$__range])) / clamp_min(count(count by (session_id) (claude_code_session_count)), 1)",
          "instant": true
        }
      ],
      "description": "Total cost in the range divided by the number of distinct sessions."
    },
    {
      "type": "row",
      "title": "Code activity",
      "collapsed": true,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 21
      },
      "panels": [
        {
          "type": "timeseries",
          "title": "Lines of code by type",
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_MONITORING}"
          },
          "gridPos": {
            "h": 8,
            "w": 12,
            "x": 0,
            "y": 22
          },
          "fieldConfig": {
            "defaults": {
              "unit": "short",
              "custom": {
                "fillOpacity": 20,
                "stacking": {
                  "mode": "normal"
                }
              }
            },
            "overrides": []
          },
          "targets": [
            {
              "refId": "A",
              "expr": "sum by (type) (increase(claude_code_lines_of_code_count[$__rate_interval]))",
              "legendFormat": "{{type}}"
            }
          ]
        },
        {
          "type": "stat",
          "title": "Commits",
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_MONITORING}"
          },
          "gridPos": {
            "h": 8,
            "w": 6,
            "x": 12,
            "y": 22
          },
          "fieldConfig": {
            "defaults": {
              "unit": "short",
              "decimals": 0
            },
            "overrides": []
          },
          "targets": [
            {
              "refId": "A",
              "expr": "sum(max_over_time(claude_code_commit_count[$__range]))",
              "instant": true
            }
          ]
        },
        {
          "type": "bargauge",
          "title": "Edit suggestions by decision",
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_MONITORING}"
          },
          "gridPos": {
            "h": 8,
            "w": 6,
            "x": 18,
            "y": 22
          },
          "options": {
            "displayMode": "gradient",
            "orientation": "horizontal"
          },
          "fieldConfig": {
            "defaults": {
              "unit": "short",
              "decimals": 2
            },
            "overrides": []
          },
          "targets": [
            {
              "refId": "A",
              "expr": "sum by (decision) (max_over_time(claude_code_code_edit_tool_decision[$__range]))",
              "legendFormat": "{{decision}}",
              "instant": true
            }
          ]
        }
      ]
    }
  ]
}
```

</details>

Once a few sessions have been reported, the imported Grafana dashboard displays Claude Code usage and costs over the last seven days. Four stat panels across the top show total cost, session count, active time, and reused input token share. Below these sit time-series charts detailing cost per model, a model breakdown bar gauge, and a cost-per-session stat.

![The Claude Code usage and cost dashboard in Grafana](https://3040852435-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FEpuEvuLJIyhyeRGhmrv1%2Fuploads%2Fgit-blob-f228f56ce98e3fd39f57bc81b13f2dbc237e9e80%2Ftutorial-monitor-claude-code-dashboard.png?alt=media)

The **Reused input token share** panel is the most actionable. Reused input tokens bill at a fraction of fresh ones, so a low share on a long-running project points to a prompt that changes more often than necessary.

The **Code activity** row is collapsed by default because its metrics populate only after Claude Code edits files, makes a commit, or requests your approval. You can expand it once you have completed a few tasks.

The stat panels use `max_over_time` rather than `increase`. Because each session reports its own counter series, a counter that reaches its final value and stops updating will evaluate to an increase of zero. This causes the panel to appear empty even when data exists.
{% endstep %}

{% step %}

### Create a tracing pipeline

{% hint style="info" %}
**Note:** Steps 8 to 10 are optional. The metrics above aggregate usage over time. Traces record each interaction on its own, including the model requests inside it and the tool calls that those requests triggered. Use them to see what one particular interaction did, rather than what a week of them cost.
{% endhint %}

{% hint style="info" %}
**Prerequisite:** These steps use a second service, so they need the **Access and manage Tracing** privilege. It is granted separately from the monitoring one.
{% endhint %}

1\. Create a tracing pipeline with the protocol set to `otlp-http`, which matches the `http/protobuf` setting used in the next step. Do not select a gRPC pipeline. A gRPC endpoint consists of a host and a port without a path, so it cannot accept the endpoint format shown below. For more information, see [<mark style="color:blue;">Tracing Service User Guide</mark>](https://docs.ionos.com/cloud/observability/tracing-service).

2\. Note the HTTP endpoint of the pipeline, then store its API key:

```bash
export IONOS_TRACING_APIKEY="$(pbpaste | tr -d '\n\r')"
echo "key length: ${#IONOS_TRACING_APIKEY}"
```

{% hint style="warning" %}
**Warning:** As with the monitoring pipeline, the Tracing Service returns the ingestion key only once. Store it in your password manager. If it is exposed, rotate the pipeline key to generate a new one.
{% endhint %}
{% endstep %}

{% step %}

### Configure Claude Code to export traces

1\. Tracing is a beta feature of Claude Code and requires an extra variable. Configure as follows:

```bash
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://<pipeline-id>-traces.<tenant>.tracing.<region>.ionos.com/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="apikey=$IONOS_TRACING_APIKEY"
```

2\. Restart Claude Code. Ask it to read a file so that the trace contains both a tool call and model requests.

{% hint style="info" %}
**Note:** Availability of the Claude Code tracing beta depends on your Claude plan. This beta applies to Claude Code only. The <code class="expression">space.vars.ionos\_cloud</code> Tracing Service is generally available. To check the Claude Code feature without involving the pipeline, run the following and look for printed spans:

```bash
CLAUDE_CODE_ENABLE_TELEMETRY=1 CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 \
OTEL_TRACES_EXPORTER=console claude -p "say hi"
```

{% endhint %}
{% endstep %}

{% step %}

### View an interaction as a trace

1\. In Grafana, select the Tempo data source and run the following in the **TraceQL** tab:

```
{ resource.service.name = "claude-code" }
```

2\. The **Search** tab produces the same result without any syntax. Set **Service Name** to `claude-code`.

3\. Open a trace. It has the following shape:

```
claude_code.interaction
├─ claude_code.llm_request
├─ claude_code.tool
│  ├─ claude_code.tool.blocked_on_user
│  └─ claude_code.tool.execution
└─ claude_code.llm_request
```

A real interaction repeats the pattern as many times as the task requires. The following trace shows a single prompt that took 11.08 seconds and generated 12 spans, alternating between model requests and tool calls. A 9.9-second parent interaction span contains five model request spans, the longest running for 5.45 seconds, and two tool spans. Each tool span includes separate child spans for approval wait time and execution.

![A Claude Code trace in Grafana Tempo](https://3040852435-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FEpuEvuLJIyhyeRGhmrv1%2Fuploads%2Fgit-blob-0a3f92ab78a17d1356163b376778fa22d5169c6c%2Ftutorial-monitor-claude-code-trace.png?alt=media)

The waterfall reveals details that metrics cannot capture. Here, model requests dominate elapsed time, tool execution is brief, and the tiny `blocked_on_user` spans of 6 and 2.21 milliseconds prove that the interaction did not wait for human approval.

Each `claude_code.llm_request` span records one call, with `model`, `input_tokens`, `output_tokens`, `duration_ms`, `ttft_ms`, `stop_reason`, and the counts of tokens reused from earlier requests. Each `claude_code.tool.blocked_on_user` span records how long Claude Code waited for a tool call approval, which no metric reports.

Resource attributes reach the spans, so the `team` value set in `OTEL_RESOURCE_ATTRIBUTES` is available as a span attribute and can be used to filter traces.

{% hint style="warning" %}
**Warning:** Spans carry the same identifying attributes as metrics, including `user.email`, `organization.id`, and the account identifiers. Prompt text is redacted by default and appears as `<REDACTED>`, with only its length recorded. Read the guidance in step 6 before you send traces from more than one machine.
{% endhint %}
{% endstep %}
{% endstepper %}

## Result

Claude Code activity from your machine is stored in your <code class="expression">space.vars.ionos\_cloud</code> monitoring pipeline, and the imported dashboard reports total cost, session count, active time, the share of input tokens that were reused, cost broken down by model, and token usage broken down by type. If you completed the optional steps, you can also open an individual interaction as a trace. The trace shows every model request and tool call within it.

## Troubleshooting

**No metrics appear in Grafana**

The arrival of data is the only reliable confirmation that the configuration works, so start there. Run the following over the last hour, open a session, send a prompt, and wait for one export interval:

```
count(count by (session_id) (claude_code_session_count))
```

If the count rises, the configuration is correct. If it does not, work through the following checks in order, because each one isolates a different stage.

1. **Confirm that the configuration reached Claude Code.** Asking Claude Code from inside a session does not work, because `OTEL_*` variables are deliberately removed from child processes. A session cannot read its own exporter settings and reports them as empty even when they are set.

   How to check depends on where you put the configuration.

   If you exported the variables in a shell, inspect the environment the process started with:

   ```bash
   pgrep -fl claude
   ps eww -p <PID> | tr ' ' '\n' | grep -E '^(OTEL_|CLAUDE_CODE_)'
   ```

   Variables set after Claude Code started do not apply to it, so restart it if the list is incomplete.

   If you used the `env` block in `~/.claude/settings.json`, this command returns nothing, which is expected. Those values are read by Claude Code and never enter the process environment. Confirm the file instead, and restart Claude Code after any edit:

   ```bash
   python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.claude/settings.json'))).get('env'))"
   ```

   Do not mix the two. A shell export takes precedence over the `env` block, so a stale export in one terminal sends that session somewhere else.
2. **Confirm that Claude Code produces metrics at all.** Run the following in a separate terminal. Metric records printed to the terminal mean that the problem lies in transport rather than collection.

   ```bash
   CLAUDE_CODE_ENABLE_TELEMETRY=1 OTEL_METRICS_EXPORTER=console claude -p "say hi"
   ```
3. **Check the protocol.** If `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` is unset, OpenTelemetry uses gRPC, which cannot reach an HTTPS path endpoint. Set it to `http/protobuf`.
4. **Check the endpoint path.** It must end in `/otlp/v1/metrics`.
5. **Check the region.** If you have pipelines in more than one region, confirm that you are querying the Grafana instance belonging to the same pipeline you are sending to.

**Metrics appear but the values look incorrect**

Confirm that `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` is set to `cumulative`. With delta values, `rate()` and `increase()` produce meaningless results.

**A query returns `vector cannot contain metrics with the same labelset`**

Functions such as `count_over_time` drop the metric name, which makes the `_sum` and `_count` series of a histogram indistinguishable. Aggregate one metric at a time instead of using a wildcard selector.

## Decommission resources

To stop the export, unset the variables and restart Claude Code:

```bash
unset CLAUDE_CODE_ENABLE_TELEMETRY
unset $(env | grep -o '^OTEL_[A-Z_]*' | tr '\n' ' ')
```

Remove any `env` block that you added to `~/.claude/settings.json`.

Metrics that have already been ingested remain until the retention period of the pipeline expires. To stop all charges, delete the pipeline in the DCD.

## Next steps

* **Route through an OpenTelemetry Collector for team rollouts.** This gives developers a single endpoint, centralises credential management, and lets you remove identifying attributes before anything is stored:

  ```yaml
  processors:
    attributes/redact:
      actions:
        - key: user.email
          action: hash
        - key: user.account_uuid
          action: delete
  ```

  Hashing rather than deleting preserves per-developer grouping for cost attribution without storing the address itself.
* **Add the activity metrics.** Once sessions start editing files and committing, `claude_code_lines_of_code_count`, `claude_code_commit_count`, and `claude_code_code_edit_tool_decision` become available. Add panels for accepted and rejected edit suggestions.
* **Alert on cost.** Create a Grafana alert on `sum(increase(claude_code_cost_usage[24h]))` to catch unexpected spend.
* **Reduce cardinality before scaling up.** Revisit `OTEL_METRICS_INCLUDE_SESSION_ID` and `OTEL_METRICS_INCLUDE_ACCOUNT_UUID` once more than a few developers are reporting.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ionos.com/cloud/tutorials/observability/monitoring-service/monitor-claude-code-with-monitoring-service.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
