mirror of
https://github.com/katanemo/plano.git
synced 2026-07-11 16:12:13 +02:00
use custom exporter
This commit is contained in:
parent
b861f41c03
commit
7eec3bc932
11 changed files with 349 additions and 44 deletions
1
crates/.vscode/launch.json
vendored
1
crates/.vscode/launch.json
vendored
|
|
@ -14,6 +14,7 @@
|
||||||
"RUST_LOG": "debug",
|
"RUST_LOG": "debug",
|
||||||
"RUST_BACKTRACE": "1",
|
"RUST_BACKTRACE": "1",
|
||||||
"ARCH_CONFIG_PATH_RENDERED": "../demos/use_cases/multi_agent_with_crewai_langchain/config.yaml_rendered",
|
"ARCH_CONFIG_PATH_RENDERED": "../demos/use_cases/multi_agent_with_crewai_langchain/config.yaml_rendered",
|
||||||
|
// "ARCH_CONFIG_PATH_RENDERED": "../demos/use_cases/preference_based_routing/config.yaml_rendered",
|
||||||
"OTEL_COLLECTOR_URL": "http://localhost:4317",
|
"OTEL_COLLECTOR_URL": "http://localhost:4317",
|
||||||
"OTEL_TRACING_ENABLED": "true"
|
"OTEL_TRACING_ENABLED": "true"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use hermesllm::ProviderRequestType;
|
||||||
use http_body_util::combinators::BoxBody;
|
use http_body_util::combinators::BoxBody;
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use hyper::{Request, Response};
|
use hyper::{Request, Response};
|
||||||
|
use opentelemetry::trace::get_active_span;
|
||||||
use serde::ser::Error as SerError;
|
use serde::ser::Error as SerError;
|
||||||
use tracing::{debug, info, info_span, warn, Instrument};
|
use tracing::{debug, info, info_span, warn, Instrument};
|
||||||
|
|
||||||
|
|
@ -16,6 +17,7 @@ use super::agent_selector::{AgentSelectionError, AgentSelector};
|
||||||
use super::pipeline_processor::{PipelineError, PipelineProcessor};
|
use super::pipeline_processor::{PipelineError, PipelineProcessor};
|
||||||
use super::response_handler::ResponseHandler;
|
use super::response_handler::ResponseHandler;
|
||||||
use crate::router::plano_orchestrator::OrchestratorService;
|
use crate::router::plano_orchestrator::OrchestratorService;
|
||||||
|
use crate::tracing::{operation_component, set_service_name};
|
||||||
|
|
||||||
/// Main errors for agent chat completions
|
/// Main errors for agent chat completions
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|
@ -52,7 +54,8 @@ pub async fn agent_chat(
|
||||||
|
|
||||||
// Create a span with request_id that will be included in all log lines
|
// Create a span with request_id that will be included in all log lines
|
||||||
let request_span = info_span!(
|
let request_span = info_span!(
|
||||||
"agent_chat_handler",
|
"(orchestrator)",
|
||||||
|
component = "orchestrator",
|
||||||
request_id = %request_id,
|
request_id = %request_id,
|
||||||
http.method = %request.method(),
|
http.method = %request.method(),
|
||||||
http.path = %request.uri().path()
|
http.path = %request.uri().path()
|
||||||
|
|
@ -60,6 +63,9 @@ pub async fn agent_chat(
|
||||||
|
|
||||||
// Execute the handler inside the span
|
// Execute the handler inside the span
|
||||||
async {
|
async {
|
||||||
|
// Set service name for orchestrator operations
|
||||||
|
set_service_name(operation_component::ORCHESTRATOR);
|
||||||
|
|
||||||
match handle_agent_chat_inner(
|
match handle_agent_chat_inner(
|
||||||
request,
|
request,
|
||||||
orchestrator_service,
|
orchestrator_service,
|
||||||
|
|
@ -163,13 +169,17 @@ async fn handle_agent_chat_inner(
|
||||||
.and_then(|name| name.to_str().ok());
|
.and_then(|name| name.to_str().ok());
|
||||||
|
|
||||||
// Find the appropriate listener
|
// Find the appropriate listener
|
||||||
let listener = {
|
let listener: common::configuration::Listener = {
|
||||||
let listeners = listeners.read().await;
|
let listeners = listeners.read().await;
|
||||||
agent_selector
|
agent_selector
|
||||||
.find_listener(listener_name, &listeners)
|
.find_listener(listener_name, &listeners)
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
get_active_span(|span| {
|
||||||
|
span.update_name(format!("(orchestrator) {}", listener.name));
|
||||||
|
});
|
||||||
|
|
||||||
info!(listener = %listener.name, "handling request");
|
info!(listener = %listener.name, "handling request");
|
||||||
|
|
||||||
// Parse request body
|
// Parse request body
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ use crate::state::response_state_processor::ResponsesStateProcessor;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
extract_input_items, retrieve_and_combine_input, StateStorage, StateStorageError,
|
extract_input_items, retrieve_and_combine_input, StateStorage, StateStorageError,
|
||||||
};
|
};
|
||||||
use crate::tracing::operation_component;
|
use crate::tracing::{operation_component, set_service_name};
|
||||||
|
|
||||||
fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
|
fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
|
||||||
Full::new(chunk.into())
|
Full::new(chunk.into())
|
||||||
|
|
@ -55,7 +55,8 @@ pub async fn llm_chat(
|
||||||
|
|
||||||
// Create a span with request_id that will be included in all log lines
|
// Create a span with request_id that will be included in all log lines
|
||||||
let request_span = info_span!(
|
let request_span = info_span!(
|
||||||
"llm_chat_handler",
|
"llm",
|
||||||
|
component = "llm",
|
||||||
request_id = %request_id,
|
request_id = %request_id,
|
||||||
http.method = %request.method(),
|
http.method = %request.method(),
|
||||||
http.path = %request_path,
|
http.path = %request_path,
|
||||||
|
|
@ -92,6 +93,9 @@ async fn llm_chat_inner(
|
||||||
request_path: String,
|
request_path: String,
|
||||||
mut request_headers: hyper::HeaderMap,
|
mut request_headers: hyper::HeaderMap,
|
||||||
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
|
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
|
||||||
|
// Set service name for LLM operations
|
||||||
|
set_service_name(operation_component::LLM);
|
||||||
|
|
||||||
// Extract or generate traceparent - this establishes the trace context for all spans
|
// Extract or generate traceparent - this establishes the trace context for all spans
|
||||||
let traceparent: String = match request_headers
|
let traceparent: String = match request_headers
|
||||||
.get(TRACE_PARENT_HEADER)
|
.get(TRACE_PARENT_HEADER)
|
||||||
|
|
@ -148,20 +152,25 @@ async fn llm_chat_inner(
|
||||||
let model_from_request = client_request.model().to_string();
|
let model_from_request = client_request.model().to_string();
|
||||||
let _temperature = client_request.get_temperature();
|
let _temperature = client_request.get_temperature();
|
||||||
let is_streaming_request = client_request.is_streaming();
|
let is_streaming_request = client_request.is_streaming();
|
||||||
let resolved_model = resolve_model_alias(&model_from_request, &model_aliases);
|
let alias_resolved_model = resolve_model_alias(&model_from_request, &model_aliases);
|
||||||
|
|
||||||
// Record model information in span
|
// Record model information in span
|
||||||
tracing::Span::current().record("model.requested", model_from_request.as_str());
|
tracing::Span::current().record("model.requested", model_from_request.as_str());
|
||||||
tracing::Span::current().record("model.alias_resolved", resolved_model.as_str());
|
tracing::Span::current().record("model.alias_resolved", alias_resolved_model.as_str());
|
||||||
|
|
||||||
// Validate that the requested model exists in configuration
|
// Validate that the requested model exists in configuration
|
||||||
// This matches the validation in llm_gateway routing.rs
|
// This matches the validation in llm_gateway routing.rs
|
||||||
if llm_providers.read().await.get(&resolved_model).is_none() {
|
if llm_providers
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get(&alias_resolved_model)
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
let err_msg = format!(
|
let err_msg = format!(
|
||||||
"Model '{}' not found in configured providers",
|
"Model '{}' not found in configured providers",
|
||||||
resolved_model
|
alias_resolved_model
|
||||||
);
|
);
|
||||||
warn!(model = %resolved_model, "model not found in configured providers");
|
warn!(model = %alias_resolved_model, "model not found in configured providers");
|
||||||
let mut bad_request = Response::new(full(err_msg));
|
let mut bad_request = Response::new(full(err_msg));
|
||||||
*bad_request.status_mut() = StatusCode::BAD_REQUEST;
|
*bad_request.status_mut() = StatusCode::BAD_REQUEST;
|
||||||
return Ok(bad_request);
|
return Ok(bad_request);
|
||||||
|
|
@ -169,10 +178,10 @@ async fn llm_chat_inner(
|
||||||
|
|
||||||
// Handle provider/model slug format (e.g., "openai/gpt-4")
|
// Handle provider/model slug format (e.g., "openai/gpt-4")
|
||||||
// Extract just the model name for upstream (providers don't understand the slug)
|
// Extract just the model name for upstream (providers don't understand the slug)
|
||||||
let model_name_only = if let Some((_, model)) = resolved_model.split_once('/') {
|
let model_name_only = if let Some((_, model)) = alias_resolved_model.split_once('/') {
|
||||||
model.to_string()
|
model.to_string()
|
||||||
} else {
|
} else {
|
||||||
resolved_model.clone()
|
alias_resolved_model.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract tool names and user message preview for span attributes
|
// Extract tool names and user message preview for span attributes
|
||||||
|
|
@ -207,9 +216,9 @@ async fn llm_chat_inner(
|
||||||
// Get the upstream path and check if it's ResponsesAPI
|
// Get the upstream path and check if it's ResponsesAPI
|
||||||
let upstream_path = get_upstream_path(
|
let upstream_path = get_upstream_path(
|
||||||
&llm_providers,
|
&llm_providers,
|
||||||
&resolved_model,
|
&alias_resolved_model,
|
||||||
&request_path,
|
&request_path,
|
||||||
&resolved_model,
|
&alias_resolved_model,
|
||||||
is_streaming_request,
|
is_streaming_request,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
@ -294,31 +303,39 @@ async fn llm_chat_inner(
|
||||||
// Determine final model to use
|
// Determine final model to use
|
||||||
// Router returns "none" as a sentinel value when it doesn't select a specific model
|
// Router returns "none" as a sentinel value when it doesn't select a specific model
|
||||||
let router_selected_model = routing_result.model_name;
|
let router_selected_model = routing_result.model_name;
|
||||||
let model_name = if router_selected_model != "none" {
|
let resolved_model = if router_selected_model != "none" {
|
||||||
// Router selected a specific model via routing preferences
|
// Router selected a specific model via routing preferences
|
||||||
router_selected_model
|
router_selected_model
|
||||||
} else {
|
} else {
|
||||||
// Router returned "none" sentinel, use validated resolved_model from request
|
// Router returned "none" sentinel, use validated resolved_model from request
|
||||||
resolved_model.clone()
|
alias_resolved_model.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Record the routed model in span
|
// Record the routed model in span
|
||||||
tracing::Span::current().record("model.routing_resolved", model_name.as_str());
|
tracing::Span::current().record("model.routing_resolved", resolved_model.as_str());
|
||||||
|
|
||||||
get_active_span(|span| {
|
get_active_span(|span| {
|
||||||
span.update_name(format!("llm_chat POST {} -> {}", request_path, model_name));
|
let span_name = if model_from_request == resolved_model {
|
||||||
|
format!("(llm) {} {}", request_path, resolved_model)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"(llm) {} {} -> {}",
|
||||||
|
request_path, model_from_request, resolved_model
|
||||||
|
)
|
||||||
|
};
|
||||||
|
span.update_name(span_name);
|
||||||
});
|
});
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
url = %full_qualified_llm_provider_url,
|
url = %full_qualified_llm_provider_url,
|
||||||
provider_hint = %model_name,
|
provider_hint = %resolved_model,
|
||||||
upstream_model = %model_name_only,
|
upstream_model = %model_name_only,
|
||||||
"Routing to upstream"
|
"Routing to upstream"
|
||||||
);
|
);
|
||||||
|
|
||||||
request_headers.insert(
|
request_headers.insert(
|
||||||
ARCH_PROVIDER_HINT_HEADER,
|
ARCH_PROVIDER_HINT_HEADER,
|
||||||
header::HeaderValue::from_str(&model_name).unwrap(),
|
header::HeaderValue::from_str(&resolved_model).unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
request_headers.insert(
|
request_headers.insert(
|
||||||
|
|
@ -385,8 +402,8 @@ async fn llm_chat_inner(
|
||||||
base_processor,
|
base_processor,
|
||||||
state_store,
|
state_store,
|
||||||
original_input_items,
|
original_input_items,
|
||||||
|
alias_resolved_model.clone(),
|
||||||
resolved_model.clone(),
|
resolved_model.clone(),
|
||||||
model_name.clone(),
|
|
||||||
is_streaming_request,
|
is_streaming_request,
|
||||||
false, // Not OpenAI upstream since should_manage_state is true
|
false, // Not OpenAI upstream since should_manage_state is true
|
||||||
content_encoding,
|
content_encoding,
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ use hermesllm::{ProviderRequest, ProviderRequestType};
|
||||||
use hyper::header::HeaderMap;
|
use hyper::header::HeaderMap;
|
||||||
use opentelemetry::global;
|
use opentelemetry::global;
|
||||||
use opentelemetry::propagation::Injector;
|
use opentelemetry::propagation::Injector;
|
||||||
|
use opentelemetry::trace::get_active_span;
|
||||||
use tracing::{debug, info, instrument, warn};
|
use tracing::{debug, info, instrument, warn};
|
||||||
|
|
||||||
use crate::handlers::jsonrpc::{
|
use crate::handlers::jsonrpc::{
|
||||||
JsonRpcId, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JSON_RPC_VERSION,
|
JsonRpcId, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JSON_RPC_VERSION,
|
||||||
MCP_INITIALIZE, MCP_INITIALIZE_NOTIFICATION, TOOL_CALL_METHOD,
|
MCP_INITIALIZE, MCP_INITIALIZE_NOTIFICATION, TOOL_CALL_METHOD,
|
||||||
};
|
};
|
||||||
|
use crate::tracing::{operation_component, set_service_name};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Errors that can occur during pipeline processing
|
/// Errors that can occur during pipeline processing
|
||||||
|
|
@ -91,14 +93,14 @@ impl PipelineProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process the filter chain of agents (all except the terminal agent)
|
// /// Process the filter chain of agents (all except the terminal agent)
|
||||||
#[instrument(
|
// #[instrument(
|
||||||
skip(self, chat_history, agent_filter_chain, agent_map, request_headers),
|
// skip(self, chat_history, agent_filter_chain, agent_map, request_headers),
|
||||||
fields(
|
// fields(
|
||||||
filter_count = agent_filter_chain.filter_chain.as_ref().map(|fc| fc.len()).unwrap_or(0),
|
// filter_count = agent_filter_chain.filter_chain.as_ref().map(|fc| fc.len()).unwrap_or(0),
|
||||||
message_count = chat_history.len()
|
// message_count = chat_history.len()
|
||||||
)
|
// )
|
||||||
)]
|
// )]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn process_filter_chain(
|
pub async fn process_filter_chain(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -309,6 +311,9 @@ impl PipelineProcessor {
|
||||||
agent: &Agent,
|
agent: &Agent,
|
||||||
request_headers: &HeaderMap,
|
request_headers: &HeaderMap,
|
||||||
) -> Result<Vec<Message>, PipelineError> {
|
) -> Result<Vec<Message>, PipelineError> {
|
||||||
|
// Set service name for this filter span
|
||||||
|
set_service_name(operation_component::AGENT_FILTER);
|
||||||
|
|
||||||
// Update current span name to include filter name
|
// Update current span name to include filter name
|
||||||
use opentelemetry::trace::get_active_span;
|
use opentelemetry::trace::get_active_span;
|
||||||
get_active_span(|span| {
|
get_active_span(|span| {
|
||||||
|
|
@ -524,6 +529,9 @@ impl PipelineProcessor {
|
||||||
agent: &Agent,
|
agent: &Agent,
|
||||||
request_headers: &HeaderMap,
|
request_headers: &HeaderMap,
|
||||||
) -> Result<Vec<Message>, PipelineError> {
|
) -> Result<Vec<Message>, PipelineError> {
|
||||||
|
// Set service name for this filter span
|
||||||
|
set_service_name(operation_component::AGENT_FILTER);
|
||||||
|
|
||||||
// Update current span name to include filter name
|
// Update current span name to include filter name
|
||||||
use opentelemetry::trace::get_active_span;
|
use opentelemetry::trace::get_active_span;
|
||||||
get_active_span(|span| {
|
get_active_span(|span| {
|
||||||
|
|
@ -626,9 +634,18 @@ impl PipelineProcessor {
|
||||||
terminal_agent: &Agent,
|
terminal_agent: &Agent,
|
||||||
request_headers: &HeaderMap,
|
request_headers: &HeaderMap,
|
||||||
) -> Result<reqwest::Response, PipelineError> {
|
) -> Result<reqwest::Response, PipelineError> {
|
||||||
|
// Set service name for agent invocation span
|
||||||
|
set_service_name(operation_component::AGENT);
|
||||||
|
|
||||||
// let mut request = original_request.clone();
|
// let mut request = original_request.clone();
|
||||||
original_request.set_messages(messages);
|
original_request.set_messages(messages);
|
||||||
|
|
||||||
|
let request_url = "/v1/chat/completions";
|
||||||
|
|
||||||
|
get_active_span(|span| {
|
||||||
|
span.update_name(format!("(agent) {} {}", terminal_agent.id, request_url));
|
||||||
|
});
|
||||||
|
|
||||||
let request_body = ProviderRequestType::to_bytes(&original_request).unwrap();
|
let request_body = ProviderRequestType::to_bytes(&original_request).unwrap();
|
||||||
// let request_body = serde_json::to_string(&request)?;
|
// let request_body = serde_json::to_string(&request)?;
|
||||||
debug!("sending request to terminal agent {}", terminal_agent.id);
|
debug!("sending request to terminal agent {}", terminal_agent.id);
|
||||||
|
|
@ -657,7 +674,7 @@ impl PipelineProcessor {
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
.post(format!("{}/v1/chat/completions", self.url))
|
.post(format!("{}{}", self.url, request_url))
|
||||||
.headers(agent_headers)
|
.headers(agent_headers)
|
||||||
.body(request_body)
|
.body(request_body)
|
||||||
.send()
|
.send()
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use tokio_stream::StreamExt;
|
||||||
use tracing::{info, warn, Instrument};
|
use tracing::{info, warn, Instrument};
|
||||||
|
|
||||||
use crate::signals::{SignalAnalyzer, TextBasedSignalAnalyzer};
|
use crate::signals::{SignalAnalyzer, TextBasedSignalAnalyzer};
|
||||||
|
use crate::tracing::set_service_name;
|
||||||
use hermesllm::apis::openai::Message;
|
use hermesllm::apis::openai::Message;
|
||||||
|
|
||||||
/// Trait for processing streaming chunks
|
/// Trait for processing streaming chunks
|
||||||
|
|
@ -41,7 +42,9 @@ impl ObservableStreamProcessor {
|
||||||
/// Create a new passthrough processor
|
/// Create a new passthrough processor
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `service_name` - The service name for this span (e.g., "archgw(llm)")
|
/// * `service_name` - The service name for this span (e.g., "plano(llm)")
|
||||||
|
/// This will be set as the `service.name.override` attribute on the current span,
|
||||||
|
/// allowing the ServiceNameOverrideExporter to route spans to different services.
|
||||||
/// * `start_time` - When the request started (for duration calculation)
|
/// * `start_time` - When the request started (for duration calculation)
|
||||||
/// * `messages` - Optional conversation messages for signal analysis
|
/// * `messages` - Optional conversation messages for signal analysis
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
|
@ -49,8 +52,14 @@ impl ObservableStreamProcessor {
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
messages: Option<Vec<Message>>,
|
messages: Option<Vec<Message>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let service_name = service_name.into();
|
||||||
|
|
||||||
|
// Set the service name override on the current span for OpenTelemetry export
|
||||||
|
// This allows the ServiceNameOverrideExporter to route this span to the correct service
|
||||||
|
set_service_name(&service_name);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
service_name: service_name.into(),
|
service_name,
|
||||||
total_bytes: 0,
|
total_bytes: 0,
|
||||||
chunk_count: 0,
|
chunk_count: 0,
|
||||||
start_time,
|
start_time,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,54 @@
|
||||||
mod constants;
|
mod constants;
|
||||||
|
mod service_name_exporter;
|
||||||
|
|
||||||
pub use constants::{
|
pub use constants::{
|
||||||
error, http, llm, operation_component, routing, signals, OperationNameBuilder,
|
error, http, llm, operation_component, routing, signals, OperationNameBuilder,
|
||||||
};
|
};
|
||||||
|
pub use service_name_exporter::{ServiceNameOverrideExporter, SERVICE_NAME_OVERRIDE_KEY};
|
||||||
|
|
||||||
|
use opentelemetry::trace::TraceContextExt;
|
||||||
|
use opentelemetry::KeyValue;
|
||||||
|
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||||
|
|
||||||
|
/// Sets the service name override on the current tracing span.
|
||||||
|
///
|
||||||
|
/// This function adds the `service.name.override` attribute to the underlying
|
||||||
|
/// OpenTelemetry span, which allows observability backends to filter and group
|
||||||
|
/// spans by their logical service (e.g., `plano(llm)`, `plano(filter)`).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `service_name` - The service name to use (e.g., `operation_component::LLM`)
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// use brightstaff::tracing::{set_service_name, operation_component};
|
||||||
|
///
|
||||||
|
/// // Inside a traced function:
|
||||||
|
/// set_service_name(operation_component::LLM);
|
||||||
|
/// ```
|
||||||
|
pub fn set_service_name(service_name: &str) {
|
||||||
|
let span = tracing::Span::current();
|
||||||
|
let otel_context = span.context();
|
||||||
|
let otel_span = otel_context.span();
|
||||||
|
otel_span.set_attribute(KeyValue::new(
|
||||||
|
SERVICE_NAME_OVERRIDE_KEY,
|
||||||
|
service_name.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the service name override on the given tracing span.
|
||||||
|
///
|
||||||
|
/// Use this when you have a specific span reference and want to set
|
||||||
|
/// the service name override attribute on it.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `span` - The tracing span to set the service name on
|
||||||
|
/// * `service_name` - The service name to use (e.g., `operation_component::LLM`)
|
||||||
|
pub fn set_service_name_on_span(span: &tracing::Span, service_name: &str) {
|
||||||
|
let otel_context = span.context();
|
||||||
|
let otel_span = otel_context.span();
|
||||||
|
otel_span.set_attribute(KeyValue::new(
|
||||||
|
SERVICE_NAME_OVERRIDE_KEY,
|
||||||
|
service_name.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
|
||||||
167
crates/brightstaff/src/tracing/service_name_exporter.rs
Normal file
167
crates/brightstaff/src/tracing/service_name_exporter.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
//! Service Name Override Exporter
|
||||||
|
//!
|
||||||
|
//! This module provides a custom SpanExporter that allows per-span service.name overrides.
|
||||||
|
//! In OpenTelemetry, `service.name` is part of the Resource, which is tied to the TracerProvider.
|
||||||
|
//! However, if you need different service names for different spans (e.g., `plano(orchestrator)`,
|
||||||
|
//! `plano(filter)`, `plano(llm)`) within the same provider, this exporter handles that by:
|
||||||
|
//!
|
||||||
|
//! 1. Looking for a special span attribute `service.name.override`
|
||||||
|
//! 2. Grouping spans by their effective service name
|
||||||
|
//! 3. Exporting each group via a dedicated OTLP exporter whose Resource has the correct
|
||||||
|
//! `service.name`
|
||||||
|
//!
|
||||||
|
//! All per-service exporters are created eagerly at construction time so that no tonic
|
||||||
|
//! channel creation happens later inside `futures_executor::block_on` (which the
|
||||||
|
//! `BatchSpanProcessor` uses and which lacks a tokio runtime).
|
||||||
|
//!
|
||||||
|
//! # Usage
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use brightstaff::tracing::{set_service_name, operation_component};
|
||||||
|
//!
|
||||||
|
//! // In your instrumented code, set the service name override:
|
||||||
|
//! set_service_name(operation_component::LLM);
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use opentelemetry::Key;
|
||||||
|
use opentelemetry_otlp::WithExportConfig;
|
||||||
|
use opentelemetry_sdk::error::OTelSdkResult;
|
||||||
|
use opentelemetry_sdk::trace::{SpanData, SpanExporter};
|
||||||
|
use opentelemetry_sdk::Resource;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use super::operation_component;
|
||||||
|
|
||||||
|
/// The attribute key used to override the service name for a specific span.
|
||||||
|
/// Set this as a span attribute to route the span to a different service.
|
||||||
|
pub const SERVICE_NAME_OVERRIDE_KEY: &str = "service.name.override";
|
||||||
|
|
||||||
|
/// Default service name used when no override is set on a span.
|
||||||
|
const DEFAULT_SERVICE_NAME: &str = "plano";
|
||||||
|
|
||||||
|
/// All known service names that will have dedicated exporters.
|
||||||
|
const ALL_SERVICE_NAMES: &[&str] = &[
|
||||||
|
DEFAULT_SERVICE_NAME,
|
||||||
|
operation_component::INBOUND,
|
||||||
|
operation_component::ROUTING,
|
||||||
|
operation_component::ORCHESTRATOR,
|
||||||
|
operation_component::HANDOFF,
|
||||||
|
operation_component::AGENT_FILTER,
|
||||||
|
operation_component::AGENT,
|
||||||
|
operation_component::LLM,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A SpanExporter that supports per-span `service.name` overrides.
|
||||||
|
///
|
||||||
|
/// Internally it holds one OTLP exporter per known service name. Each exporter
|
||||||
|
/// has its own `Resource` with the correct `service.name`, so backends like
|
||||||
|
/// Jaeger see the spans under the right service.
|
||||||
|
pub struct ServiceNameOverrideExporter {
|
||||||
|
/// Map from service name → pre-created OTLP exporter (behind tokio Mutex
|
||||||
|
/// because `SpanExporter::export` takes `&self` and the future must be Send).
|
||||||
|
exporters: HashMap<String, Mutex<opentelemetry_otlp::SpanExporter>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual Debug because `opentelemetry_otlp::SpanExporter` doesn't implement Debug
|
||||||
|
impl std::fmt::Debug for ServiceNameOverrideExporter {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ServiceNameOverrideExporter")
|
||||||
|
.field("services", &self.exporters.keys().collect::<Vec<_>>())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceNameOverrideExporter {
|
||||||
|
/// Create a new `ServiceNameOverrideExporter`.
|
||||||
|
///
|
||||||
|
/// This eagerly creates one OTLP gRPC exporter per known service name so
|
||||||
|
/// that the tonic channel is established while a tokio runtime is available.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `endpoint` – The OTLP collector endpoint URL (e.g. `http://localhost:4317`)
|
||||||
|
pub fn new(endpoint: &str) -> Self {
|
||||||
|
let mut exporters = HashMap::new();
|
||||||
|
|
||||||
|
for &service_name in ALL_SERVICE_NAMES {
|
||||||
|
let resource = Resource::builder_empty()
|
||||||
|
.with_service_name(service_name)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut exporter = opentelemetry_otlp::SpanExporter::builder()
|
||||||
|
.with_tonic()
|
||||||
|
.with_endpoint(endpoint)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create OTLP span exporter");
|
||||||
|
|
||||||
|
exporter.set_resource(&resource);
|
||||||
|
exporters.insert(service_name.to_string(), Mutex::new(exporter));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { exporters }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpanExporter for ServiceNameOverrideExporter {
|
||||||
|
fn export(
|
||||||
|
&self,
|
||||||
|
batch: Vec<SpanData>,
|
||||||
|
) -> impl std::future::Future<Output = OTelSdkResult> + Send {
|
||||||
|
let override_key = Key::new(SERVICE_NAME_OVERRIDE_KEY);
|
||||||
|
|
||||||
|
// Group spans by their effective service name
|
||||||
|
let mut spans_by_service: HashMap<String, Vec<SpanData>> = HashMap::new();
|
||||||
|
|
||||||
|
for span in batch {
|
||||||
|
let service_name = span
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.find(|kv| kv.key == override_key)
|
||||||
|
.map(|kv| kv.value.to_string())
|
||||||
|
.unwrap_or_else(|| DEFAULT_SERVICE_NAME.to_string());
|
||||||
|
|
||||||
|
spans_by_service.entry(service_name).or_default().push(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect grouped spans into a Vec so the async block owns the data.
|
||||||
|
let results: Vec<(String, Vec<SpanData>)> = spans_by_service.into_iter().collect();
|
||||||
|
async move {
|
||||||
|
for (service_name, spans) in results {
|
||||||
|
// Look up the pre-created exporter; fall back to default if
|
||||||
|
// the service name isn't one of the known ones.
|
||||||
|
let key = if self.exporters.contains_key(&service_name) {
|
||||||
|
service_name.clone()
|
||||||
|
} else {
|
||||||
|
DEFAULT_SERVICE_NAME.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(exporter_mutex) = self.exporters.get(&key) {
|
||||||
|
let exporter = exporter_mutex.lock().await;
|
||||||
|
if let Err(e) = exporter.export(spans).await {
|
||||||
|
tracing::warn!(
|
||||||
|
service = %service_name,
|
||||||
|
error = ?e,
|
||||||
|
"Failed to export spans"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown_with_timeout(&mut self, timeout: Duration) -> OTelSdkResult {
|
||||||
|
for (_, exporter_mutex) in self.exporters.iter() {
|
||||||
|
if let Ok(mut exporter) = exporter_mutex.try_lock() {
|
||||||
|
let _ = exporter.shutdown_with_timeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_resource(&mut self, _resource: &Resource) {
|
||||||
|
// Each inner exporter already has its own resource set at creation time.
|
||||||
|
// Nothing to propagate.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,6 @@ use std::fmt;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use opentelemetry::global;
|
use opentelemetry::global;
|
||||||
use opentelemetry_otlp::WithExportConfig;
|
|
||||||
use opentelemetry_sdk::{propagation::TraceContextPropagator, trace::SdkTracerProvider};
|
use opentelemetry_sdk::{propagation::TraceContextPropagator, trace::SdkTracerProvider};
|
||||||
use time::macros::format_description;
|
use time::macros::format_description;
|
||||||
use tracing::{Event, Subscriber};
|
use tracing::{Event, Subscriber};
|
||||||
|
|
@ -12,6 +11,8 @@ use tracing_subscriber::registry::LookupSpan;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
use crate::tracing::ServiceNameOverrideExporter;
|
||||||
|
|
||||||
struct BracketedTime;
|
struct BracketedTime;
|
||||||
|
|
||||||
impl FormatTime for BracketedTime {
|
impl FormatTime for BracketedTime {
|
||||||
|
|
@ -96,14 +97,13 @@ pub fn init_tracer() -> &'static SdkTracerProvider {
|
||||||
if tracing_enabled {
|
if tracing_enabled {
|
||||||
// Set service name via environment if not already set
|
// Set service name via environment if not already set
|
||||||
if std::env::var("OTEL_SERVICE_NAME").is_err() {
|
if std::env::var("OTEL_SERVICE_NAME").is_err() {
|
||||||
std::env::set_var("OTEL_SERVICE_NAME", "brightstaff");
|
std::env::set_var("OTEL_SERVICE_NAME", "plano");
|
||||||
}
|
}
|
||||||
|
|
||||||
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
// Create ServiceNameOverrideExporter to support per-span service names
|
||||||
.with_tonic()
|
// This allows spans to have different service names (e.g., plano(orchestrator),
|
||||||
.with_endpoint(&otel_endpoint)
|
// plano(filter), plano(llm)) by setting the "service.name.override" attribute
|
||||||
.build()
|
let exporter = ServiceNameOverrideExporter::new(&otel_endpoint);
|
||||||
.expect("Failed to create OTLP span exporter");
|
|
||||||
|
|
||||||
let provider = SdkTracerProvider::builder()
|
let provider = SdkTracerProvider::builder()
|
||||||
.with_batch_exporter(exporter)
|
.with_batch_exporter(exporter)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,19 @@
|
||||||
services:
|
services:
|
||||||
|
|
||||||
|
plano:
|
||||||
|
build:
|
||||||
|
context: ../../../
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "12000:12000"
|
||||||
|
- "12001:12001"
|
||||||
|
environment:
|
||||||
|
- ARCH_CONFIG_PATH=/app/arch_config.yaml
|
||||||
|
- OPENAI_API_KEY=${OPENAI_API_KEY:?OPENAI_API_KEY environment variable is required but not set}
|
||||||
|
- OTEL_TRACING_HTTP_ENDPOINT=http://host.docker.internal:4318/v1/traces
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/app/arch_config.yaml:ro
|
||||||
|
- /etc/ssl/cert.pem:/etc/ssl/cert.pem
|
||||||
|
|
||||||
open-web-ui:
|
open-web-ui:
|
||||||
image: dyrnq/open-webui:main
|
image: dyrnq/open-webui:main
|
||||||
|
|
|
||||||
|
|
@ -38,15 +38,20 @@ services:
|
||||||
- LLM_GATEWAY_ENDPOINT=http://plano:12000/v1
|
- LLM_GATEWAY_ENDPOINT=http://plano:12000/v1
|
||||||
command: ["python", "-u", "langchain/weather_agent.py"]
|
command: ["python", "-u", "langchain/weather_agent.py"]
|
||||||
|
|
||||||
open-web-ui:
|
anythingllm:
|
||||||
image: dyrnq/open-webui:main
|
image: mintplexlabs/anythingllm
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "3001:3001"
|
||||||
|
cap_add:
|
||||||
|
- SYS_ADMIN
|
||||||
environment:
|
environment:
|
||||||
- DEFAULT_MODEL=gpt-4o-mini
|
- STORAGE_DIR=/app/server/storage
|
||||||
- ENABLE_OPENAI_API=true
|
- LLM_PROVIDER=generic-openai
|
||||||
- OPENAI_API_BASE_URL=http://plano:8001/v1
|
- GENERIC_OPEN_AI_BASE_PATH=http://plano:8001/v1
|
||||||
|
- GENERIC_OPEN_AI_MODEL_PREF=gpt-4o-mini
|
||||||
|
- GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=128000
|
||||||
|
- GENERIC_OPEN_AI_API_KEY=sk-placeholder
|
||||||
|
|
||||||
jaeger:
|
jaeger:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,21 @@
|
||||||
services:
|
services:
|
||||||
|
|
||||||
|
plano:
|
||||||
|
build:
|
||||||
|
context: ../../../
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "12000:12000"
|
||||||
|
- "12001:12001"
|
||||||
|
environment:
|
||||||
|
- ARCH_CONFIG_PATH=/app/arch_config.yaml
|
||||||
|
- OPENAI_API_KEY=${OPENAI_API_KEY:?OPENAI_API_KEY environment variable is required but not set}
|
||||||
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:?ANTHROPIC_API_KEY environment variable is required but not set}
|
||||||
|
- OTEL_TRACING_HTTP_ENDPOINT=http://host.docker.internal:4318/v1/traces
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/app/arch_config.yaml:ro
|
||||||
|
- /etc/ssl/cert.pem:/etc/ssl/cert.pem
|
||||||
|
|
||||||
open-web-ui:
|
open-web-ui:
|
||||||
image: dyrnq/open-webui:main
|
image: dyrnq/open-webui:main
|
||||||
restart: always
|
restart: always
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue