> ## Documentation Index
> Fetch the complete documentation index at: https://docs.costgraph.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Usage

> Enable request header attribution, send tagged requests, and read the results

This page covers the client requirements, enabling capture, how header names
become tag keys, and how to read the results.

## Requirements

A request is attributed only when all of the following are true:

* **The instrumented process is the HTTP client.** Attribution is counted on the
  side that sends the request. A server that receives tagged requests does not
  attribute them.
* **The request carries at least one non-empty `X-CostGraph-<key>` header.**
  Requests without one are not attributed.
* **The protocol is HTTP/1.1 or HTTP/2 over TCP.** HTTP/3 (QUIC over UDP) is not
  supported.
* **The TLS stack is supported.** This means Go's `crypto/tls`, or a dynamically
  loaded `libssl.so` that exports `SSL_read` and `SSL_write`. See
  [Runtime compatibility](#runtime-compatibility).
* **The client runs on a Linux node the agent monitors** and is visible through
  the host's `/proc`.
* **The process resolves to a container.** Requests from processes running
  directly on the host are dropped unless
  [host-flow collection](#host-processes) is enabled.
* **Every proxy hop you want to attribute preserves the header.** See
  [Proxies and sidecars](#proxies-and-sidecars).

## Runtime compatibility

| Typical client | Expected result |
| - | - |
| Go `net/http` using `crypto/tls` | ✅ |
| Go HTTP/2 | ✅ |
| curl/libcurl with dynamic OpenSSL | ✅ |
| Python `requests` / `httpx` with dynamic OpenSSL | ✅ |
| Ruby/PHP with dynamic OpenSSL | ✅ |
| nginx with dynamic OpenSSL | ✅ for header capture |
| Bun compiled executable | ❌ |
| Node with statically bundled OpenSSL | ❌ with our current capture API |
| Deno | ❌ |
| Rust using `rustls` | ❌ |
| Java JSSE | ❌ currently |
| Static/musl executable bundling OpenSSL | ❌ |
| Envoy/Istio with bundled BoringSSL | ❌ currently |
| Linkerd/rustls sidecar | ❌ currently |

The rule behind the table: CostGraph needs a TLS library it can hook. Node, Bun, Deno, and static or musl builds compile their TLS into the binary.
`rustls`, Java JSSE, and BoringSSL are TLS stacks CostGraph doesn't hook yet.

To check whether a binary links OpenSSL dynamically, run
`ldd /path/to/binary | grep libssl`. A `libssl.so` line in the output means the
library can be hooked.

## Enable capture

Per-request capture runs in the network metrics scraper. Where you turn it on
depends on how you run CostGraph:

| | Setting | Default |
| - | - | - |
| CostGraph operator chart | `flowtrace.config.perRequestCollector` | `false` |
| CostGraph agent | `--flowtrace-per-request-collector` | `true` |

<Tabs>
  <Tab title="Operator">
    In the operator chart's values, set `perRequestCollector` on the `flowtrace`
    component:

    ```yaml theme={null}
    flowtrace:
      enabled: true
      config:
        perRequestCollector: true
    ```

    The `flowtrace` DaemonSet already mounts the host's `/proc` at `/host/proc`, so it
    can see client processes. See
    [Network Metrics scraper](/costgraph/operator/configuration#network-metrics-scraper)
    for its other settings.
  </Tab>

  <Tab title="Agent">
    The [CostGraph agent](/costgraph/agent) captures per request by default. To set
    it explicitly in the `costgraph-agent` Helm chart:

    ```yaml theme={null}
    costgraph:
      agent:
        flowtracePerRequestCollector: true
    ```

    The chart runs the agent with the host PID namespace while this is on, so it can
    see client processes. If you run the agent without the chart, pass
    `--flowtrace-per-request-collector=true`.
  </Tab>
</Tabs>

### Host processes

By default, requests from processes that don't belong to a container are
dropped. To attribute traffic from processes running directly on the node, also
enable host-flow collection: `flowtrace.config.includeHostFlows: true` in the
operator chart, or `--flowtrace-include-host-flows` on the agent.

## Send tagged requests

Add one or more `X-CostGraph-<key>` headers to the outgoing requests you want to
attribute. The part after `X-CostGraph-` is the tag key, and the header value is
the tag value.

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    req.Header.Set("X-CostGraph-Tenant", tenantID)
    req.Header.Set("X-CostGraph-Feature", "export")
    resp, err := http.DefaultClient.Do(req)
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import requests

    requests.get(
        url,
        headers={"X-CostGraph-Tenant": tenant_id, "X-CostGraph-Feature": "export"},
    )
    ```
  </Tab>

  <Tab title="curl">
    ```shell theme={null}
    curl https://storage.internal/v1/objects/report.csv \
      -H "X-CostGraph-Tenant: acme" \
      -H "X-CostGraph-Feature: export"
    ```
  </Tab>
</Tabs>

Headers are normally set where requests are built, for example in a shared HTTP
client, middleware, or a transport wrapper. That way every call carries the
context without changes at each call site.

The agent only reads these headers. It doesn't remove them, so every service you
call receives them, including third-party APIs. Use values that aren't sensitive,
and add the headers only to requests whose destinations you want attributed.

### How header names become keys

| Rule | Example |
| - | - |
| The prefix is matched case-insensitively | `x-costgraph-tenant` and `X-CostGraph-Tenant` are the same header |
| Keys are lowercased | `X-CostGraph-Tenant` becomes `tenant` |
| Dashes become underscores | `X-CostGraph-Cost-Center` becomes `cost_center` |
| Only `a-z`, `0-9`, and `_` are allowed in keys | Headers with other characters in the key are ignored |
| Empty values are ignored | `X-CostGraph-Tenant:` attributes nothing |
| A repeated header keeps its last value | Two `X-CostGraph-Tenant` headers becomes the second wins |

Because dashes become underscores, `X-CostGraph-Cost-Center` and
`X-CostGraph-Cost_Center` produce the same key.

### Limits

* **8 tag keys per request.** Extra keys are not recorded.
* **256 bytes per value.** Longer values are truncated. Invalid UTF-8 is replaced
  with the Unicode replacement character.

Every distinct value becomes its own series. Use values from a bounded set,
like a tenant ID, team, or feature name. Don't use request IDs, timestamps, or
anything else that is unique per request.

<Warning>
  Header values are stored and shown to anyone with access to your CostGraph
  organization. Don't put secrets or personal data in `X-CostGraph-*` headers.
</Warning>

## Proxies and sidecars

Attribution happens at the client process that sends the request. Behind a proxy, each hop that is itself a supported client can be attributed.
This works only if the header is still on the request it forwards.

* If your gateway or proxy strips unknown headers, allow `X-CostGraph-*` through
  so later hops can attribute too.
* A sidecar that originates TLS (for example Envoy in Istio, or the Linkerd
  proxy) uses a TLS stack that isn't supported yet. The application's own request
  to the sidecar can still be attributed if the application sends it through a
  supported client.

## Read the results

Open **Network** in the dashboard and go to **Request header attribution**.

The cards at the top show totals for the selected period:

* **Request bytes** and **Response bytes** are the total size of attributed
  requests and responses, including headers and bodies.
* **Attributed cost** is the network cost of those bytes, priced by where the
  traffic went.

Each card has a **Previously** footer with the value for the period before.
When you compare numbers, compare like with like: the footer is the previous
window, not the current one.

### Group and filter

* **Group by** picks one or more tag keys. With **All tags**, every key is shown.
  A request with two keys, like `tenant` and `feature`, appears under both, so
  totals across different keys can add up to more than the overall total.
* **Filter by** narrows the results to a tag value, a destination **Port**, or a
  **Protocol**.
* The table can be sorted by cost or bytes. Expand a row to see the connections
  behind it.

### Locality

Each connection is priced by where its destination was:

| Locality | Billed |
| - | - |
| Cross zone | Yes |
| Cross region | Yes |
| Internet | Yes |
| Provider service (for example object storage) | Depends on the provider and service |
| Same zone | No |
| Same node | No |
| Same network, zone unknown | Can't be priced |
| Private, unattributed | Can't be priced |

A cost of `0` means the traffic was measured and the provider doesn't bill it.
A `-` means the traffic couldn't be priced because CostGraph doesn't know where
the peer is with enough precision.

## Common problems

<AccordionGroup>
  <Accordion title="My requests don't appear">
    Check each item in [Requirements](#requirements), in this order:

    1. The header is set on the **outgoing** request from the client, not on the
       response or on the server.
    2. The client's runtime is in the supported half of the
       [compatibility table](#runtime-compatibility).
    3. Per-request capture is on for that node. For the operator,
       `flowtrace.config.perRequestCollector` defaults to `false`. See
       [Enable capture](#enable-capture).
    4. The process runs in a container, or host-flow collection is on.
    5. The key contains only letters, digits, dashes, or underscores, and the value
       is not empty.
  </Accordion>

  <Accordion title="Only some hops are attributed">
    A proxy along the path is probably stripping the header, or the hop uses an
    unsupported TLS stack such as a BoringSSL or rustls sidecar. See
    [Proxies and sidecars](#proxies-and-sidecars).
  </Accordion>

  <Accordion title="Two keys I expected to be separate were merged">
    Dashes become underscores, so `X-CostGraph-Cost-Center` and
    `X-CostGraph-Cost_Center` share the key `cost_center`. Rename one of them.
  </Accordion>

  <Accordion title="Cost shows as -">
    The traffic was measured, but CostGraph couldn't place the destination well
    enough to price it. This is usually a private address that doesn't match a
    known resource, or a peer in the same network whose zone isn't known.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.