Skip to content

Importing existing traffic

If your LLM calls are already recorded somewhere else, you do not have to wait for new traffic to see anything in Metergraph. A separate command line tool, metergraphrelay, reads each recorded call from the tool that holds it, reshapes it into a Metergraph row, and uploads it through the same ingest API the SDK uses. Your application is not touched.

Run it once to load history, or run it hourly to keep Metergraph level with the other tool.

Importing is the faster path to a populated dashboard. Instrumenting is the one that captures more.

ImportInstall the SDK
Code changeNoneOne init() call
HistoryEverything the other tool keptNew calls only
Function attributionNoYes
Route namesOnly if the source recorded oneYes, inferred or declared
Streaming and time to first tokenNoYes
Recorded outcomesNoYes, via record_outcome()
Content controlsNone on most pathsPer field, per call

The two are not exclusive. A common shape is to import the last few months from your existing tool, install the SDK for everything from today, and let the dashboard span both.

An imported row is only ever as good as what the source tool stored. The relay never invents a value it did not read.

What comes across on most paths. Timestamp, provider, model, input and output token counts, status, error type, the source’s own request id, trace and span ids, and the prompt and response text the source held. Cache and reasoning token buckets come across wherever the source recorded them.

What does not come across, on any path:

  • Function attribution. The SDK walks the Python stack at capture time and records the module and function that made the call (metergraph/_capture.py:621-652). No source tool stores that, so an imported row has no func and no module, and the parts of the dashboard that group spend by function will be empty for imported traffic.
  • Route names, unless the source recorded one. Every provider page below says what the relay uses as the route and what it falls back to. When the source carried nothing usable you get a bare SDK method name, a model class name, or a literal <provider>/backfill. Pass --route to name the workflow yourself. See Name a route.
  • Streaming detail. No stream flag and no time to first token.
  • Batch metadata, template hashes, and unit counts.
  • Recorded outcomes. Outcomes are written by your application through the SDK, so there is nothing in a trace export to import. See Record real outcomes.
  • Repository identity, so imported rows do not link back to a commit.

Latency is per source: Braintrust, LangSmith, Phoenix and Portkey all carry it, and Langfuse does not, because the relay reads only the observation’s start time.

Cost is also per source. Where the source computed a cost, that figure is carried across as reported. Where it did not, the row arrives unpriced and Metergraph prices it from its own model catalog at read time.

Where your calls are todayPageBackfillHourly sync
Langfuse Cloud, or self-hosted v4 and newerLangfuseyesyes
LangSmithLangSmithyesyes
BraintrustBraintrustyesyes
Arize Phoenix 13.15 and newerPhoenixyesyes
PortkeyPortkeyyesyes, API mode only
OpenAI stored completionsOpenAIyesno

Every path imports model calls only. Chains, tools, retrievers, agents, evals, scores, annotations and feedback are never read.

If you send OpenTelemetry spans today, you may not need an import at all. The SDK’s span exporter reads the OpenInference and gen_ai.* dialects directly, so those spans can reach Metergraph live. See OTLP spans.

Do this once, then follow the page for your tool.

  1. Sign in to Metergraph and open Keys. Copy an ingest key. Read keys and agent keys cannot upload traces and will be refused with a 403. Keep the key in a secret manager. See Manage your workspace.

  2. Any machine that can reach both your source tool and Metergraph will do. It needs Python 3.10 or newer.

    pip install metergraphrelay
  3. Every relay command loads a .env from the directory you run it in. Start with the Metergraph key. Each provider page adds its own lines.

    .env
    METERGRAPH_APP_TOKEN=<your ingest key>
    # Only for a Metergraph you operate yourself. Leave unset for hosted.
    # METERGRAPH_INGEST_URL=https://metergraph.internal.example.com

    Commit a .env.example with the names and no values. Never commit .env.

pull writes a local JSONL file of Metergraph-shaped rows for the range you ask for. Read the file, then push uploads it. Use this for a one time load of history, or any time you want to see the rows before they leave the machine.

metergraphrelay pull <provider> [selectors] --since 2026-08-01T00:00:00Z --output traces.jsonl
metergraphrelay push traces.jsonl

--output defaults to ./traces.jsonl. -n caps the number of rows, not the number of traces, and defaults to 100 on every provider except OpenAI, where it is 10.

One command pulls the next window and uploads it. Metergraph holds the checkpoint, so the relay keeps no local state and the same cron line is safe to run from more than one machine.

metergraphrelay sync <provider> --source-scope <stable name> --initial-since 2026-08-01T00:00:00Z [selectors]

Sync is a shipped feature, not a plan. The coordination routes are POST /v1/import-sync/acquire, POST /v1/import-sync/leases/{id}/renew, POST /v1/import-sync/leases/{id}/complete, DELETE /v1/import-sync/leases/{id} and GET /v1/import-sync/state (app/src/metergraph_app/api/import_sync.py), and the server accepts portkey, langfuse, braintrust, phoenix and langsmith as sources (import_identity.py:30).

The contract is the same for every provider:

  • The server picks the window. Each run imports one window of at most one hour, measured from the committed checkpoint (import_sync.py:27, MAX_WINDOW_SECONDS = 3600).
  • Windows overlap by five minutes. Every window after the first reaches five minutes back behind the checkpoint (import_sync.py:28), so a row written slightly late at the source is not missed.
  • Re-read rows are deduplicated. Each synced row carries import_source, import_source_scope and import_event_id. The worker claims those identities before it does any pricing work, and a row whose identity is already claimed by another batch is dropped (import_identity.claim_rows). Overlap never double counts.
  • The lease lasts fifteen minutes and the relay renews it every five minutes while a run is in flight (import_sync.py:29, sync_core.py:17).
  • The checkpoint advances only on a clean window. A failed upload, a row the relay could not read, or a lost lease all leave the checkpoint where it was, and the same window is retried on the next run.

--source-scope is required, and must never change

Section titled “--source-scope is required, and must never change”

It names this stream’s checkpoint on the Metergraph side, and deduplication is keyed on it. Use a plain, permanent label such as the project name. Never derive it from a credential or from the flag list: rotating a key or reordering flags would start a fresh checkpoint and re-import your history. Give each selector set its own scope. Two cron lines sharing one scope share a checkpoint, and each one drops the other’s rows.

Once a checkpoint exists the server ignores it, so cron can pass it on every run. Omitting it on a stream’s very first run is a 422.

0 * * * * metergraphrelay sync langfuse --source-scope support-bot-prod --initial-since 2026-08-01T00:00:00+00:00 --trace-name support-bot --tag prod

To work through a backlog, run the same command back to back until it reports that it is caught up.

0 means the window completed, the server was already caught up, or another run holds the lease. Anything else prints an Error: line to stderr naming the phase that failed, releases the lease, and leaves the window to be retried. Two cases are worth recognising:

  • “could not be normalized” means the source returned a row the relay could not read. The window stays pending so nothing is lost quietly. Fix the row at the source, or pass --allow-skipped to advance past it. Do that deliberately: once the checkpoint moves, a skipped row is gone.
  • A 422 on acquire means either the Metergraph server predates support for that source, or this is the stream’s first run and --initial-since was omitted.

Open Traces in Metergraph. Imported calls show the provider, model, tokens, status and route, carrying the source tool’s original timestamp. Three things to check on the first rows:

  • The route is what you expect. A value ending in /backfill, or a bare SDK method or model class name, means the source carried no workflow name. --route is the fix.
  • input_tokens is the whole prompt. On a cached call it should be larger than the cache read count, not smaller.
  • Failed calls are present, with their token counts. A failed call still cost its input tokens, and the import keeps that.
SymptomLikely causeWhat to do
401 or 403 from MetergraphThe token is not an ingest keyMint an ingest key on Keys
402 from MetergraphThe monthly call allowance is spentSee Limits and allowances
A pull returns 0 rows just after traffic was recordedThe source tool has not made the rows readable yetWait, then rerun
Every sync run reports busyAnother machine’s run holds the lease, or a crash left one behindWait up to fifteen minutes for the lease to expire
Rows counted twicepull and sync ran over one rangeOne method per range
Every route is a model or method nameThe source carried no workflow namePass --route

Problems specific to one source are on that source’s page.

These paths need a metergraphrelay release with the sync command for your provider, and a Metergraph server that has applied migration 0073_import_sync_sources. Hosted Metergraph has both. A Metergraph you operate yourself needs that migration before any sync command will get past acquire.