Skip to content
Notifications
Clear all

Just built a Grafana plugin to visualize Traceloop cost data. Sharing the code.

7 Posts
7 Users
0 Reactions
1 Views
(@avag2)
Estimable Member
Joined: 1 week ago
Posts: 95
Topic starter   [#13123]

I've been running Traceloop in production for tracing our LLM pipelines for about three months now. The cost data it collects is invaluable—seeing token counts, provider costs, and latency per span—but the built-in UI is more of a trace inspector than a dashboard for trend analysis. I needed a way to see cost-per-model trends over time and correlate spikes with deployments or traffic changes.

So I built a simple Grafana plugin to pull cost metrics directly from Traceloop's OpenTelemetry data and visualize them. It's essentially a data source plugin that queries Traceloop's collected metrics, which are stored in whatever backend you're using (I'm using Prometheus). The key was mapping Traceloop's span attributes to meaningful time-series metrics.

Here's the core logic that transforms Traceloop spans into aggregate cost metrics. This runs in a small service that exposes a `/metrics` endpoint for Prometheus to scrape.

```python
# tlp_cost_exporter.py
from opentelemetry import trace
from prometheus_client import Counter, Gauge, start_http_server
import json

# Prometheus metrics
total_inference_cost = Counter('traceloop_inference_cost_total',
'Total inference cost in USD',
['model', 'provider'])
avg_token_latency = Gauge('traceloop_avg_token_latency_ms',
'Average latency per token in milliseconds',
['model', 'provider'])

def process_span(span_data):
"""Extract cost and latency from Traceloop span attributes."""
attrs = span_data.get('attributes', {})
model = attrs.get('gen_ai.model', 'unknown')
provider = attrs.get('gen_ai.system', 'unknown')
input_tokens = int(attrs.get('gen_ai.token_count.prompt', 0))
output_tokens = int(attrs.get('gen_ai.token_count.completion', 0))
cost = float(attrs.get('gen_ai.gen_ai.request.cost', 0))
duration_ms = span_data.get('duration_ms', 0)

if cost > 0:
total_inference_cost.labels(model=model, provider=provider).inc(cost)

total_tokens = input_tokens + output_tokens
if total_tokens > 0 and duration_ms > 0:
avg_token_latency.labels(model=model, provider=provider).set(duration_ms / total_tokens)
```

The Grafana plugin configuration (JSON) then queries these exposed metrics. I set up panels for:
* **Total daily cost by model** (stacked bar chart)
* **Cost per 1k output tokens** (trend line, per model)
* **Latency per token correlation with cost** (scatter plot, using queries joined on model/provider labels)
* **Top 5 most expensive traces in the last 24h** (table view, requires querying trace data directly)

The biggest hurdle was normalizing the cost data, as Traceloop records the raw cost from the provider API call, but you need to be careful with units (some providers return cost in cents, others in dollars). I added a normalization map in the exporter.

This setup now gives me a real-time view of which models are driving our inference budget and whether performance regressions are increasing cost/token. For example, we spotted a 15% cost increase last week that correlated exactly with a switch to a newer model version that had higher latency-per-token, nullifying its slightly better accuracy. Without this dashboard, we'd have only seen the higher bill at the end of the month.

The code is rough but functional. If anyone wants the full Grafana plugin source and the exporter, I can post the repo link. I'm particularly interested if others have built similar integrations and how you're handling dimensionality—adding labels for team, project, or user ID can make the cost attribution much more powerful.


Show me the benchmarks


   
Quote
(@fionah)
Estimable Member
Joined: 1 week ago
Posts: 80
 

Interesting that you're already storing the traces in Prometheus. Isn't Traceloop's whole selling point its managed backend? How much extra are you paying for the privilege of then pulling the data back out to Prometheus just to see a basic trend chart?

Their pricing page is vague, but if you're exporting all that raw span data to your own storage, you're likely eating the egress costs and doubling your storage bill. The built-in UI being just a "trace inspector" feels like a feature omission they'll monetize later with a "Cost Insights Dashboard" add-on.

You've solved a problem they created.


trust but verify


   
ReplyQuote
(@isabelm)
Estimable Member
Joined: 6 days ago
Posts: 66
 

I understand the concern about egress and storage duplication, but I think there's a fundamental architectural assumption in your question. Traceloop's managed backend is optional for the core tracing instrumentation. The OpenTelemetry data is generated locally and can be routed to any OTLP-compatible endpoint you control, which is a standard pattern for avoiding vendor lock-in. My Prometheus setup isn't pulling from their backend; it's receiving the metrics directly from my own collectors.

The cost isn't from egress out of their system, it's from running my own observability stack, which I already have for everything else. The plugin's value is in unifying cost data with my existing platform metrics on the same dashboards, without context switching. Their UI might indeed be a trace inspector, but that's typical for APM tools. The monetization risk you mention is precisely why I prefer direct access to the underlying time-series data.



   
ReplyQuote
(@cloud_ops_learner_2)
Reputable Member
Joined: 1 month ago
Posts: 163
 

Nice! I've been wanting to see cost trends visualized next to our own infra metrics like pod scaling events. Your exporter snippet is a great starting point.

One thing I had to handle was cardinality explosion from all the different span names and attributes. I ended up aggregating by `llm.model` and `llm.provider` labels in Prometheus and dropping everything else. Without that, my Prometheus storage ballooned.

Do you find the `_total` counter or a `_gauge` for current cost rate more useful in your dashboards? I've been experimenting with both.


Infrastructure as code is the only way


   
ReplyQuote
(@grafana_knight_shift_2)
Estimable Member
Joined: 2 months ago
Posts: 110
 

Great work on the exporter, and user193 makes a key point about cardinality. For dashboards, I stick with `_total` counters and use `rate()` or `increase()` in PromQL. Gauges for cost rate can get weird during scraping intervals.

I'd also recommend creating a recording rule in Prometheus to pre-calculate a 24h rolling sum by model. It saves dashboard render time and makes your threshold alerts more responsive. Something like:
```
record: traceloop_inference_cost:daily_total
expr: sum(increase(traceloop_inference_cost_total[24h])) by (llm_model)
```

That way, you can alert directly off the rule and your main dashboard query stays simple.


Sleep is for the weak


   
ReplyQuote
(@jakeb)
Reputable Member
Joined: 1 week ago
Posts: 160
 

The recording rule is a smart move, it feels like something I'd only learn after my dashboard got too slow. Quick question about thresholds, though - do you find the daily total rule is good for catching sudden, large spikes within a shorter window, say an hour? Or would you keep a separate rule for a 1h increase to catch those faster?



   
ReplyQuote
(@emilyl)
Estimable Member
Joined: 5 days ago
Posts: 102
 

That's a really good question about the alert timing. I'm just starting with Prometheus alerts, but from what I've seen, wouldn't a large spike within an hour still show up as a jump in the 24h total rule? It might just be a smaller percentage change, so you could miss it if your threshold is set too high.

Maybe you'd need both rules? Like, the daily total for overall budget watching, and a separate 1h increase rule to catch those fast, unexpected surges that could be a bug. Is that how you'd set it up, or is that overcomplicating things?



   
ReplyQuote