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:
1 parent
696d9fa8f3
commit
10ed84d7b4
4 files changed
+108
-153
No files matched your search
+22
-44
@@ -23,7 +23,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;
|
||||
@@ -523,11 +523,7 @@ impl LlamaCppClient {
|
||||
let _gpu = crate::ai::gpu::llm_lease().await;
|
||||
let mut request = 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,
|
||||
@@ -541,21 +537,15 @@ impl LlamaCppClient {
|
||||
|
||||
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!(
|
||||
"llama-swap chat request failed: {status}"
|
||||
)));
|
||||
}
|
||||
crate::otel::record_http_failure(
|
||||
span.as_deref_mut(),
|
||||
status.as_u16(),
|
||||
&format!("llama-swap chat request failed: {status}"),
|
||||
);
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
bail!("llama-swap 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 = resp.json().await.context("parsing chat response")?;
|
||||
let choice = parsed
|
||||
@@ -700,11 +690,7 @@ impl LlmClient for LlamaCppClient {
|
||||
// below so the lease spans the whole generation, not just the send.
|
||||
let gpu = crate::ai::gpu::llm_lease().await;
|
||||
let mut request = 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
|
||||
@@ -712,17 +698,15 @@ impl LlmClient for LlamaCppClient {
|
||||
|
||||
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!(
|
||||
"llama-swap stream request failed: {status}"
|
||||
)));
|
||||
crate::otel::record_http_failure(
|
||||
Some(&mut span),
|
||||
status.as_u16(),
|
||||
&format!("llama-swap stream request failed: {status}"),
|
||||
);
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
bail!("llama-swap 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());
|
||||
|
||||
let byte_stream = resp.bytes_stream();
|
||||
let stream = async_stream::stream! {
|
||||
@@ -860,11 +844,7 @@ impl LlmClient for LlamaCppClient {
|
||||
});
|
||||
|
||||
let mut request = 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 = match request.send().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
@@ -875,17 +855,15 @@ impl LlmClient for LlamaCppClient {
|
||||
|
||||
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!(
|
||||
"llama-swap embedding request failed: {status}"
|
||||
)));
|
||||
crate::otel::record_http_failure(
|
||||
Some(&mut span),
|
||||
status.as_u16(),
|
||||
&format!("llama-swap embedding request failed: {status}"),
|
||||
);
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
bail!("llama-swap 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 {
|
||||
|
||||
+29
-64
@@ -2,7 +2,7 @@ use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use chrono::NaiveDate;
|
||||
use opentelemetry::trace::{Span, Status, TraceContextExt};
|
||||
use opentelemetry::trace::{Span, Status};
|
||||
use opentelemetry::{Context, KeyValue};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -12,7 +12,6 @@ use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::ai::llm_client::{LlmClient, LlmStreamEvent};
|
||||
use crate::otel::traceparent_headers;
|
||||
use futures::stream::{BoxStream, StreamExt};
|
||||
|
||||
// Re-export shared types so existing `crate::ai::ollama::{...}` imports
|
||||
@@ -392,7 +391,7 @@ impl OllamaClient {
|
||||
.post(format!("{}/api/generate", url))
|
||||
.json(&request);
|
||||
if let Some(s) = span.as_mut() {
|
||||
request_builder = self.inject_traceparent(request_builder, s);
|
||||
request_builder = crate::otel::with_traceparent(request_builder, s.span_context());
|
||||
}
|
||||
let response = match request_builder.send().await {
|
||||
Ok(r) => r,
|
||||
@@ -406,10 +405,11 @@ impl OllamaClient {
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.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!("Ollama request failed: {status}")));
|
||||
}
|
||||
crate::otel::record_http_failure(
|
||||
span.as_deref_mut(),
|
||||
status.as_u16(),
|
||||
&format!("Ollama request failed: {status}"),
|
||||
);
|
||||
let error_body = response.text().await.unwrap_or_default();
|
||||
return Err(anyhow::anyhow!(
|
||||
"Ollama request failed: {} - {}",
|
||||
@@ -417,12 +417,7 @@ impl OllamaClient {
|
||||
error_body
|
||||
));
|
||||
}
|
||||
if let Some(s) = span.as_mut() {
|
||||
s.set_attribute(KeyValue::new(
|
||||
"http.status_code",
|
||||
response.status().as_u16() as i64,
|
||||
));
|
||||
}
|
||||
crate::otel::set_http_status(span.as_deref_mut(), response.status().as_u16());
|
||||
|
||||
let result: OllamaResponse = match response.json().await {
|
||||
Ok(r) => r,
|
||||
@@ -451,22 +446,6 @@ impl OllamaClient {
|
||||
Ok(result.response)
|
||||
}
|
||||
|
||||
/// Attach the W3C traceparent header to an outgoing Ollama request so
|
||||
/// a server that understands it can parent its own spans onto ours.
|
||||
fn inject_traceparent(
|
||||
&self,
|
||||
builder: reqwest::RequestBuilder,
|
||||
span: &opentelemetry::global::BoxedSpan,
|
||||
) -> reqwest::RequestBuilder {
|
||||
let mut builder = builder;
|
||||
for (k, v) in traceparent_headers(
|
||||
&Context::new().with_remote_span_context(span.span_context().clone()),
|
||||
) {
|
||||
builder = builder.header(k, v);
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
/// Start the `llm.request` span for an outgoing Ollama call.
|
||||
fn start_request_span(
|
||||
&self,
|
||||
@@ -899,7 +878,7 @@ Analyze the image and use specific details from both the visual content and the
|
||||
|
||||
let response = {
|
||||
let mut rb = self.client.post(&url).json(&request_body);
|
||||
rb = self.inject_traceparent(rb, &span);
|
||||
rb = crate::otel::with_traceparent(rb, span.span_context());
|
||||
match rb.send().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
@@ -912,10 +891,11 @@ Analyze the image and use specific details from both the visual content and the
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
span.set_attribute(KeyValue::new("http.status_code", status.as_u16() as i64));
|
||||
span.set_status(Status::error(format!(
|
||||
"Ollama stream request failed: {status}"
|
||||
)));
|
||||
crate::otel::record_http_failure(
|
||||
Some(&mut span),
|
||||
status.as_u16(),
|
||||
&format!("Ollama stream request failed: {status}"),
|
||||
);
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!(
|
||||
"Ollama stream request failed with status {}: {}",
|
||||
@@ -923,10 +903,7 @@ Analyze the image and use specific details from both the visual content and the
|
||||
body
|
||||
);
|
||||
}
|
||||
span.set_attribute(KeyValue::new(
|
||||
"http.status_code",
|
||||
response.status().as_u16() as i64,
|
||||
));
|
||||
crate::otel::set_http_status(Some(&mut span), response.status().as_u16());
|
||||
|
||||
// Ollama streams NDJSON: each line is a full `OllamaStreamChunk`.
|
||||
// We buffer partial lines across chunks from the byte stream.
|
||||
@@ -1064,7 +1041,7 @@ Analyze the image and use specific details from both the visual content and the
|
||||
let response = {
|
||||
let mut rb = self.client.post(&url).json(&request_body);
|
||||
if let Some(s) = span.as_mut() {
|
||||
rb = self.inject_traceparent(rb, s);
|
||||
rb = crate::otel::with_traceparent(rb, s.span_context());
|
||||
}
|
||||
match rb.send().await {
|
||||
Ok(r) => r,
|
||||
@@ -1080,12 +1057,11 @@ Analyze the image and use specific details from both the visual content and the
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.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!(
|
||||
"Ollama chat request failed: {status}"
|
||||
)));
|
||||
}
|
||||
crate::otel::record_http_failure(
|
||||
span.as_deref_mut(),
|
||||
status.as_u16(),
|
||||
&format!("Ollama chat request failed: {status}"),
|
||||
);
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
// warn, not error — the outer `chat_with_tools` may recover via
|
||||
// the fallback server. When both fail, the outer layer emits the
|
||||
@@ -1102,12 +1078,7 @@ Analyze the image and use specific details from both the visual content and the
|
||||
body
|
||||
);
|
||||
}
|
||||
if let Some(s) = span.as_mut() {
|
||||
s.set_attribute(KeyValue::new(
|
||||
"http.status_code",
|
||||
response.status().as_u16() as i64,
|
||||
));
|
||||
}
|
||||
crate::otel::set_http_status(span.as_deref_mut(), response.status().as_u16());
|
||||
|
||||
let chat_response: OllamaChatResponse = match response.json().await {
|
||||
Ok(r) => r,
|
||||
@@ -1268,7 +1239,7 @@ Analyze the image and use specific details from both the visual content and the
|
||||
.post(format!("{}/api/embed", url))
|
||||
.json(&request);
|
||||
if let Some(s) = span.as_mut() {
|
||||
rb = self.inject_traceparent(rb, s);
|
||||
rb = crate::otel::with_traceparent(rb, s.span_context());
|
||||
}
|
||||
match rb.send().await {
|
||||
Ok(r) => r,
|
||||
@@ -1283,12 +1254,11 @@ Analyze the image and use specific details from both the visual content and the
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.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!(
|
||||
"Ollama batch embedding request failed: {status}"
|
||||
)));
|
||||
}
|
||||
crate::otel::record_http_failure(
|
||||
span.as_deref_mut(),
|
||||
status.as_u16(),
|
||||
&format!("Ollama batch embedding request failed: {status}"),
|
||||
);
|
||||
let error_body = response.text().await.unwrap_or_default();
|
||||
return Err(anyhow::anyhow!(
|
||||
"Ollama batch embedding request failed: {} - {}",
|
||||
@@ -1296,12 +1266,7 @@ Analyze the image and use specific details from both the visual content and the
|
||||
error_body
|
||||
));
|
||||
}
|
||||
if let Some(s) = span.as_mut() {
|
||||
s.set_attribute(KeyValue::new(
|
||||
"http.status_code",
|
||||
response.status().as_u16() as i64,
|
||||
));
|
||||
}
|
||||
crate::otel::set_http_status(span.as_deref_mut(), response.status().as_u16());
|
||||
|
||||
let result: OllamaEmbedResponse = match response.json().await {
|
||||
Ok(r) => r,
|
||||
|
||||
+22
-44
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user