use opentelemetry::global::BoxedSpan;
use opentelemetry::trace::Span;
use opentelemetry::{Array, KeyValue, StringValue, Value};
pub const LANGFUSE_TRACE_NAME: &str = "langfuse.trace.name";
pub const LANGFUSE_TRACE_INPUT: &str = "langfuse.trace.input";
pub const LANGFUSE_TRACE_OUTPUT: &str = "langfuse.trace.output";
pub const LANGFUSE_TRACE_TAGS: &str = "langfuse.trace.tags";
pub const LANGFUSE_TRACE_PUBLIC: &str = "langfuse.trace.public";
pub const LANGFUSE_TRACE_METADATA_PREFIX: &str = "langfuse.trace.metadata.";
pub const LANGFUSE_SESSION_ID: &str = "langfuse.session.id";
pub const LANGFUSE_USER_ID: &str = "langfuse.user.id";
pub const LANGFUSE_RELEASE: &str = "langfuse.release";
pub const LANGFUSE_VERSION: &str = "langfuse.version";
pub const LANGFUSE_ENVIRONMENT: &str = "langfuse.environment";
pub const LANGFUSE_OBSERVATION_TYPE: &str = "langfuse.observation.type";
pub const LANGFUSE_OBSERVATION_INPUT: &str = "langfuse.observation.input";
pub const LANGFUSE_OBSERVATION_OUTPUT: &str = "langfuse.observation.output";
pub const LANGFUSE_OBSERVATION_LEVEL: &str = "langfuse.observation.level";
pub const LANGFUSE_OBSERVATION_STATUS_MESSAGE: &str = "langfuse.observation.status_message";
pub const LANGFUSE_OBSERVATION_USAGE_DETAILS: &str = "langfuse.observation.usage_details";
pub const LANGFUSE_OBSERVATION_COST_DETAILS: &str = "langfuse.observation.cost_details";
pub const LANGFUSE_OBSERVATION_MODEL_NAME: &str = "langfuse.observation.model.name";
pub const LANGFUSE_OBSERVATION_PROMPT_NAME: &str = "langfuse.observation.prompt.name";
pub const LANGFUSE_OBSERVATION_PROMPT_VERSION: &str = "langfuse.observation.prompt.version";
pub const LANGFUSE_OBSERVATION_METADATA_PREFIX: &str = "langfuse.observation.metadata.";
pub const DEFAULT_TRACE_TEXT_MAX_CHARS: usize = 10_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ObservationType {
Span,
Generation,
Agent,
Tool,
Chain,
Retriever,
Evaluator,
Embedding,
Guardrail,
Event,
}
impl ObservationType {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Span => "span",
Self::Generation => "generation",
Self::Agent => "agent",
Self::Tool => "tool",
Self::Chain => "chain",
Self::Retriever => "retriever",
Self::Evaluator => "evaluator",
Self::Embedding => "embedding",
Self::Guardrail => "guardrail",
Self::Event => "event",
}
}
}
pub fn tag_observation(span: &mut BoxedSpan, ty: ObservationType) {
if !span.is_recording() {
return;
}
span.set_attribute(KeyValue::new(LANGFUSE_OBSERVATION_TYPE, ty.as_str()));
}
pub fn set_trace_name(span: &mut BoxedSpan, value: impl Into<String>) {
set_string_attribute(span, LANGFUSE_TRACE_NAME, value);
}
pub fn set_trace_input(span: &mut BoxedSpan, value: impl Into<String>) {
set_string_attribute(span, LANGFUSE_TRACE_INPUT, value);
}
pub fn set_trace_output(span: &mut BoxedSpan, value: impl Into<String>) {
set_string_attribute(span, LANGFUSE_TRACE_OUTPUT, value);
}
pub fn set_release(span: &mut BoxedSpan, value: impl Into<String>) {
set_string_attribute(span, LANGFUSE_RELEASE, value);
}
pub fn set_environment(span: &mut BoxedSpan, value: impl Into<String>) {
set_string_attribute(span, LANGFUSE_ENVIRONMENT, value);
}
pub fn set_trace_metadata(span: &mut BoxedSpan, key: &str, value: impl Into<String>) {
if !span.is_recording() {
return;
}
let attr_key = format!("{LANGFUSE_TRACE_METADATA_PREFIX}{key}");
span.set_attribute(KeyValue::new(attr_key, value.into()));
}
pub fn set_trace_tags(span: &mut BoxedSpan, tags: &[String]) {
if !span.is_recording() {
return;
}
let values: Vec<StringValue> = tags.iter().cloned().map(StringValue::from).collect();
span.set_attribute(KeyValue::new(
LANGFUSE_TRACE_TAGS,
Value::Array(Array::String(values)),
));
}
#[must_use]
pub fn truncate_trace_text(text: &str, max_chars: usize) -> String {
if text.is_empty() || max_chars == 0 {
return String::new();
}
if text.chars().count() <= max_chars {
return text.to_string();
}
if max_chars == 1 {
return "…".to_string();
}
let mut out: String = text.chars().take(max_chars - 1).collect();
out.push('…');
out
}
fn set_string_attribute(span: &mut BoxedSpan, key: &'static str, value: impl Into<String>) {
if !span.is_recording() {
return;
}
span.set_attribute(KeyValue::new(key, value.into()));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn observation_type_as_str_round_trip() {
let cases = [
(ObservationType::Span, "span"),
(ObservationType::Generation, "generation"),
(ObservationType::Agent, "agent"),
(ObservationType::Tool, "tool"),
(ObservationType::Chain, "chain"),
(ObservationType::Retriever, "retriever"),
(ObservationType::Evaluator, "evaluator"),
(ObservationType::Embedding, "embedding"),
(ObservationType::Guardrail, "guardrail"),
(ObservationType::Event, "event"),
];
for (variant, expected) in cases {
assert_eq!(variant.as_str(), expected);
}
}
#[test]
fn truncate_trace_text_returns_empty_on_empty_input() {
assert_eq!(truncate_trace_text("", 100), "");
assert_eq!(truncate_trace_text("", 0), "");
}
#[test]
fn truncate_trace_text_returns_empty_when_max_is_zero() {
assert_eq!(truncate_trace_text("anything", 0), "");
}
#[test]
fn truncate_trace_text_no_truncation_when_short() {
assert_eq!(truncate_trace_text("hello", 10), "hello");
assert_eq!(truncate_trace_text("hello", 5), "hello");
}
#[test]
fn truncate_trace_text_max_one_returns_ellipsis_for_overlong_input() {
assert_eq!(truncate_trace_text("hello", 1), "…");
}
#[test]
fn truncate_trace_text_handles_multibyte_chars() {
let input = "ab😀汉字cd";
assert_eq!(input.chars().count(), 7);
let truncated = truncate_trace_text(input, 5);
assert_eq!(truncated.chars().count(), 5);
assert!(truncated.ends_with('…'));
assert_eq!(truncated, "ab😀汉…");
}
#[test]
fn truncate_trace_text_default_ceiling_is_ten_thousand() {
assert_eq!(DEFAULT_TRACE_TEXT_MAX_CHARS, 10_000);
}
}