Convert opencode transcripts to otel (or agent) traces
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Integrate 'updated at' timestamp into parser reverse-threading logic

- Parse log.updated timestamp and pass to parse_entries_with_time
- Update calculate_timestamps to use updated_at instead of Timestamp::now()
- Simplified exporter by removing updated_at parameter (parser now handles timing)
- Fixed FromStr import for Timestamp parsing
- Now reverse-threading works backwards from 'updated at' time with proper padding

rektide 6da8cbfb ddffc614

+24 -22
+1 -6
src/commands/export.rs
··· 1 1 use anyhow::{Context, Result}; 2 - use jiff::Timestamp; 3 - use std::str::FromStr; 4 2 5 3 use crate::cli::{ExportArgs, OtlpProtocol, OutputFormat}; 6 4 use crate::config::{OtlpConfig, OtlpProtocol as ConfigOtlpProtocol}; 7 5 use crate::parser::LogParser; 8 - use crate::parser::GenAiSpan; 9 6 use crate::exporter::OtelExporter; 10 7 use crate::formatter; 11 8 pub async fn execute(args: ExportArgs, output_format: OutputFormat) -> Result<()> { ··· 51 48 52 49 let filtered_entries = entries; 53 50 54 - let updated_at = Timestamp::from_str(&log.updated).ok(); 55 - 56 51 if use_stdout { 57 52 tracing::info!("Outputting {} spans to stdout in {:?} format", filtered_entries.len(), output_format); 58 53 let output = formatter::format_spans(&filtered_entries, output_format)?; ··· 69 64 tracing::info!("Exporting {} spans...", filtered_entries.len()); 70 65 71 66 for entry in &filtered_entries { 72 - exporter.export_log(entry, updated_at.as_ref()); 67 + exporter.export_log(entry); 73 68 } 74 69 75 70 tracing::info!("Flushing pending spans...");
+10 -13
src/exporter.rs
··· 116 116 Ok((tracer, provider)) 117 117 } 118 118 119 - pub fn export_log(&self, span: &GenAiSpan, updated_at: Option<&Timestamp>) { 119 + pub fn export_log(&self, span: &GenAiSpan) { 120 120 match span.span_type { 121 - SpanType::ToolCall => self.export_tool_call(span, updated_at), 122 - SpanType::Chat => self.export_chat(span, updated_at), 123 - SpanType::Thinking => self.export_thinking(span, updated_at), 121 + SpanType::ToolCall => self.export_tool_call(span), 122 + SpanType::Chat => self.export_chat(span), 123 + SpanType::Thinking => self.export_thinking(span), 124 124 } 125 125 } 126 126 127 - fn export_tool_call(&self, span: &GenAiSpan, updated_at: Option<&Timestamp>) { 127 + fn export_tool_call(&self, span: &GenAiSpan) { 128 128 let mut attributes = vec![ 129 129 KeyValue::new("gen_ai.conversation.id", span.session_id.clone()), 130 130 KeyValue::new("network.transport", "tcp"), ··· 167 167 builder.span_kind = Some(SpanKind::Client); 168 168 builder.attributes = Some(attributes); 169 169 170 - let timestamp = updated_at.unwrap_or(&span.timestamp); 171 - let start_time = convert_timestamp_to_system_time(timestamp); 170 + let start_time = convert_timestamp_to_system_time(&span.timestamp); 172 171 builder.start_time = Some(start_time); 173 172 174 173 if let Some(duration) = convert_duration_ms(span.duration_ms) { ··· 185 184 ); 186 185 } 187 186 188 - fn export_chat(&self, span: &GenAiSpan, updated_at: Option<&Timestamp>) { 187 + fn export_chat(&self, span: &GenAiSpan) { 189 188 let mut attributes = vec![ 190 189 KeyValue::new("gen_ai.conversation.id", span.session_id.clone()), 191 190 KeyValue::new("network.transport", "tcp"), ··· 230 229 builder.span_kind = Some(SpanKind::Client); 231 230 builder.attributes = Some(attributes); 232 231 233 - let timestamp = updated_at.unwrap_or(&span.timestamp); 234 - let start_time = convert_timestamp_to_system_time(timestamp); 232 + let start_time = convert_timestamp_to_system_time(&span.timestamp); 235 233 builder.start_time = Some(start_time); 236 234 237 235 if let Some(duration) = convert_duration_ms(span.duration_ms) { ··· 248 246 ); 249 247 } 250 248 251 - fn export_thinking(&self, span: &GenAiSpan, updated_at: Option<&Timestamp>) { 249 + fn export_thinking(&self, span: &GenAiSpan) { 252 250 let mut attributes = vec![ 253 251 KeyValue::new("gen_ai.conversation.id", span.session_id.clone()), 254 252 KeyValue::new("network.transport", "tcp"), ··· 282 280 builder.span_kind = Some(SpanKind::Internal); 283 281 builder.attributes = Some(attributes); 284 282 285 - let timestamp = updated_at.unwrap_or(&span.timestamp); 286 - let start_time = convert_timestamp_to_system_time(timestamp); 283 + let start_time = convert_timestamp_to_system_time(&span.timestamp); 287 284 builder.start_time = Some(start_time); 288 285 289 286 if let Some(duration) = convert_duration_ms(span.duration_ms) {
+13 -3
src/parser.rs
··· 3 3 use regex::Regex; 4 4 use serde::Deserialize; 5 5 use std::fs; 6 + use std::str::FromStr; 6 7 7 8 #[derive(Debug, Clone, Deserialize)] 8 9 pub struct OpenCodeLog { ··· 230 231 } 231 232 232 233 pub fn parse_entries(&self, log: &OpenCodeLog) -> Vec<GenAiSpan> { 234 + let updated_at = Timestamp::from_str(&log.updated).ok(); 235 + self.parse_entries_with_time(log, updated_at) 236 + } 237 + 238 + fn parse_entries_with_time( 239 + &self, 240 + log: &OpenCodeLog, 241 + updated_at: Option<Timestamp>, 242 + ) -> Vec<GenAiSpan> { 233 243 let mut spans = Vec::new(); 234 244 let mut current_user_msg: Option<&LogMessage> = None; 235 245 ··· 263 273 } 264 274 } 265 275 266 - self.calculate_timestamps(&mut spans); 276 + self.calculate_timestamps(&mut spans, updated_at); 267 277 spans 268 278 } 269 279 ··· 459 469 } 460 470 } 461 471 462 - pub fn calculate_timestamps(&self, spans: &mut Vec<GenAiSpan>) { 463 - let now = Timestamp::now(); 472 + pub fn calculate_timestamps(&self, spans: &mut Vec<GenAiSpan>, updated_at: Option<Timestamp>) { 473 + let now = updated_at.unwrap_or_else(|| Timestamp::now()); 464 474 let mut current_end_ms = now.as_millisecond(); 465 475 466 476 let mut chat_indices: Vec<usize> = spans