mirror of
https://github.com/katanemo/plano.git
synced 2026-06-23 15:38:07 +02:00
refactor: prefix custom trace attributes and update schema handlers tests configs
This commit is contained in:
parent
3a663aa780
commit
aa64704ed1
7 changed files with 73 additions and 155 deletions
|
|
@ -382,28 +382,10 @@ properties:
|
||||||
type: integer
|
type: integer
|
||||||
trace_arch_internal:
|
trace_arch_internal:
|
||||||
type: boolean
|
type: boolean
|
||||||
custom_attributes:
|
custom_attribute_prefixes:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: object
|
type: string
|
||||||
properties:
|
|
||||||
key:
|
|
||||||
type: string
|
|
||||||
type:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- str
|
|
||||||
- bool
|
|
||||||
- float
|
|
||||||
- int
|
|
||||||
header:
|
|
||||||
type: string
|
|
||||||
additionalProperties: false
|
|
||||||
required:
|
|
||||||
- key
|
|
||||||
- type
|
|
||||||
- header
|
|
||||||
additionalProperties: false
|
|
||||||
mode:
|
mode:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ async fn handle_agent_chat(
|
||||||
tracing_config
|
tracing_config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|tracing| tracing.custom_attributes.as_deref()),
|
.and_then(|tracing| tracing.custom_attribute_prefixes.as_deref()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let chat_request_bytes = request.collect().await?.to_bytes();
|
let chat_request_bytes = request.collect().await?.to_bytes();
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ pub async fn llm_chat(
|
||||||
tracing_config
|
tracing_config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|tracing| tracing.custom_attributes.as_deref()),
|
.and_then(|tracing| tracing.custom_attribute_prefixes.as_deref()),
|
||||||
);
|
);
|
||||||
let request_id: String = match request_headers
|
let request_id: String = match request_headers
|
||||||
.get(REQUEST_ID_HEADER)
|
.get(REQUEST_ID_HEADER)
|
||||||
|
|
@ -434,7 +434,7 @@ async fn build_llm_span(
|
||||||
tool_names: Option<Vec<String>>,
|
tool_names: Option<Vec<String>>,
|
||||||
user_message_preview: Option<String>,
|
user_message_preview: Option<String>,
|
||||||
temperature: Option<f32>,
|
temperature: Option<f32>,
|
||||||
llm_providers: &Arc<RwLock<Vec<LlmProvider>>>,
|
llm_providers: &Arc<RwLock<LlmProviders>>,
|
||||||
custom_attrs: &HashMap<String, String>,
|
custom_attrs: &HashMap<String, String>,
|
||||||
) -> common::traces::Span {
|
) -> common::traces::Span {
|
||||||
use crate::tracing::{http, llm, OperationNameBuilder};
|
use crate::tracing::{http, llm, OperationNameBuilder};
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,45 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use common::configuration::{CustomTraceAttribute, CustomTraceAttributeType};
|
|
||||||
use common::traces::SpanBuilder;
|
use common::traces::SpanBuilder;
|
||||||
use hyper::header::{HeaderMap, HeaderName};
|
use hyper::header::HeaderMap;
|
||||||
|
|
||||||
pub fn extract_custom_trace_attributes(
|
pub fn extract_custom_trace_attributes(
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
custom_attributes: Option<&[CustomTraceAttribute]>,
|
span_attribute_header_prefixes: Option<&[String]>,
|
||||||
) -> HashMap<String, String> {
|
) -> HashMap<String, String> {
|
||||||
let mut attributes = HashMap::new();
|
let mut attributes = HashMap::new();
|
||||||
let Some(custom_attributes) = custom_attributes else {
|
let Some(span_attribute_header_prefixes) = span_attribute_header_prefixes else {
|
||||||
return attributes;
|
return attributes;
|
||||||
};
|
};
|
||||||
|
if span_attribute_header_prefixes.is_empty() {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
for attribute in custom_attributes {
|
for (name, value) in headers.iter() {
|
||||||
// Normalize/validate the configured header name; skip invalid names.
|
let header_name = name.as_str();
|
||||||
let header_name = match HeaderName::from_bytes(attribute.header.as_bytes()) {
|
let mut matched_prefix: Option<&str> = None;
|
||||||
Ok(name) => name,
|
for prefix in span_attribute_header_prefixes {
|
||||||
Err(_) => continue,
|
if header_name.starts_with(prefix) {
|
||||||
|
matched_prefix = Some(prefix.as_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(prefix) = matched_prefix else {
|
||||||
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract header value as UTF-8 text; skip missing or invalid values.
|
let raw_value = match value.to_str().ok() {
|
||||||
let raw_value = match headers
|
|
||||||
.get(header_name)
|
|
||||||
.and_then(|value| value.to_str().ok())
|
|
||||||
{
|
|
||||||
Some(value) => value.trim(),
|
Some(value) => value.trim(),
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the header value according to the configured type.
|
let suffix = header_name.strip_prefix(prefix).unwrap_or("");
|
||||||
let parsed_value = match attribute.value_type {
|
let suffix_key = suffix.trim_start_matches('-').replace('-', ".");
|
||||||
CustomTraceAttributeType::Str => Some(raw_value.to_string()),
|
if suffix_key.is_empty() {
|
||||||
CustomTraceAttributeType::Bool => raw_value.parse::<bool>().ok().map(|v| v.to_string()),
|
continue;
|
||||||
CustomTraceAttributeType::Float => raw_value.parse::<f64>().ok().map(|v| v.to_string()),
|
|
||||||
CustomTraceAttributeType::Int => raw_value.parse::<i64>().ok().map(|v| v.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only include attributes that successfully parsed.
|
|
||||||
if let Some(value) = parsed_value {
|
|
||||||
attributes.insert(attribute.key.clone(), value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attributes.insert(suffix_key, raw_value.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes
|
attributes
|
||||||
|
|
@ -48,9 +47,9 @@ pub fn extract_custom_trace_attributes(
|
||||||
|
|
||||||
pub fn collect_custom_trace_attributes(
|
pub fn collect_custom_trace_attributes(
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
custom_attributes: Option<&[CustomTraceAttribute]>,
|
span_attribute_header_prefixes: Option<&[String]>,
|
||||||
) -> HashMap<String, String> {
|
) -> HashMap<String, String> {
|
||||||
extract_custom_trace_attributes(headers, custom_attributes)
|
extract_custom_trace_attributes(headers, span_attribute_header_prefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append_span_attributes(
|
pub fn append_span_attributes(
|
||||||
|
|
@ -66,72 +65,22 @@ pub fn append_span_attributes(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::extract_custom_trace_attributes;
|
use super::extract_custom_trace_attributes;
|
||||||
use common::configuration::{CustomTraceAttribute, CustomTraceAttributeType};
|
|
||||||
use hyper::header::{HeaderMap, HeaderValue};
|
use hyper::header::{HeaderMap, HeaderValue};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extracts_and_parses_custom_headers() {
|
fn extracts_headers_by_prefix() {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert("x-workspace-id", HeaderValue::from_static("ws_123"));
|
headers.insert("x-katanemo-tenant-id", HeaderValue::from_static("ten_456"));
|
||||||
headers.insert("x-tenant-id", HeaderValue::from_static("ten_456"));
|
headers.insert("x-katanemo-user-id", HeaderValue::from_static("usr_789"));
|
||||||
headers.insert("x-user-id", HeaderValue::from_static("usr_789"));
|
headers.insert("x-katanemo-admin-level", HeaderValue::from_static("3"));
|
||||||
headers.insert("x-admin-level", HeaderValue::from_static("3"));
|
headers.insert("x-other-id", HeaderValue::from_static("ignored"));
|
||||||
headers.insert("x-is-internal", HeaderValue::from_static("true"));
|
|
||||||
headers.insert("x-budget", HeaderValue::from_static("42.5"));
|
|
||||||
headers.insert("x-bad-int", HeaderValue::from_static("nope"));
|
|
||||||
|
|
||||||
let custom_attributes = vec![
|
let prefixes = vec!["x-katanemo-".to_string()];
|
||||||
CustomTraceAttribute {
|
let attrs = extract_custom_trace_attributes(&headers, Some(&prefixes));
|
||||||
key: "workspace.id".to_string(),
|
|
||||||
value_type: CustomTraceAttributeType::Str,
|
|
||||||
header: "x-workspace-id".to_string(),
|
|
||||||
},
|
|
||||||
CustomTraceAttribute {
|
|
||||||
key: "tenant.id".to_string(),
|
|
||||||
value_type: CustomTraceAttributeType::Str,
|
|
||||||
header: "x-tenant-id".to_string(),
|
|
||||||
},
|
|
||||||
CustomTraceAttribute {
|
|
||||||
key: "user.id".to_string(),
|
|
||||||
value_type: CustomTraceAttributeType::Str,
|
|
||||||
header: "x-user-id".to_string(),
|
|
||||||
},
|
|
||||||
CustomTraceAttribute {
|
|
||||||
key: "admin.level".to_string(),
|
|
||||||
value_type: CustomTraceAttributeType::Int,
|
|
||||||
header: "x-admin-level".to_string(),
|
|
||||||
},
|
|
||||||
CustomTraceAttribute {
|
|
||||||
key: "is.internal".to_string(),
|
|
||||||
value_type: CustomTraceAttributeType::Bool,
|
|
||||||
header: "x-is-internal".to_string(),
|
|
||||||
},
|
|
||||||
CustomTraceAttribute {
|
|
||||||
key: "budget.value".to_string(),
|
|
||||||
value_type: CustomTraceAttributeType::Float,
|
|
||||||
header: "x-budget".to_string(),
|
|
||||||
},
|
|
||||||
CustomTraceAttribute {
|
|
||||||
key: "bad.int".to_string(),
|
|
||||||
value_type: CustomTraceAttributeType::Int,
|
|
||||||
header: "x-bad-int".to_string(),
|
|
||||||
},
|
|
||||||
CustomTraceAttribute {
|
|
||||||
key: "missing.header".to_string(),
|
|
||||||
value_type: CustomTraceAttributeType::Str,
|
|
||||||
header: "x-missing".to_string(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let attrs = extract_custom_trace_attributes(&headers, Some(&custom_attributes));
|
|
||||||
|
|
||||||
assert_eq!(attrs.get("workspace.id"), Some(&"ws_123".to_string()));
|
|
||||||
assert_eq!(attrs.get("tenant.id"), Some(&"ten_456".to_string()));
|
assert_eq!(attrs.get("tenant.id"), Some(&"ten_456".to_string()));
|
||||||
assert_eq!(attrs.get("user.id"), Some(&"usr_789".to_string()));
|
assert_eq!(attrs.get("user.id"), Some(&"usr_789".to_string()));
|
||||||
assert_eq!(attrs.get("admin.level"), Some(&"3".to_string()));
|
assert_eq!(attrs.get("admin.level"), Some(&"3".to_string()));
|
||||||
assert_eq!(attrs.get("is.internal"), Some(&"true".to_string()));
|
assert!(!attrs.contains_key("other.id"));
|
||||||
assert_eq!(attrs.get("budget.value"), Some(&"42.5".to_string()));
|
|
||||||
assert!(!attrs.contains_key("bad.int"));
|
|
||||||
assert!(!attrs.contains_key("missing.header"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,24 +90,7 @@ pub struct Overrides {
|
||||||
pub struct Tracing {
|
pub struct Tracing {
|
||||||
pub sampling_rate: Option<f64>,
|
pub sampling_rate: Option<f64>,
|
||||||
pub trace_arch_internal: Option<bool>,
|
pub trace_arch_internal: Option<bool>,
|
||||||
pub custom_attributes: Option<Vec<CustomTraceAttribute>>,
|
pub custom_attribute_prefixes: Option<Vec<String>>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CustomTraceAttribute {
|
|
||||||
pub key: String,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub value_type: CustomTraceAttributeType,
|
|
||||||
pub header: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum CustomTraceAttributeType {
|
|
||||||
Str,
|
|
||||||
Bool,
|
|
||||||
Float,
|
|
||||||
Int,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
|
||||||
|
|
|
||||||
|
|
@ -55,22 +55,5 @@ listeners:
|
||||||
|
|
||||||
tracing:
|
tracing:
|
||||||
random_sampling: 100
|
random_sampling: 100
|
||||||
custom_attributes:
|
custom_attribute_prefixes:
|
||||||
- header: x-workspace-id
|
- x-katanemo-
|
||||||
key: workspace.id
|
|
||||||
type: str
|
|
||||||
- header: x-tenant-id
|
|
||||||
key: tenant.id
|
|
||||||
type: str
|
|
||||||
- header: x-user-id
|
|
||||||
key: user.id
|
|
||||||
type: str
|
|
||||||
- header: x-admin-level
|
|
||||||
key: admin.level
|
|
||||||
type: int
|
|
||||||
- header: x-is-internal
|
|
||||||
key: is.internal
|
|
||||||
type: bool
|
|
||||||
- header: x-budget
|
|
||||||
key: budget.value
|
|
||||||
type: float
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@
|
||||||
### Travel Agent Chat Completion Request
|
### Travel Agent Chat Completion Request
|
||||||
POST {{llm_endpoint}}/v1/chat/completions HTTP/1.1
|
POST {{llm_endpoint}}/v1/chat/completions HTTP/1.1
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-Workspace-Id: ws_7e2c5d91b4224f59b0e6a4e0125c21b3
|
X-Katanemo-Workspace-Id: ws_7e2c5d91b4224f59b0e6a4e0125c21b3
|
||||||
X-Tenant-Id: ten_4102a8c7fa6542b084b395d2df184a9a
|
X-Katanemo-Tenant-Id: ten_4102a8c7fa6542b084b395d2df184a9a
|
||||||
X-User-Id: usr_19df7e6751b846f9ba026776e3c12abe
|
X-Katanemo-User-Id: usr_19df7e6751b846f9ba026776e3c12abe
|
||||||
X-Admin-Level: 3
|
X-Katanemo-Admin-Level: 3
|
||||||
X-Is-Internal: true
|
X-Katanemo-Is-Internal: true
|
||||||
X-Budget: 42.5
|
X-Katanemo-Budget: 42.5
|
||||||
|
|
||||||
{
|
{
|
||||||
"model": "gpt-4o",
|
"model": "gpt-5.2",
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
|
|
@ -26,7 +26,28 @@ X-Budget: 42.5
|
||||||
"content": "What is one Alaska flight that goes direct to Atlanta from Seattle?"
|
"content": "What is one Alaska flight that goes direct to Atlanta from Seattle?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"max_tokens": 1000,
|
"max_completion_tokens": 1000,
|
||||||
|
"stream": false,
|
||||||
|
"temperature": 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### Travel Agent Request (prefix mismatch - ignored)
|
||||||
|
POST {{llm_endpoint}}/v1/chat/completions HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
X-Other-Workspace-Id: ws_7e2c5d91b4224f59b0e6a4e0125c21b3
|
||||||
|
X-Other-Tenant-Id: ten_4102a8c7fa6542b084b395d2df184a9a
|
||||||
|
X-Other-User-Id: usr_19df7e6751b846f9ba026776e3c12abe
|
||||||
|
|
||||||
|
{
|
||||||
|
"model": "gpt-5.2",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "What's the weather in Seattle?"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_completion_tokens": 1000,
|
||||||
"stream": false,
|
"stream": false,
|
||||||
"temperature": 1.0
|
"temperature": 1.0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue