everruns_core/
telemetry.rs1#[cfg(feature = "telemetry")]
12use opentelemetry::KeyValue;
13#[cfg(feature = "telemetry")]
14use opentelemetry::trace::TracerProvider as _;
15#[cfg(feature = "telemetry")]
16use opentelemetry_otlp::{SpanExporter, WithExportConfig};
17#[cfg(feature = "telemetry")]
18use opentelemetry_sdk::{
19 Resource,
20 trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
21};
22#[cfg(feature = "telemetry")]
23use std::time::Duration;
24#[cfg(feature = "telemetry")]
25use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
26
27pub mod gen_ai {
34 pub const OPERATION_NAME: &str = "gen_ai.operation.name";
37 pub const PROVIDER_NAME: &str = "gen_ai.provider.name";
39
40 pub const REQUEST_MODEL: &str = "gen_ai.request.model";
43 pub const REQUEST_MAX_TOKENS: &str = "gen_ai.request.max_tokens";
45 pub const REQUEST_TEMPERATURE: &str = "gen_ai.request.temperature";
47 pub const REQUEST_TOP_P: &str = "gen_ai.request.top_p";
49 pub const REQUEST_TOP_K: &str = "gen_ai.request.top_k";
51 pub const REQUEST_FREQUENCY_PENALTY: &str = "gen_ai.request.frequency_penalty";
53 pub const REQUEST_PRESENCE_PENALTY: &str = "gen_ai.request.presence_penalty";
55 pub const REQUEST_STOP_SEQUENCES: &str = "gen_ai.request.stop_sequences";
57 pub const REQUEST_SEED: &str = "gen_ai.request.seed";
59
60 pub const RESPONSE_ID: &str = "gen_ai.response.id";
63 pub const RESPONSE_MODEL: &str = "gen_ai.response.model";
65 pub const RESPONSE_FINISH_REASONS: &str = "gen_ai.response.finish_reasons";
67
68 pub const USAGE_INPUT_TOKENS: &str = "gen_ai.usage.input_tokens";
71 pub const USAGE_OUTPUT_TOKENS: &str = "gen_ai.usage.output_tokens";
73 pub const USAGE_CACHE_READ_TOKENS: &str = "gen_ai.usage.cache_read_tokens";
75 pub const USAGE_CACHE_CREATION_TOKENS: &str = "gen_ai.usage.cache_creation_tokens";
77
78 pub const INPUT_MESSAGES: &str = "gen_ai.input.messages";
81 pub const OUTPUT_MESSAGES: &str = "gen_ai.output.messages";
83 pub const SYSTEM_INSTRUCTIONS: &str = "gen_ai.system_instructions";
85 pub const TOOL_DEFINITIONS: &str = "gen_ai.tool.definitions";
87
88 pub const TOOL_NAME: &str = "gen_ai.tool.name";
91 pub const TOOL_TYPE: &str = "gen_ai.tool.type";
93 pub const TOOL_DESCRIPTION: &str = "gen_ai.tool.description";
95 pub const TOOL_CALL_ID: &str = "gen_ai.tool.call.id";
97 pub const TOOL_CALL_ARGUMENTS: &str = "gen_ai.tool.call.arguments";
99 pub const TOOL_CALL_RESULT: &str = "gen_ai.tool.call.result";
101
102 pub const CONVERSATION_ID: &str = "gen_ai.conversation.id";
105
106 pub const EMBEDDINGS_DIMENSION_COUNT: &str = "gen_ai.embeddings.dimension.count";
109 pub const REQUEST_ENCODING_FORMATS: &str = "gen_ai.request.encoding_formats";
111
112 pub const REQUEST_CHOICE_COUNT: &str = "gen_ai.request.choice.count";
115
116 pub const OUTPUT_TYPE: &str = "gen_ai.output.type";
119
120 pub const AGENT_ID: &str = "gen_ai.agent.id";
123 pub const AGENT_NAME: &str = "gen_ai.agent.name";
125 pub const AGENT_DESCRIPTION: &str = "gen_ai.agent.description";
127
128 pub const SERVER_ADDRESS: &str = "server.address";
131 pub const SERVER_PORT: &str = "server.port";
133
134 pub const SYSTEM: &str = "gen_ai.system";
137
138 pub mod operation {
140 pub const CHAT: &str = "chat";
141 pub const EMBEDDINGS: &str = "embeddings";
142 pub const TEXT_COMPLETION: &str = "text_completion";
143 pub const GENERATE_CONTENT: &str = "generate_content";
144 pub const EXECUTE_TOOL: &str = "execute_tool";
145 pub const CREATE_AGENT: &str = "create_agent";
146 pub const INVOKE_AGENT: &str = "invoke_agent";
147 pub const REASON: &str = "reason";
149 pub const ACT: &str = "act";
150 pub const THINKING: &str = "thinking";
151 }
152
153 pub mod provider {
155 pub const OPENAI: &str = "openai";
156 pub const ANTHROPIC: &str = "anthropic";
157 }
158
159 pub mod tool_type {
161 pub const FUNCTION: &str = "function";
162 pub const EXTENSION: &str = "extension";
163 pub const DATASTORE: &str = "datastore";
164 }
165
166 pub mod output_type {
168 pub const TEXT: &str = "text";
169 pub const IMAGE: &str = "image";
170 pub const JSON: &str = "json";
171 pub const SPEECH: &str = "speech";
172 }
173}
174
175#[derive(Debug, Clone)]
181pub struct TelemetryConfig {
182 pub service_name: String,
184 pub service_version: Option<String>,
186 pub otlp_endpoint: Option<String>,
188 pub environment: Option<String>,
190 pub enable_console: bool,
192 pub log_filter: Option<String>,
194 pub record_content: bool,
197}
198
199impl Default for TelemetryConfig {
200 fn default() -> Self {
201 Self {
202 service_name: "everruns".to_string(),
203 service_version: None,
204 otlp_endpoint: None,
205 environment: None,
206 enable_console: true,
207 log_filter: None,
208 record_content: false,
209 }
210 }
211}
212
213impl TelemetryConfig {
214 pub fn from_env() -> Self {
226 use crate::config::{env_bool, env_opt_string, env_string};
227
228 let sdk_disabled = env_bool("OTEL_SDK_DISABLED", false);
229
230 Self {
231 service_name: env_string("OTEL_SERVICE_NAME", "everruns"),
232 service_version: env_opt_string("OTEL_SERVICE_VERSION"),
233 otlp_endpoint: if sdk_disabled {
234 None
235 } else {
236 env_opt_string("OTEL_EXPORTER_OTLP_ENDPOINT")
237 },
238 environment: env_opt_string("OTEL_ENVIRONMENT"),
239 enable_console: true,
240 log_filter: env_opt_string("RUST_LOG").or_else(|| env_opt_string("LOG_LEVEL")),
241 record_content: std::env::var("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT")
243 .or_else(|_| std::env::var("OTEL_RECORD_CONTENT"))
244 .map(|v| v.to_lowercase() == "true")
245 .unwrap_or(false),
246 }
247 }
248}
249
250#[cfg(feature = "telemetry")]
256pub struct TelemetryGuard {
257 _provider: Option<SdkTracerProvider>,
258}
259
260#[cfg(feature = "telemetry")]
261impl Drop for TelemetryGuard {
262 fn drop(&mut self) {
263 if let Some(provider) = self._provider.take()
264 && let Err(e) = provider.shutdown()
265 {
266 eprintln!("Failed to shutdown tracer provider: {:?}", e);
267 }
268 }
269}
270
271pub fn install_crypto_provider() {
279 let _ = rustls::crypto::ring::default_provider().install_default();
281}
282
283#[cfg(feature = "telemetry")]
301pub fn init_telemetry(config: TelemetryConfig) -> TelemetryGuard {
302 install_crypto_provider();
303
304 let mut resource_attrs = vec![KeyValue::new("service.name", config.service_name.clone())];
306
307 if let Some(version) = &config.service_version {
308 resource_attrs.push(KeyValue::new("service.version", version.clone()));
309 }
310
311 if let Some(env) = &config.environment {
312 resource_attrs.push(KeyValue::new("deployment.environment", env.clone()));
313 }
314
315 let resource = Resource::builder().with_attributes(resource_attrs).build();
316
317 let filter = config
319 .log_filter
320 .as_ref()
321 .and_then(|f| EnvFilter::try_new(f).ok())
322 .unwrap_or_else(|| EnvFilter::new("info"));
323
324 let console_layer = if config.enable_console {
326 Some(
327 tracing_subscriber::fmt::layer()
328 .with_target(true)
329 .with_filter(filter),
330 )
331 } else {
332 None
333 };
334
335 let (tracer_provider, otel_layer, otel_status) = if let Some(endpoint) = &config.otlp_endpoint {
337 match build_otlp_tracer(endpoint, resource) {
338 Ok((provider, tracer)) => {
339 let layer = tracing_opentelemetry::layer().with_tracer(tracer);
340 (Some(provider), Some(layer), Some(Ok(endpoint.clone())))
341 }
342 Err(e) => (None, None, Some(Err(e.to_string()))),
343 }
344 } else {
345 (None, None, None)
346 };
347
348 tracing_subscriber::registry()
350 .with(console_layer)
351 .with(otel_layer)
352 .init();
353
354 match otel_status {
356 Some(Ok(endpoint)) => {
357 tracing::info!(endpoint = %endpoint, "OpenTelemetry tracing enabled");
358 }
359 Some(Err(e)) => {
360 tracing::warn!(error = %e, "Failed to initialize OTLP tracer, continuing without tracing");
361 }
362 None => {
363 tracing::debug!("OpenTelemetry tracing disabled: OTEL_EXPORTER_OTLP_ENDPOINT not set");
364 }
365 }
366
367 TelemetryGuard {
368 _provider: tracer_provider,
369 }
370}
371
372#[cfg(feature = "telemetry")]
373fn build_otlp_tracer(
374 endpoint: &str,
375 resource: Resource,
376) -> Result<
377 (SdkTracerProvider, opentelemetry_sdk::trace::Tracer),
378 Box<dyn std::error::Error + Send + Sync>,
379> {
380 let exporter = SpanExporter::builder()
382 .with_http()
383 .with_endpoint(endpoint)
384 .with_timeout(Duration::from_secs(10))
385 .build()?;
386
387 let provider = SdkTracerProvider::builder()
388 .with_batch_exporter(exporter)
389 .with_sampler(Sampler::AlwaysOn)
390 .with_id_generator(RandomIdGenerator::default())
391 .with_resource(resource)
392 .build();
393
394 let tracer = provider.tracer("everruns");
395
396 Ok((provider, tracer))
397}
398
399pub fn chat_span_name(model: &str) -> String {
408 format!("{} {}", gen_ai::operation::CHAT, model)
409}
410
411pub fn tool_span_name(tool_name: &str) -> String {
416 format!("{} {}", gen_ai::operation::EXECUTE_TOOL, tool_name)
417}
418
419pub fn text_completion_span_name(model: &str) -> String {
424 format!("{} {}", gen_ai::operation::TEXT_COMPLETION, model)
425}
426
427pub fn create_agent_span_name(agent_name: &str) -> String {
432 format!("{} {}", gen_ai::operation::CREATE_AGENT, agent_name)
433}
434
435pub fn invoke_agent_span_name(agent_name: &str) -> String {
440 format!("{} {}", gen_ai::operation::INVOKE_AGENT, agent_name)
441}
442
443pub fn embeddings_span_name(model: &str) -> String {
448 format!("{} {}", gen_ai::operation::EMBEDDINGS, model)
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn test_chat_span_name() {
457 assert_eq!(chat_span_name("gpt-4"), "chat gpt-4");
458 assert_eq!(chat_span_name("claude-3-opus"), "chat claude-3-opus");
459 }
460
461 #[test]
462 fn test_tool_span_name() {
463 assert_eq!(tool_span_name("read_file"), "execute_tool read_file");
464 assert_eq!(tool_span_name("web_search"), "execute_tool web_search");
465 }
466
467 #[test]
468 fn test_text_completion_span_name() {
469 assert_eq!(
470 text_completion_span_name("gpt-3.5-turbo-instruct"),
471 "text_completion gpt-3.5-turbo-instruct"
472 );
473 }
474
475 #[test]
476 fn test_create_agent_span_name() {
477 assert_eq!(
478 create_agent_span_name("customer_support"),
479 "create_agent customer_support"
480 );
481 }
482
483 #[test]
484 fn test_invoke_agent_span_name() {
485 assert_eq!(
486 invoke_agent_span_name("customer_support"),
487 "invoke_agent customer_support"
488 );
489 }
490
491 #[test]
492 fn test_embeddings_span_name() {
493 assert_eq!(
494 embeddings_span_name("text-embedding-ada-002"),
495 "embeddings text-embedding-ada-002"
496 );
497 }
498
499 #[test]
500 fn test_config_defaults() {
501 let config = TelemetryConfig::default();
502 assert_eq!(config.service_name, "everruns");
503 assert!(config.otlp_endpoint.is_none());
504 assert!(config.enable_console);
505 assert!(!config.record_content);
506 }
507}