llm_optimizer_processor/telemetry/
tracing.rs1use crate::telemetry::resource::ResourceBuilder;
7use opentelemetry::{
8 global,
9 trace::{SpanKind, Status, TraceError, Tracer, TracerProvider as _},
10 KeyValue,
11};
12use opentelemetry_otlp::WithExportConfig;
13use opentelemetry_sdk::{
14 propagation::TraceContextPropagator,
15 runtime,
16 trace::{
17 BatchConfig, BatchSpanProcessor, RandomIdGenerator, Sampler, SpanLimits,
18 TracerProvider,
19 },
20 Resource,
21};
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use std::time::Duration;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "lowercase")]
29pub enum OtlpProtocol {
30 Grpc,
32 Http,
34}
35
36impl Default for OtlpProtocol {
37 fn default() -> Self {
38 Self::Grpc
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(tag = "type", rename_all = "snake_case")]
45pub enum SamplingStrategy {
46 AlwaysOn,
48 AlwaysOff,
50 TraceIdRatio { ratio: f64 },
52 ParentBased {
54 #[serde(flatten)]
55 root: Box<SamplingStrategy>,
56 },
57}
58
59impl Default for SamplingStrategy {
60 fn default() -> Self {
61 Self::ParentBased {
62 root: Box::new(Self::TraceIdRatio { ratio: 1.0 }),
63 }
64 }
65}
66
67impl SamplingStrategy {
68 pub fn to_sampler(&self) -> Sampler {
70 match self {
71 Self::AlwaysOn => Sampler::AlwaysOn,
72 Self::AlwaysOff => Sampler::AlwaysOff,
73 Self::TraceIdRatio { ratio } => Sampler::TraceIdRatioBased(*ratio),
74 Self::ParentBased { root } => Sampler::ParentBased(Box::new(root.to_sampler())),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct BatchConfiguration {
82 #[serde(default = "default_max_queue_size")]
84 pub max_queue_size: usize,
85 #[serde(default = "default_max_export_batch_size")]
87 pub max_export_batch_size: usize,
88 #[serde(with = "humantime_serde", default = "default_scheduled_delay")]
90 pub scheduled_delay: Duration,
91 #[serde(with = "humantime_serde", default = "default_export_timeout")]
93 pub export_timeout: Duration,
94}
95
96fn default_max_queue_size() -> usize {
97 2048
98}
99
100fn default_max_export_batch_size() -> usize {
101 512
102}
103
104fn default_scheduled_delay() -> Duration {
105 Duration::from_secs(5)
106}
107
108fn default_export_timeout() -> Duration {
109 Duration::from_secs(30)
110}
111
112impl Default for BatchConfiguration {
113 fn default() -> Self {
114 Self {
115 max_queue_size: default_max_queue_size(),
116 max_export_batch_size: default_max_export_batch_size(),
117 scheduled_delay: default_scheduled_delay(),
118 export_timeout: default_export_timeout(),
119 }
120 }
121}
122
123impl From<BatchConfiguration> for BatchConfig {
124 fn from(_config: BatchConfiguration) -> Self {
125 BatchConfig::default()
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct TraceConfig {
134 #[serde(default = "default_enabled")]
136 pub enabled: bool,
137
138 pub service_name: String,
140
141 #[serde(default = "default_service_version")]
143 pub service_version: String,
144
145 #[serde(default = "default_otlp_endpoint")]
147 pub otlp_endpoint: String,
148
149 #[serde(default)]
151 pub otlp_protocol: OtlpProtocol,
152
153 #[serde(default)]
155 pub sampling: SamplingStrategy,
156
157 #[serde(default)]
159 pub batch_config: BatchConfiguration,
160
161 #[serde(default)]
163 pub headers: HashMap<String, String>,
164
165 #[serde(default)]
167 pub environment: Option<String>,
168}
169
170fn default_enabled() -> bool {
171 true
172}
173
174fn default_service_version() -> String {
175 env!("CARGO_PKG_VERSION").to_string()
176}
177
178fn default_otlp_endpoint() -> String {
179 "http://localhost:4317".to_string()
180}
181
182impl Default for TraceConfig {
183 fn default() -> Self {
184 Self {
185 enabled: default_enabled(),
186 service_name: "llm-optimizer-processor".to_string(),
187 service_version: default_service_version(),
188 otlp_endpoint: default_otlp_endpoint(),
189 otlp_protocol: OtlpProtocol::default(),
190 sampling: SamplingStrategy::default(),
191 batch_config: BatchConfiguration::default(),
192 headers: HashMap::new(),
193 environment: None,
194 }
195 }
196}
197
198pub fn init_tracer_provider(config: TraceConfig) -> Result<TracerProvider, TraceError> {
204 if !config.enabled {
205 tracing::info!("OpenTelemetry tracing is disabled");
206 return Err(TraceError::Other("tracing disabled".into()));
207 }
208
209 global::set_text_map_propagator(TraceContextPropagator::new());
211
212 let resource = ResourceBuilder::new()
214 .with_service_name(config.service_name.clone())
215 .with_service_version(config.service_version.clone())
216 .with_attribute("service.component", "stream-processor");
217
218 let resource = if let Some(env) = config.environment.clone() {
219 resource.with_deployment_environment(env)
220 } else {
221 resource
222 };
223
224 let resource = resource.build();
225
226 let exporter = match config.otlp_protocol {
228 OtlpProtocol::Grpc => {
229 let mut exporter = opentelemetry_otlp::new_exporter()
230 .tonic()
231 .with_endpoint(&config.otlp_endpoint);
232
233 if !config.headers.is_empty() {
235 let mut metadata = tonic::metadata::MetadataMap::new();
236 for (key, value) in &config.headers {
237 if let (Ok(k), Ok(v)) = (
238 key.parse::<tonic::metadata::MetadataKey<_>>(),
239 value.parse::<tonic::metadata::MetadataValue<_>>(),
240 ) {
241 metadata.insert(k, v);
242 }
243 }
244 exporter = exporter.with_metadata(metadata);
245 }
246
247 opentelemetry_otlp::new_pipeline()
248 .tracing()
249 .with_exporter(exporter)
250 .with_trace_config(
251 opentelemetry_sdk::trace::Config::default()
252 .with_sampler(config.sampling.to_sampler())
253 .with_id_generator(RandomIdGenerator::default())
254 .with_resource(resource),
255 )
256 .with_batch_config(config.batch_config.into())
257 .install_batch(runtime::Tokio)?
258 }
259 OtlpProtocol::Http => {
260 opentelemetry_otlp::new_pipeline()
263 .tracing()
264 .with_exporter(
265 opentelemetry_otlp::new_exporter()
266 .tonic()
267 .with_endpoint(&config.otlp_endpoint),
268 )
269 .with_trace_config(
270 opentelemetry_sdk::trace::Config::default()
271 .with_sampler(config.sampling.to_sampler())
272 .with_id_generator(RandomIdGenerator::default())
273 .with_resource(resource),
274 )
275 .with_batch_config(config.batch_config.into())
276 .install_batch(runtime::Tokio)?
277 }
278 };
279
280 Ok(exporter)
281}
282
283pub async fn shutdown_tracer_provider() {
285 global::shutdown_tracer_provider();
286}
287
288pub struct SpanBuilder {
290 name: String,
291 kind: SpanKind,
292 attributes: Vec<KeyValue>,
293}
294
295impl SpanBuilder {
296 pub fn new(name: impl Into<String>) -> Self {
298 Self {
299 name: name.into(),
300 kind: SpanKind::Internal,
301 attributes: Vec::new(),
302 }
303 }
304
305 pub fn with_kind(mut self, kind: SpanKind) -> Self {
307 self.kind = kind;
308 self
309 }
310
311 pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
313 self.attributes
314 .push(KeyValue::new(key.into(), value.into()));
315 self
316 }
317
318 pub fn with_attributes(mut self, attributes: Vec<KeyValue>) -> Self {
320 self.attributes.extend(attributes);
321 self
322 }
323
324 pub fn start(self) -> impl opentelemetry::trace::Span {
326 let tracer = global::tracer("llm-optimizer-processor");
327 let span = tracer
328 .span_builder(self.name)
329 .with_kind(self.kind)
330 .with_attributes(self.attributes)
331 .start(&tracer);
332 span
333 }
334
335 pub fn start_with_tracer<T: Tracer>(
337 self,
338 tracer: &T,
339 ) -> T::Span {
340 tracer
341 .span_builder(self.name)
342 .with_kind(self.kind)
343 .with_attributes(self.attributes)
344 .start(tracer)
345 }
346}
347
348#[macro_export]
358macro_rules! span {
359 ($name:expr) => {
360 $crate::telemetry::SpanBuilder::new($name).start()
361 };
362 ($name:expr, kind = $kind:expr) => {
363 $crate::telemetry::SpanBuilder::new($name)
364 .with_kind($kind)
365 .start()
366 };
367 ($name:expr, $($key:expr => $value:expr),+ $(,)?) => {
368 {
369 let mut builder = $crate::telemetry::SpanBuilder::new($name);
370 $(
371 builder = builder.with_attribute($key, $value);
372 )+
373 builder.start()
374 }
375 };
376 ($name:expr, kind = $kind:expr, $($key:expr => $value:expr),+ $(,)?) => {
377 {
378 let mut builder = $crate::telemetry::SpanBuilder::new($name)
379 .with_kind($kind);
380 $(
381 builder = builder.with_attribute($key, $value);
382 )+
383 builder.start()
384 }
385 };
386}
387
388pub fn record_exception<T: opentelemetry::trace::Span>(
390 span: &mut T,
391 error: &dyn std::error::Error,
392) {
393 span.record_error(error);
394 span.set_status(Status::error(error.to_string()));
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_sampling_strategy_default() {
403 let strategy = SamplingStrategy::default();
404 let sampler = strategy.to_sampler();
405 assert!(matches!(sampler, Sampler::ParentBased(_)));
407 }
408
409 #[test]
410 fn test_sampling_strategy_always_on() {
411 let strategy = SamplingStrategy::AlwaysOn;
412 let sampler = strategy.to_sampler();
413 assert!(matches!(sampler, Sampler::AlwaysOn));
414 }
415
416 #[test]
417 fn test_sampling_strategy_trace_id_ratio() {
418 let strategy = SamplingStrategy::TraceIdRatio { ratio: 0.5 };
419 let sampler = strategy.to_sampler();
420 assert!(matches!(sampler, Sampler::TraceIdRatioBased(_)));
421 }
422
423 #[test]
424 fn test_batch_config_default() {
425 let config = BatchConfiguration::default();
426 assert_eq!(config.max_queue_size, 2048);
427 assert_eq!(config.max_export_batch_size, 512);
428 }
429
430 #[test]
431 fn test_trace_config_default() {
432 let config = TraceConfig::default();
433 assert!(config.enabled);
434 assert_eq!(config.service_name, "llm-optimizer-processor");
435 assert_eq!(config.otlp_endpoint, "http://localhost:4317");
436 assert_eq!(config.otlp_protocol, OtlpProtocol::Grpc);
437 }
438
439 #[test]
440 fn test_span_builder() {
441 let builder = SpanBuilder::new("test-span")
442 .with_kind(SpanKind::Server)
443 .with_attribute("test.key", "test.value");
444
445 assert_eq!(builder.name, "test-span");
446 assert_eq!(builder.kind, SpanKind::Server);
447 assert_eq!(builder.attributes.len(), 1);
448 }
449
450 #[test]
451 fn test_otlp_protocol_serialization() {
452 let grpc = OtlpProtocol::Grpc;
453 let http = OtlpProtocol::Http;
454
455 let grpc_json = serde_json::to_string(&grpc).unwrap();
456 let http_json = serde_json::to_string(&http).unwrap();
457
458 assert_eq!(grpc_json, "\"grpc\"");
459 assert_eq!(http_json, "\"http\"");
460 }
461}