use anyhow::{Context, Result};
use prost::Message;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use crate::core::telemetry::types::*;
#[derive(Clone, PartialEq, Message)]
pub struct ExportTraceServiceRequest {
#[prost(message, repeated, tag = "1")]
pub resource_spans: Vec<ResourceSpans>,
}
#[derive(Clone, PartialEq, Message)]
pub struct ResourceSpans {
#[prost(message, optional, tag = "1")]
pub resource: Option<Resource>,
#[prost(message, repeated, tag = "2")]
pub scope_spans: Vec<ScopeSpans>,
}
#[derive(Clone, PartialEq, Message)]
pub struct Resource {
#[prost(message, repeated, tag = "1")]
pub attributes: Vec<KeyValue>,
}
#[derive(Clone, PartialEq, Message)]
pub struct ScopeSpans {
#[prost(message, repeated, tag = "2")]
pub spans: Vec<ProtobufSpan>,
}
#[derive(Clone, PartialEq, Message)]
pub struct ProtobufSpan {
#[prost(string, tag = "2")]
pub name: String,
#[prost(fixed64, tag = "7")]
pub start_time_unix_nano: u64,
#[prost(fixed64, tag = "8")]
pub end_time_unix_nano: u64,
#[prost(message, repeated, tag = "9")]
pub attributes: Vec<KeyValue>,
#[prost(message, optional, tag = "15")]
pub status: Option<Status>,
}
#[derive(Clone, PartialEq, Message)]
pub struct Status {
#[prost(enumeration = "StatusCode", tag = "2")]
pub code: i32,
#[prost(string, tag = "3")]
pub message: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, prost::Enumeration)]
#[repr(i32)]
pub enum StatusCode {
Unset = 0,
Ok = 1,
Error = 2,
}
#[derive(Clone, PartialEq, Message)]
pub struct KeyValue {
#[prost(string, tag = "1")]
pub key: String,
#[prost(message, optional, tag = "2")]
pub value: Option<AnyValue>,
}
#[derive(Clone, PartialEq, Message)]
pub struct AnyValue {
#[prost(oneof = "any_value::Value", tags = "1, 2, 3, 4")]
pub value: Option<any_value::Value>,
}
pub mod any_value {
#[allow(clippy::enum_variant_names)]
#[derive(Clone, PartialEq, prost::Oneof)]
pub enum Value {
#[prost(string, tag = "1")]
StringValue(String),
#[prost(int64, tag = "2")]
IntValue(i64),
#[prost(double, tag = "3")]
DoubleValue(f64),
#[prost(bool, tag = "4")]
BoolValue(bool),
}
}
pub trait OtlpParser {
fn parse_file(&self, path: &Path) -> Result<ParsedTelemetry>;
fn parse_bytes(&self, data: &[u8]) -> Result<ParsedTelemetry>;
}
pub struct JsonParser;
impl JsonParser {
pub fn new() -> Self {
Self
}
}
impl Default for JsonParser {
fn default() -> Self {
Self::new()
}
}
impl JsonParser {
fn extract_spans(&self, otlp: OtlpJson) -> Vec<TelemetrySpan> {
let mut spans = Vec::new();
if let Some(resource_spans) = otlp.resource_spans {
for rs in resource_spans {
let service_name = rs
.resource
.as_ref()
.and_then(|r| r.attributes.as_ref())
.and_then(|attrs| {
attrs
.iter()
.find(|a| a.key == "service.name")
.and_then(|a| a.value.as_ref())
.and_then(|v| v.string_value.clone())
});
if let Some(scope_spans) = rs.scope_spans {
for ss in scope_spans {
if let Some(span_list) = ss.spans {
for span in span_list {
if let Some(telemetry_span) =
self.convert_span(span, service_name.clone())
{
spans.push(telemetry_span);
}
}
}
}
}
}
}
spans
}
fn convert_span(&self, span: Span, service_name: Option<String>) -> Option<TelemetrySpan> {
let mut telemetry_span = TelemetrySpan {
name: span.name,
function_name: None,
file_path: None,
line_number: None,
service_name,
start_time_nanos: span
.start_time_unix_nano
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0),
end_time_nanos: span
.end_time_unix_nano
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0),
duration_ms: 0.0,
attributes: HashMap::new(),
};
if let Some(attributes) = &span.attributes {
for attr in attributes {
match attr.key.as_str() {
"code.function.name" => {
telemetry_span.function_name =
attr.value.as_ref().and_then(|v| v.string_value.clone());
}
"code.file.path" | "code.filepath" => {
telemetry_span.file_path = attr
.value
.as_ref()
.and_then(|v| v.string_value.as_ref())
.map(|p| Path::new(p).to_path_buf());
}
"code.line.number" | "code.lineno" => {
telemetry_span.line_number = attr
.value
.as_ref()
.and_then(|v| v.int_value.as_ref())
.and_then(|s| s.parse::<u32>().ok());
}
_ => {
if let Some(value) = &attr.value {
if let Some(attr_value) =
self.convert_json_attribute_value(value.clone())
{
telemetry_span
.attributes
.insert(attr.key.clone(), attr_value);
}
}
}
}
}
}
telemetry_span.calculate_duration_ms();
Some(telemetry_span)
}
fn convert_json_attribute_value(
&self,
value: crate::core::telemetry::types::AnyValue,
) -> Option<AttributeValue> {
if let Some(s) = value.string_value {
Some(AttributeValue::String(s))
} else if let Some(i) = value.int_value {
i.parse::<i64>().ok().map(AttributeValue::Int)
} else if let Some(d) = value.double_value {
Some(AttributeValue::Double(d))
} else {
value.bool_value.map(AttributeValue::Bool)
}
}
}
impl OtlpParser for JsonParser {
fn parse_file(&self, path: &Path) -> Result<ParsedTelemetry> {
let data = fs::read(path)
.with_context(|| format!("Failed to read telemetry file: {}", path.display()))?;
self.parse_bytes(&data)
}
fn parse_bytes(&self, data: &[u8]) -> Result<ParsedTelemetry> {
let otlp: OtlpJson = serde_json::from_slice(data).context("Failed to parse OTLP JSON")?;
let spans = self.extract_spans(otlp);
let code_spans = spans
.iter()
.filter(|s| s.function_name.is_some() || s.file_path.is_some())
.cloned()
.collect();
Ok(ParsedTelemetry { spans, code_spans })
}
}
pub struct ProtobufParser;
impl ProtobufParser {
pub fn new() -> Self {
Self
}
}
impl Default for ProtobufParser {
fn default() -> Self {
Self::new()
}
}
impl ProtobufParser {
fn extract_spans(&self, otlp: ExportTraceServiceRequest) -> Vec<TelemetrySpan> {
let mut spans = Vec::new();
for resource_span in otlp.resource_spans {
let service_name = resource_span.resource.as_ref().and_then(|r| {
r.attributes
.iter()
.find(|attr| attr.key == "service.name")
.and_then(|attr| attr.value.as_ref())
.and_then(|v| match &v.value {
Some(any_value::Value::StringValue(s)) => Some(s.clone()),
_ => None,
})
});
for scope_span in resource_span.scope_spans {
for span in scope_span.spans {
if let Some(telemetry_span) =
self.convert_protobuf_span(span, service_name.clone())
{
spans.push(telemetry_span);
}
}
}
}
spans
}
fn convert_protobuf_span(
&self,
span: ProtobufSpan,
service_name: Option<String>,
) -> Option<TelemetrySpan> {
let mut telemetry_span = TelemetrySpan {
name: span.name,
function_name: None,
file_path: None,
line_number: None,
service_name,
start_time_nanos: span.start_time_unix_nano,
end_time_nanos: span.end_time_unix_nano,
duration_ms: 0.0,
attributes: HashMap::new(),
};
for attr in span.attributes {
match attr.key.as_str() {
"code.function.name" => {
if let Some(value) = attr.value.and_then(|v| match v.value {
Some(any_value::Value::StringValue(s)) => Some(s),
_ => None,
}) {
telemetry_span.function_name = Some(value);
}
}
"code.file.path" | "code.filepath" => {
if let Some(value) = attr.value.and_then(|v| match v.value {
Some(any_value::Value::StringValue(s)) => Some(s),
_ => None,
}) {
telemetry_span.file_path = Some(Path::new(&value).to_path_buf());
}
}
"code.line.number" | "code.lineno" => {
if let Some(value) = attr.value.and_then(|v| match v.value {
Some(any_value::Value::IntValue(i)) => Some(i as u32),
_ => None,
}) {
telemetry_span.line_number = Some(value);
}
}
_ => {
if let Some(attr_value) = self.convert_protobuf_attribute_value(attr.value) {
telemetry_span.attributes.insert(attr.key, attr_value);
}
}
}
}
if let Some(status) = span.status {
telemetry_span.attributes.insert(
"status.code".to_string(),
AttributeValue::Int(status.code as i64),
);
if !status.message.is_empty() {
telemetry_span.attributes.insert(
"status.message".to_string(),
AttributeValue::String(status.message),
);
}
}
telemetry_span.calculate_duration_ms();
Some(telemetry_span)
}
fn convert_protobuf_attribute_value(&self, value: Option<AnyValue>) -> Option<AttributeValue> {
value.and_then(|v| match v.value {
Some(any_value::Value::StringValue(s)) => Some(AttributeValue::String(s)),
Some(any_value::Value::IntValue(i)) => Some(AttributeValue::Int(i)),
Some(any_value::Value::DoubleValue(d)) => Some(AttributeValue::Double(d)),
Some(any_value::Value::BoolValue(b)) => Some(AttributeValue::Bool(b)),
None => None,
})
}
}
impl OtlpParser for ProtobufParser {
fn parse_file(&self, path: &Path) -> Result<ParsedTelemetry> {
let data = fs::read(path)
.with_context(|| format!("Failed to read telemetry file: {}", path.display()))?;
self.parse_bytes(&data)
}
fn parse_bytes(&self, data: &[u8]) -> Result<ParsedTelemetry> {
if data.is_empty() {
return Ok(ParsedTelemetry {
spans: vec![],
code_spans: vec![],
});
}
let otlp = ExportTraceServiceRequest::decode(data)
.context("Failed to decode OTLP protobuf data")?;
let spans = self.extract_spans(otlp);
let code_spans = spans
.iter()
.filter(|s| s.function_name.is_some() || s.file_path.is_some())
.cloned()
.collect();
Ok(ParsedTelemetry { spans, code_spans })
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_protobuf_parser_basic_deserialization() {
let parser = ProtobufParser::new();
let empty_data = vec![];
let result = parser.parse_bytes(&empty_data);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.spans.len(), 0);
assert_eq!(parsed.code_spans.len(), 0);
}
#[test]
fn test_protobuf_parser_invalid_data() {
let parser = ProtobufParser::new();
let invalid_data = vec![0xFF, 0xFF, 0xFF, 0xFF];
let result = parser.parse_bytes(&invalid_data);
match result {
Ok(parsed) => {
assert_eq!(parsed.spans.len(), 0);
assert_eq!(parsed.code_spans.len(), 0);
}
Err(_) => {
}
}
}
#[test]
fn test_protobuf_parser_file_reading() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let test_data = vec![0x08, 0x96, 0x01]; temp_file
.write_all(&test_data)
.expect("Failed to write test data");
let parser = ProtobufParser::new();
let result = parser.parse_file(temp_file.path());
match result {
Ok(_) => {
}
Err(e) => {
println!("Expected error for invalid protobuf data: {e}");
}
}
}
#[test]
fn test_protobuf_parser_vs_json_parser_interface() {
let protobuf_parser = ProtobufParser::new();
let json_parser = JsonParser::new();
let empty_data = vec![];
let pb_result = protobuf_parser.parse_bytes(&empty_data);
let json_result = json_parser.parse_bytes(b"{}");
assert!(pb_result.is_ok());
assert!(json_result.is_ok());
}
use std::path::PathBuf;
#[test]
fn test_parse_otlp_json_with_code_attributes() {
let json_data = r#"{
"resourceSpans": [{
"resource": {
"attributes": [{
"key": "service.name",
"value": { "stringValue": "payment-api" }
}]
},
"scopeSpans": [{
"spans": [{
"name": "process_payment",
"startTimeUnixNano": "1704067200000000000",
"endTimeUnixNano": "1704067200050000000",
"attributes": [
{
"key": "code.function.name",
"value": { "stringValue": "process_payment" }
},
{
"key": "code.file.path",
"value": { "stringValue": "src/api/handlers.rs" }
},
{
"key": "code.line.number",
"value": { "intValue": "42" }
}
]
}]
}]
}]
}"#;
let parser = JsonParser::new();
let result = parser.parse_bytes(json_data.as_bytes()).unwrap();
assert_eq!(result.spans.len(), 1);
assert_eq!(result.code_spans.len(), 1);
let span = &result.code_spans[0];
assert_eq!(span.name, "process_payment");
assert_eq!(span.function_name, Some("process_payment".to_string()));
assert_eq!(span.file_path, Some(PathBuf::from("src/api/handlers.rs")));
assert_eq!(span.line_number, Some(42));
assert_eq!(span.service_name, Some("payment-api".to_string()));
assert_eq!(span.duration_ms, 50.0);
}
#[test]
fn test_parse_otlp_json_without_code_attributes() {
let json_data = r#"{
"resourceSpans": [{
"scopeSpans": [{
"spans": [{
"name": "database_query",
"startTimeUnixNano": "1704067200000000000",
"endTimeUnixNano": "1704067200100000000",
"attributes": [
{
"key": "db.statement",
"value": { "stringValue": "SELECT * FROM users" }
}
]
}]
}]
}]
}"#;
let parser = JsonParser::new();
let result = parser.parse_bytes(json_data.as_bytes()).unwrap();
assert_eq!(result.spans.len(), 1);
assert_eq!(result.code_spans.len(), 0);
let span = &result.spans[0];
assert_eq!(span.name, "database_query");
assert!(span.function_name.is_none());
assert!(span.file_path.is_none());
}
#[test]
fn test_parse_invalid_json() {
let invalid_json = r#"{ invalid json }"#;
let parser = JsonParser::new();
let result = parser.parse_bytes(invalid_json.as_bytes());
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to parse OTLP JSON"));
}
}