use crate::{IngestError, Ingested};
use amberfork_model::{Run, SchemaVersion, Step, Warning};
use serde::Deserialize;
use serde_json::{Map, Value};
pub(crate) type SpanToStep = fn(
usize,
Option<usize>,
&str,
Map<String, Value>,
Vec<(&'static str, Value)>,
Option<String>,
Option<String>,
&mut Vec<Warning>,
) -> Step;
pub(crate) fn from_otlp_json_str(
s: &str,
to_step: SpanToStep,
) -> Result<Vec<Ingested>, IngestError> {
let export: RawExport =
serde_json::from_str(s).map_err(|source| IngestError::Parse { path: None, source })?;
Ok(normalize(export, to_step))
}
struct DecodedSpan {
span_id: String,
parent_span_id: String,
name: String,
start: Option<u64>,
start_raw: Option<String>,
end_raw: Option<String>,
attrs: Map<String, Value>,
wire_pos: usize,
}
fn normalize(export: RawExport, to_step: SpanToStep) -> Vec<Ingested> {
let mut traces: Vec<(String, Vec<DecodedSpan>)> = Vec::new();
let mut wire_pos = 0usize;
for resource in export.resource_spans {
for scope in resource.scope_spans {
for span in scope.spans {
let decoded = span.decode(wire_pos);
wire_pos += 1;
let trace_id = span_trace_id(&span);
match traces.iter_mut().find(|(id, _)| id == &trace_id) {
Some((_, spans)) => spans.push(decoded),
None => traces.push((trace_id, vec![decoded])),
}
}
}
}
let mut runs: Vec<Ingested> = traces
.into_iter()
.map(|(trace_id, spans)| build_run(trace_id, spans, to_step))
.collect();
runs.sort_by(|a, b| {
earliest_start(&a.run)
.cmp(&earliest_start(&b.run))
.then_with(|| a.run.id.cmp(&b.run.id))
});
runs
}
fn earliest_start(run: &Run) -> Option<u64> {
run.steps
.iter()
.filter_map(|s| s.attrs.get("otel.start_time_unix_nano"))
.filter_map(|v| v.as_str())
.filter_map(|s| s.parse::<u64>().ok())
.min()
}
fn build_run(trace_id: String, mut spans: Vec<DecodedSpan>, to_step: SpanToStep) -> Ingested {
spans.sort_by(|a, b| {
a.start
.cmp(&b.start)
.then_with(|| a.wire_pos.cmp(&b.wire_pos))
});
let idx_of: Map<String, Value> = spans
.iter()
.enumerate()
.filter(|(_, s)| !s.span_id.is_empty())
.map(|(idx, s)| (s.span_id.clone(), Value::from(idx)))
.collect();
let mut warnings = Vec::new();
let steps = spans
.into_iter()
.enumerate()
.map(|(idx, span)| {
let parent_idx = idx_of
.get(&span.parent_span_id)
.and_then(serde_json::Value::as_u64)
.and_then(|u| usize::try_from(u).ok());
span.into_step(idx, parent_idx, to_step, &mut warnings)
})
.collect();
let run = Run {
schema_version: SchemaVersion::current(),
id: trace_id,
task: None,
outcome: None,
steps,
edges: None,
};
Ingested { run, warnings }
}
impl DecodedSpan {
fn into_step(
self,
idx: usize,
parent_idx: Option<usize>,
to_step: SpanToStep,
warnings: &mut Vec<Warning>,
) -> Step {
let mut provenance = Vec::new();
if let Some(start) = self.start_raw {
provenance.push(("otel.start_time_unix_nano", Value::from(start)));
}
if let Some(end) = self.end_raw {
provenance.push(("otel.end_time_unix_nano", Value::from(end)));
}
to_step(
idx, parent_idx, &self.name, self.attrs, provenance, None, None, warnings,
)
}
}
fn span_trace_id(span: &RawSpan) -> String {
if span.trace_id.is_empty() {
"trace-unknown".to_string()
} else {
span.trace_id.clone()
}
}
#[derive(Deserialize)]
struct RawExport {
#[serde(default, rename = "resourceSpans")]
resource_spans: Vec<RawResourceSpans>,
}
#[derive(Deserialize)]
struct RawResourceSpans {
#[serde(default, rename = "scopeSpans")]
scope_spans: Vec<RawScopeSpans>,
}
#[derive(Deserialize)]
struct RawScopeSpans {
#[serde(default)]
spans: Vec<RawSpan>,
}
#[derive(Deserialize)]
struct RawSpan {
#[serde(default, rename = "traceId")]
trace_id: String,
#[serde(default, rename = "spanId")]
span_id: String,
#[serde(default, rename = "parentSpanId")]
parent_span_id: String,
#[serde(default)]
name: String,
#[serde(default, rename = "startTimeUnixNano")]
start_time_unix_nano: Option<String>,
#[serde(default, rename = "endTimeUnixNano")]
end_time_unix_nano: Option<String>,
#[serde(default)]
attributes: Vec<RawAttr>,
}
impl RawSpan {
fn decode(&self, wire_pos: usize) -> DecodedSpan {
let mut attrs = Map::new();
for attr in &self.attributes {
attrs.insert(attr.key.clone(), decode_any_value(&attr.value));
}
DecodedSpan {
span_id: self.span_id.clone(),
parent_span_id: self.parent_span_id.clone(),
name: self.name.clone(),
start: self
.start_time_unix_nano
.as_deref()
.and_then(|s| s.parse::<u64>().ok()),
start_raw: self.start_time_unix_nano.clone(),
end_raw: self.end_time_unix_nano.clone(),
attrs,
wire_pos,
}
}
}
#[derive(Deserialize)]
struct RawAttr {
#[serde(default)]
key: String,
#[serde(default)]
value: Value,
}
fn decode_any_value(value: &Value) -> Value {
let Value::Object(obj) = value else {
return value.clone();
};
if let Some(s) = obj.get("stringValue") {
return s.clone();
}
if let Some(b) = obj.get("boolValue") {
return b.clone();
}
if let Some(d) = obj.get("doubleValue") {
return d.clone();
}
if let Some(int_value) = obj.get("intValue") {
return match int_value {
Value::String(s) => s
.parse::<i64>()
.map_or_else(|_| int_value.clone(), Value::from),
other => other.clone(),
};
}
if let Some(array) = obj.get("arrayValue") {
let values = array
.get("values")
.and_then(|v| v.as_array())
.map(|items| items.iter().map(decode_any_value).collect())
.unwrap_or_default();
return Value::Array(values);
}
if let Some(kvlist) = obj.get("kvlistValue") {
let mut map = Map::new();
if let Some(items) = kvlist.get("values").and_then(|v| v.as_array()) {
for item in items {
if let (Some(key), Some(val)) =
(item.get("key").and_then(|k| k.as_str()), item.get("value"))
{
map.insert(key.to_string(), decode_any_value(val));
}
}
}
return Value::Object(map);
}
value.clone()
}