Convert opencode transcripts to otel (or agent) traces
1use anyhow::Result;
2use clap::{Parser, Subcommand, ValueEnum};
3use std::path::PathBuf;
4
5#[derive(Parser)]
6#[command(
7 name = "exp2span",
8 version = env!("CARGO_PKG_VERSION"),
9 author = "rektide de la faye",
10 about = "Convert opencode logs to OpenTelemetry traces",
11 long_about = "Parse opencode esport markdown logs and export them as OpenTelemetry spans using MCP semantic conventions."
12)]
13pub struct Cli {
14 /// Global configuration file
15 #[arg(short, long, global = true, env = "EXP2SPAN_CONFIG")]
16 pub config: Option<PathBuf>,
17
18 /// Output format
19 #[arg(
20 short,
21 long,
22 global = true,
23 value_enum,
24 default_value_t = OutputFormat::Otlp,
25 env = "EXP2SPAN_OUTPUT_FORMAT"
26 )]
27 pub output: OutputFormat,
28
29 /// Increase logging verbosity
30 #[arg(short, long, action = clap::ArgAction::Count, global = true)]
31 pub verbose: u8,
32
33 /// Suppress all output
34 #[arg(short, long, global = true, conflicts_with = "verbose")]
35 pub quiet: bool,
36
37 #[command(subcommand)]
38 pub command: Commands,
39}
40
41#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
42pub enum OutputFormat {
43 /// Export to OTLP (default behavior)
44 Otlp,
45 /// Pretty-printed JSON output
46 Json,
47 /// JSON Lines (NDJSON) for streaming
48 JsonLines,
49 /// YAML output
50 Yaml,
51 /// Human-readable table format
52 Table,
53}
54
55#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
56pub enum OtlpProtocol {
57 /// Use HTTP/JSON for OTLP export
58 Http,
59 /// Use gRPC for OTLP export
60 Grpc,
61}
62
63impl std::fmt::Display for OtlpProtocol {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 match self {
66 OtlpProtocol::Http => write!(f, "http"),
67 OtlpProtocol::Grpc => write!(f, "grpc"),
68 }
69 }
70}
71
72#[derive(Subcommand)]
73pub enum Commands {
74 /// Export log to OpenTelemetry spans
75 Export(ExportArgs),
76
77 /// Validate log file format
78 Validate(ValidateArgs),
79
80 /// Show log metadata and statistics
81 Info(InfoArgs),
82
83 /// Generate shell completions
84 Completion(CompletionArgs),
85}
86
87#[derive(Parser, Debug)]
88pub struct ExportArgs {
89 /// Path to the log file
90 #[arg(value_name = "FILE")]
91 pub file: PathBuf,
92
93 /// Dry run - don't export, just show what would be done
94 #[arg(long)]
95 pub dry_run: bool,
96
97 /// Filter by role (user, assistant)
98 #[arg(short, long)]
99 pub filter_role: Option<String>,
100
101 /// OTLP endpoint (e.g., http://localhost:4318 for HTTP, http://localhost:4317 for gRPC)
102 #[arg(long, default_value = "http://localhost:4318")]
103 pub otlp_endpoint: String,
104
105 /// OTLP protocol to use
106 #[arg(long, default_value_t = OtlpProtocol::Http)]
107 pub otlp_protocol: OtlpProtocol,
108
109 /// Custom headers for OTLP export (e.g., "Authorization=Bearer $TOKEN")
110 #[arg(long, value_parser = parse_key_value)]
111 pub otlp_header: Vec<(String, String)>,
112
113 /// Service name for the traces
114 #[arg(long, default_value = "exp2span")]
115 pub otlp_service_name: String,
116
117 /// Batch size for span export
118 #[arg(long, default_value_t = 100)]
119 pub batch_size: usize,
120
121 /// Timeout for OTLP export in seconds
122 #[arg(long, default_value_t = 30)]
123 pub otlp_timeout_secs: u64,
124
125 /// Maximum number of retry attempts for failed exports
126 #[arg(long, default_value_t = 3)]
127 pub otlp_max_retries: u32,
128
129 /// Initial delay between retries in milliseconds
130 #[arg(long, default_value_t = 100)]
131 pub otlp_retry_delay_ms: u64,
132}
133
134fn parse_key_value(s: &str) -> Result<(String, String)> {
135 let parts: Vec<&str> = s.splitn(2, '=').collect();
136 if parts.len() != 2 {
137 return Err(anyhow::anyhow!(
138 "Invalid key-value pair: '{}'. Expected format: KEY=VALUE",
139 s
140 ));
141 }
142 Ok((parts[0].to_string(), parts[1].to_string()))
143}
144
145#[derive(Parser, Debug)]
146pub struct ValidateArgs {
147 /// Path to the log file
148 #[arg(value_name = "FILE")]
149 pub file: PathBuf,
150}
151
152#[derive(Parser, Debug)]
153pub struct InfoArgs {
154 /// Path to log file
155 #[arg(value_name = "FILE")]
156 pub file: PathBuf,
157}
158
159#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
160pub enum CompletionShell {
161 /// Bash shell
162 Bash,
163 /// Zsh shell
164 Zsh,
165 /// Fish shell
166 Fish,
167 /// PowerShell
168 PowerShell,
169 /// Elvish shell
170 Elvish,
171}
172
173#[derive(Parser, Debug)]
174pub struct CompletionArgs {
175 /// Shell to generate completions for
176 #[arg(value_enum)]
177 pub shell: CompletionShell,
178
179 /// Print completions to stdout instead of installing
180 #[arg(short, long)]
181 pub print: bool,
182}