use std::borrow::Cow;
use opentelemetry::global::{self, BoxedSpan, BoxedTracer};
use opentelemetry::trace::{
Span, SpanContext, SpanId, SpanKind, Status, TraceFlags, TraceId, TraceState, Tracer,
};
use opentelemetry::{InstrumentationScope, KeyValue};
use super::types::CaptureDecision;
const TRACER_NAME: &str = env!("CARGO_PKG_NAME");
const TRACER_VERSION: &str = env!("CARGO_PKG_VERSION");
fn tracer() -> BoxedTracer {
let scope = InstrumentationScope::builder(TRACER_NAME)
.with_version(TRACER_VERSION)
.build();
global::tracer_with_scope(scope)
}
#[must_use]
pub fn start_internal_span(name: impl Into<Cow<'static, str>>, attrs: Vec<KeyValue>) -> BoxedSpan {
let t = tracer();
t.span_builder(name)
.with_kind(SpanKind::Internal)
.with_attributes(attrs)
.start(&t)
}
#[must_use]
pub fn start_client_span(name: impl Into<Cow<'static, str>>, attrs: Vec<KeyValue>) -> BoxedSpan {
let t = tracer();
t.span_builder(name)
.with_kind(SpanKind::Client)
.with_attributes(attrs)
.start(&t)
}
pub fn set_span_error(span: &mut BoxedSpan, error_type: &str, message: &str) {
span.set_attribute(KeyValue::new(
super::attrs::ERROR_TYPE,
error_type.to_string(),
));
span.set_status(Status::error(message.to_string()));
}
pub fn add_event(span: &mut BoxedSpan, name: impl Into<Cow<'static, str>>, attrs: Vec<KeyValue>) {
if !span.is_recording() {
return;
}
span.add_event(name, attrs);
}
pub fn add_link(span: &mut BoxedSpan, target: SpanContext, attrs: Vec<KeyValue>) {
if !span.is_recording() {
return;
}
if !target.is_valid() {
return;
}
span.add_link(target, attrs);
}
pub fn link_to_replay_origin(
span: &mut BoxedSpan,
original_trace_id: &str,
original_span_id: &str,
attempt_index: u32,
) {
let Some(target) = parse_span_context(original_trace_id, original_span_id) else {
return;
};
add_link(
span,
target,
vec![
KeyValue::new(
super::attrs::AGENT_REPLAY_ORIGINAL_TRACE_ID,
original_trace_id.to_string(),
),
KeyValue::new(
super::attrs::AGENT_REPLAY_ORIGINAL_SPAN_ID,
original_span_id.to_string(),
),
super::attrs::kv_i64(
super::attrs::AGENT_REPLAY_ATTEMPT_INDEX,
i64::from(attempt_index),
),
],
);
}
pub fn link_to_parent_turn(span: &mut BoxedSpan, parent_trace_id: &str, parent_span_id: &str) {
let Some(target) = parse_span_context(parent_trace_id, parent_span_id) else {
return;
};
add_link(span, target, vec![]);
}
#[must_use]
pub fn remote_span_context(trace_hex: &str, span_hex: &str) -> Option<SpanContext> {
parse_span_context(trace_hex, span_hex)
}
fn parse_span_context(trace_hex: &str, span_hex: &str) -> Option<SpanContext> {
let trace_id = TraceId::from_hex(trace_hex).ok()?;
let span_id = SpanId::from_hex(span_hex).ok()?;
let ctx = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
true,
TraceState::default(),
);
if !ctx.is_valid() {
return None;
}
Some(ctx)
}
pub fn record_payload_on_span(
span: &mut BoxedSpan,
result: &super::types::CaptureResult,
system_json: Option<&serde_json::Value>,
input_json: &serde_json::Value,
output_json: &serde_json::Value,
) {
use super::attrs;
if !span.is_recording() {
return;
}
apply_capture_decision(
span,
&result.system_instructions,
system_json,
attrs::GEN_AI_SYSTEM_INSTRUCTIONS,
attrs::SDK_OTEL_SYSTEM_INSTRUCTIONS_REF,
);
apply_capture_decision(
span,
&result.input_messages,
Some(input_json),
attrs::GEN_AI_INPUT_MESSAGES,
attrs::SDK_OTEL_INPUT_MESSAGES_REF,
);
apply_capture_decision(
span,
&result.output_messages,
Some(output_json),
attrs::GEN_AI_OUTPUT_MESSAGES,
attrs::SDK_OTEL_OUTPUT_MESSAGES_REF,
);
}
fn apply_capture_decision(
span: &mut BoxedSpan,
decision: &CaptureDecision,
json_value: Option<&serde_json::Value>,
inline_attr: &'static str,
ref_attr: &'static str,
) {
match decision {
CaptureDecision::Inline => {
if let Some(val) = json_value {
span.set_attribute(KeyValue::new(inline_attr, val.to_string()));
}
}
CaptureDecision::Reference(r) => {
span.set_attribute(KeyValue::new(ref_attr, r.clone()));
}
CaptureDecision::Omit => {}
}
}