Otel: dedupe LLM span error/status + traceparent helpers (#130 follow-up)

Addresses the P2 refactoring follow-ups parked on issue #130:

- otel::with_traceparent(builder, &SpanContext) — one shared W3C
  traceparent injector. Replaces the 4-line inline header loop in
  llamacpp (3x) and openrouter (3x) and removes ollama's private
  inject_traceparent method (4 call sites now use the shared helper).
  Takes &SpanContext (not &dyn Span) because the Span trait is not dyn
  compatible in opentelemetry 0.31.

- otel::record_http_failure(span, status, message) and
  otel::set_http_status(span, status) — collapse the repeated
  'set http.status_code attribute + set_status(Status::error)'
  boilerplate across all three clients (~10 sites).

Net -45 lines. The remaining if-let-Some(span.as_mut()) guards (token
attributes, parse-failure status) stay inline rather than growing a
SpanHandle wrapper type for a pattern that's already readable.
This commit is contained in:
Cameron Cordes committed 2026-09-28 10:35:28 -04:00
1 parent 696d9fa8f3
commit 10ed84d7b4
4 files changed
+108 -153

No files matched your search

+22 -44
View File
@@ -5,7 +5,7 @@
use anyhow::Context as _;
use anyhow::{Result, anyhow, bail};
use async_trait::async_trait;
use opentelemetry::trace::{Span, Status, TraceContextExt};
use opentelemetry::trace::{Span, Status};
use opentelemetry::{Context, KeyValue};
use reqwest::Client;
use serde::Deserialize;
@@ -397,11 +397,7 @@ impl LlmClient for OpenRouterClient {
let mut request = self
.authed(self.client.post(&url))
.json(&Value::Object(body));
for (k, v) in crate::otel::traceparent_headers(
&Context::new().with_remote_span_context(span.span_context().clone()),
) {
request = request.header(k, v);
}
request = crate::otel::with_traceparent(request, span.span_context());
let resp = request
.send()
.await
@@ -409,17 +405,15 @@ impl LlmClient for OpenRouterClient {
if !resp.status().is_success() {
let status = resp.status();
span.set_attribute(KeyValue::new("http.status_code", status.as_u16() as i64));
span.set_status(Status::error(format!(
"OpenRouter stream request failed: {status}"
)));
crate::otel::record_http_failure(
Some(&mut span),
status.as_u16(),
&format!("OpenRouter stream request failed: {status}"),
);
let body = resp.text().await.unwrap_or_default();
bail!("OpenRouter stream request failed: {} — {}", status, body);
}
span.set_attribute(KeyValue::new(
"http.status_code",
resp.status().as_u16() as i64,
));
crate::otel::set_http_status(Some(&mut span), resp.status().as_u16());
// OpenAI-compat SSE stream. Each event is `data: <json>\n\n`, with
// `data: [DONE]` signalling completion. Tool calls arrive as
@@ -612,11 +606,7 @@ impl LlmClient for OpenRouterClient {
});
let mut request = self.authed(self.client.post(&url)).json(&body);
for (k, v) in crate::otel::traceparent_headers(
&Context::new().with_remote_span_context(span.span_context().clone()),
) {
request = request.header(k, v);
}
request = crate::otel::with_traceparent(request, span.span_context());
let resp = request
.send()
.await
@@ -627,17 +617,15 @@ impl LlmClient for OpenRouterClient {
if !resp.status().is_success() {
let status = resp.status();
span.set_attribute(KeyValue::new("http.status_code", status.as_u16() as i64));
span.set_status(Status::error(format!(
"OpenRouter embedding request failed: {status}"
)));
crate::otel::record_http_failure(
Some(&mut span),
status.as_u16(),
&format!("OpenRouter embedding request failed: {status}"),
);
let body = resp.text().await.unwrap_or_default();
bail!("OpenRouter embedding request failed: {} — {}", status, body);
}
span.set_attribute(KeyValue::new(
"http.status_code",
resp.status().as_u16() as i64,
));
crate::otel::set_http_status(Some(&mut span), resp.status().as_u16());
#[derive(Deserialize)]
struct EmbedResponse {
@@ -764,11 +752,7 @@ impl OpenRouterClient {
.authed(self.client.post(&url))
.json(&Value::Object(body));
if let Some(s) = span.as_mut() {
for (k, v) in crate::otel::traceparent_headers(
&Context::new().with_remote_span_context(s.span_context().clone()),
) {
request = request.header(k, v);
}
request = crate::otel::with_traceparent(request, s.span_context());
}
let resp = match request.send().await {
Ok(r) => r,
@@ -782,21 +766,15 @@ impl OpenRouterClient {
if !resp.status().is_success() {
let status = resp.status();
if let Some(s) = span.as_mut() {
s.set_attribute(KeyValue::new("http.status_code", status.as_u16() as i64));
s.set_status(Status::error(format!(
"OpenRouter chat request failed: {status}"
)));
}
crate::otel::record_http_failure(
span.as_deref_mut(),
status.as_u16(),
&format!("OpenRouter chat request failed: {status}"),
);
let body = resp.text().await.unwrap_or_default();
bail!("OpenRouter chat request failed: {} — {}", status, body);
}
if let Some(s) = span.as_mut() {
s.set_attribute(KeyValue::new(
"http.status_code",
resp.status().as_u16() as i64,
));
}
crate::otel::set_http_status(span.as_deref_mut(), resp.status().as_u16());
let parsed: Value = match resp.json().await {
Ok(v) => v,