Skip to main content

chio_kernel/
otel.rs

1//! OpenTelemetry semantic-convention helpers for Chio tool calls.
2
3#[cfg(feature = "otel")]
4use opentelemetry_semantic_conventions::attribute as semconv;
5
6/// Schema URL carried by `opentelemetry-semantic-conventions` 0.29.0.
7#[cfg(feature = "otel")]
8pub const OTEL_SEMCONV_SCHEMA_URL: &str = opentelemetry_semantic_conventions::SCHEMA_URL;
9
10/// Fallback schema URL used when the optional semconv crate is disabled.
11#[cfg(not(feature = "otel"))]
12pub const OTEL_SEMCONV_SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.31.0";
13
14/// Locked span name for GenAI tool calls.
15pub const GEN_AI_TOOL_CALL_SPAN_NAME: &str = "gen_ai.tool.call";
16
17/// Locked operation value for Chio mediated tool calls.
18pub const GEN_AI_TOOL_CALL_OPERATION_NAME: &str = "tool.call";
19
20#[cfg(feature = "otel")]
21pub const ATTR_GEN_AI_SYSTEM: &str = semconv::GEN_AI_SYSTEM;
22#[cfg(not(feature = "otel"))]
23pub const ATTR_GEN_AI_SYSTEM: &str = "gen_ai.system";
24
25#[cfg(feature = "otel")]
26pub const ATTR_GEN_AI_OPERATION_NAME: &str = semconv::GEN_AI_OPERATION_NAME;
27#[cfg(not(feature = "otel"))]
28pub const ATTR_GEN_AI_OPERATION_NAME: &str = "gen_ai.operation.name";
29
30#[cfg(feature = "otel")]
31pub const ATTR_GEN_AI_REQUEST_MODEL: &str = semconv::GEN_AI_REQUEST_MODEL;
32#[cfg(not(feature = "otel"))]
33pub const ATTR_GEN_AI_REQUEST_MODEL: &str = "gen_ai.request.model";
34
35#[cfg(feature = "otel")]
36pub const ATTR_GEN_AI_TOOL_CALL_ID: &str = semconv::GEN_AI_TOOL_CALL_ID;
37#[cfg(not(feature = "otel"))]
38pub const ATTR_GEN_AI_TOOL_CALL_ID: &str = "gen_ai.tool.call.id";
39
40#[cfg(feature = "otel")]
41pub const ATTR_GEN_AI_TOOL_NAME: &str = semconv::GEN_AI_TOOL_NAME;
42#[cfg(not(feature = "otel"))]
43pub const ATTR_GEN_AI_TOOL_NAME: &str = "gen_ai.tool.name";
44
45#[cfg(feature = "otel")]
46pub const ATTR_GEN_AI_RESPONSE_FINISH_REASONS: &str = semconv::GEN_AI_RESPONSE_FINISH_REASONS;
47#[cfg(not(feature = "otel"))]
48pub const ATTR_GEN_AI_RESPONSE_FINISH_REASONS: &str = "gen_ai.response.finish_reasons";
49
50#[cfg(feature = "otel")]
51pub const ATTR_GEN_AI_USAGE_INPUT_TOKENS: &str = semconv::GEN_AI_USAGE_INPUT_TOKENS;
52#[cfg(not(feature = "otel"))]
53pub const ATTR_GEN_AI_USAGE_INPUT_TOKENS: &str = "gen_ai.usage.input_tokens";
54
55#[cfg(feature = "otel")]
56pub const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS: &str = semconv::GEN_AI_USAGE_OUTPUT_TOKENS;
57#[cfg(not(feature = "otel"))]
58pub const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS: &str = "gen_ai.usage.output_tokens";
59
60pub const ATTR_CHIO_RECEIPT_ID: &str = "chio.receipt.id";
61pub const ATTR_CHIO_KERNEL_ID: &str = "chio.kernel.id";
62pub const ATTR_CHIO_SERVER_ID: &str = "chio.server.id";
63pub const ATTR_CHIO_AGENT_ID: &str = "chio.agent.id";
64
65/// Attribute names allowed on `gen_ai.tool.call` spans.
66pub const GEN_AI_TOOL_CALL_LOCKED_ATTRIBUTES: [&str; 12] = [
67    ATTR_GEN_AI_SYSTEM,
68    ATTR_GEN_AI_OPERATION_NAME,
69    ATTR_GEN_AI_REQUEST_MODEL,
70    ATTR_GEN_AI_TOOL_CALL_ID,
71    ATTR_GEN_AI_TOOL_NAME,
72    ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
73    ATTR_GEN_AI_USAGE_INPUT_TOKENS,
74    ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
75    ATTR_CHIO_RECEIPT_ID,
76    ATTR_CHIO_KERNEL_ID,
77    ATTR_CHIO_SERVER_ID,
78    ATTR_CHIO_AGENT_ID,
79];
80
81pub const ATTRIBUTE_VALUE_MAX_CHARS: usize = 128;
82pub const FINISH_REASONS_MAX_CHARS: usize = 192;
83
84/// Cardinality class for span attributes.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum AttributeCardinality {
87    /// Low-cardinality attribute with a small stable vocabulary.
88    Low,
89    /// Bounded attribute whose value is dynamic but length capped.
90    Bounded,
91    /// Identifier attribute. Length is capped and values must not be used
92    /// for aggregation dimensions without sampling.
93    High,
94}
95
96/// One OTel-compatible span attribute.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct OtelAttribute {
99    pub key: &'static str,
100    pub value: serde_json::Value,
101}
102
103/// OTel-compatible GenAI tool-call span representation.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct GenAiToolCallSpan {
106    pub name: &'static str,
107    pub schema_url: &'static str,
108    pub attributes: Vec<OtelAttribute>,
109}
110
111/// Inputs for building a locked `gen_ai.tool.call` span.
112#[derive(Debug, Clone, Copy)]
113pub struct GenAiToolCallSpanInput<'a> {
114    pub system: &'a str,
115    pub request_model: Option<&'a str>,
116    pub tool_call_id: &'a str,
117    pub tool_name: &'a str,
118    pub finish_reasons: &'a [&'a str],
119    pub usage_input_tokens: Option<u64>,
120    pub usage_output_tokens: Option<u64>,
121    pub chio_receipt_id: Option<&'a str>,
122    pub chio_kernel_id: Option<&'a str>,
123    pub chio_server_id: Option<&'a str>,
124    pub chio_agent_id: Option<&'a str>,
125}
126
127impl GenAiToolCallSpan {
128    pub fn attribute_keys(&self) -> impl Iterator<Item = &'static str> + '_ {
129        self.attributes.iter().map(|attribute| attribute.key)
130    }
131}
132
133/// Build a span with the locked attribute set.
134pub fn build_gen_ai_tool_call_span(input: GenAiToolCallSpanInput<'_>) -> GenAiToolCallSpan {
135    let mut attributes = Vec::with_capacity(GEN_AI_TOOL_CALL_LOCKED_ATTRIBUTES.len());
136    push_attr(&mut attributes, ATTR_GEN_AI_SYSTEM, input.system);
137    push_attr(
138        &mut attributes,
139        ATTR_GEN_AI_OPERATION_NAME,
140        GEN_AI_TOOL_CALL_OPERATION_NAME,
141    );
142    if let Some(request_model) = input.request_model {
143        push_attr(&mut attributes, ATTR_GEN_AI_REQUEST_MODEL, request_model);
144    }
145    push_attr(
146        &mut attributes,
147        ATTR_GEN_AI_TOOL_CALL_ID,
148        input.tool_call_id,
149    );
150    push_attr(&mut attributes, ATTR_GEN_AI_TOOL_NAME, input.tool_name);
151    if !input.finish_reasons.is_empty() {
152        push_string_array_attr(
153            &mut attributes,
154            ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
155            input.finish_reasons,
156        );
157    }
158    if let Some(tokens) = input.usage_input_tokens {
159        push_u64_attr(&mut attributes, ATTR_GEN_AI_USAGE_INPUT_TOKENS, tokens);
160    }
161    if let Some(tokens) = input.usage_output_tokens {
162        push_u64_attr(&mut attributes, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, tokens);
163    }
164    if let Some(receipt_id) = input.chio_receipt_id {
165        push_attr(&mut attributes, ATTR_CHIO_RECEIPT_ID, receipt_id);
166    }
167    if let Some(kernel_id) = input.chio_kernel_id {
168        push_attr(&mut attributes, ATTR_CHIO_KERNEL_ID, kernel_id);
169    }
170    if let Some(server_id) = input.chio_server_id {
171        push_attr(&mut attributes, ATTR_CHIO_SERVER_ID, server_id);
172    }
173    if let Some(agent_id) = input.chio_agent_id {
174        push_attr(&mut attributes, ATTR_CHIO_AGENT_ID, agent_id);
175    }
176
177    GenAiToolCallSpan {
178        name: GEN_AI_TOOL_CALL_SPAN_NAME,
179        schema_url: OTEL_SEMCONV_SCHEMA_URL,
180        attributes,
181    }
182}
183
184pub fn attribute_cardinality(key: &str) -> Option<AttributeCardinality> {
185    match key {
186        ATTR_GEN_AI_SYSTEM | ATTR_GEN_AI_OPERATION_NAME | ATTR_GEN_AI_RESPONSE_FINISH_REASONS => {
187            Some(AttributeCardinality::Low)
188        }
189        ATTR_GEN_AI_REQUEST_MODEL
190        | ATTR_GEN_AI_TOOL_NAME
191        | ATTR_GEN_AI_USAGE_INPUT_TOKENS
192        | ATTR_GEN_AI_USAGE_OUTPUT_TOKENS
193        | ATTR_CHIO_KERNEL_ID
194        | ATTR_CHIO_SERVER_ID
195        | ATTR_CHIO_AGENT_ID => Some(AttributeCardinality::Bounded),
196        ATTR_GEN_AI_TOOL_CALL_ID | ATTR_CHIO_RECEIPT_ID => Some(AttributeCardinality::High),
197        _ => None,
198    }
199}
200
201pub fn is_locked_attribute(key: &str) -> bool {
202    GEN_AI_TOOL_CALL_LOCKED_ATTRIBUTES.contains(&key)
203}
204
205fn push_attr(attributes: &mut Vec<OtelAttribute>, key: &'static str, value: &str) {
206    push_attr_with_limit(attributes, key, value, ATTRIBUTE_VALUE_MAX_CHARS);
207}
208
209fn push_attr_with_limit(
210    attributes: &mut Vec<OtelAttribute>,
211    key: &'static str,
212    value: &str,
213    max_chars: usize,
214) {
215    attributes.push(OtelAttribute {
216        key,
217        value: serde_json::Value::String(truncate_chars(value, max_chars)),
218    });
219}
220
221fn push_string_array_attr(attributes: &mut Vec<OtelAttribute>, key: &'static str, values: &[&str]) {
222    attributes.push(OtelAttribute {
223        key,
224        value: serde_json::Value::Array(
225            values
226                .iter()
227                .map(|value| {
228                    serde_json::Value::String(truncate_chars(value, FINISH_REASONS_MAX_CHARS))
229                })
230                .collect(),
231        ),
232    });
233}
234
235fn push_u64_attr(attributes: &mut Vec<OtelAttribute>, key: &'static str, value: u64) {
236    attributes.push(OtelAttribute {
237        key,
238        value: serde_json::Value::Number(serde_json::Number::from(value)),
239    });
240}
241
242fn truncate_chars(value: &str, max_chars: usize) -> String {
243    value.chars().take(max_chars).collect()
244}