use anyhow::Result; use clap::{Parser, Subcommand, ValueEnum}; use std::path::PathBuf; #[derive(Parser)] #[command( name = "exp2span", version = env!("CARGO_PKG_VERSION"), author = "rektide de la faye", about = "Convert opencode logs to OpenTelemetry traces", long_about = "Parse opencode esport markdown logs and export them as OpenTelemetry spans using MCP semantic conventions." )] pub struct Cli { /// Global configuration file #[arg(short, long, global = true, env = "EXP2SPAN_CONFIG")] pub config: Option, /// Output format #[arg( short, long, global = true, value_enum, default_value_t = OutputFormat::Otlp, env = "EXP2SPAN_OUTPUT_FORMAT" )] pub output: OutputFormat, /// Increase logging verbosity #[arg(short, long, action = clap::ArgAction::Count, global = true)] pub verbose: u8, /// Suppress all output #[arg(short, long, global = true, conflicts_with = "verbose")] pub quiet: bool, #[command(subcommand)] pub command: Commands, } #[derive(ValueEnum, Clone, Copy, Debug, PartialEq)] pub enum OutputFormat { /// Export to OTLP (default behavior) Otlp, /// Pretty-printed JSON output Json, /// JSON Lines (NDJSON) for streaming JsonLines, /// YAML output Yaml, /// Human-readable table format Table, } #[derive(ValueEnum, Clone, Copy, Debug, PartialEq)] pub enum OtlpProtocol { /// Use HTTP/JSON for OTLP export Http, /// Use gRPC for OTLP export Grpc, } impl std::fmt::Display for OtlpProtocol { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { OtlpProtocol::Http => write!(f, "http"), OtlpProtocol::Grpc => write!(f, "grpc"), } } } #[derive(Subcommand)] pub enum Commands { /// Export log to OpenTelemetry spans Export(ExportArgs), /// Validate log file format Validate(ValidateArgs), /// Show log metadata and statistics Info(InfoArgs), /// Generate shell completions Completion(CompletionArgs), } #[derive(Parser, Debug)] pub struct ExportArgs { /// Path to the log file #[arg(value_name = "FILE")] pub file: PathBuf, /// Dry run - don't export, just show what would be done #[arg(long)] pub dry_run: bool, /// Filter by role (user, assistant) #[arg(short, long)] pub filter_role: Option, /// OTLP endpoint (e.g., http://localhost:4318 for HTTP, http://localhost:4317 for gRPC) #[arg(long, default_value = "http://localhost:4318")] pub otlp_endpoint: String, /// OTLP protocol to use #[arg(long, default_value_t = OtlpProtocol::Http)] pub otlp_protocol: OtlpProtocol, /// Custom headers for OTLP export (e.g., "Authorization=Bearer $TOKEN") #[arg(long, value_parser = parse_key_value)] pub otlp_header: Vec<(String, String)>, /// Service name for the traces #[arg(long, default_value = "exp2span")] pub otlp_service_name: String, /// Batch size for span export #[arg(long, default_value_t = 100)] pub batch_size: usize, /// Timeout for OTLP export in seconds #[arg(long, default_value_t = 30)] pub otlp_timeout_secs: u64, /// Maximum number of retry attempts for failed exports #[arg(long, default_value_t = 3)] pub otlp_max_retries: u32, /// Initial delay between retries in milliseconds #[arg(long, default_value_t = 100)] pub otlp_retry_delay_ms: u64, } fn parse_key_value(s: &str) -> Result<(String, String)> { let parts: Vec<&str> = s.splitn(2, '=').collect(); if parts.len() != 2 { return Err(anyhow::anyhow!( "Invalid key-value pair: '{}'. Expected format: KEY=VALUE", s )); } Ok((parts[0].to_string(), parts[1].to_string())) } #[derive(Parser, Debug)] pub struct ValidateArgs { /// Path to the log file #[arg(value_name = "FILE")] pub file: PathBuf, } #[derive(Parser, Debug)] pub struct InfoArgs { /// Path to log file #[arg(value_name = "FILE")] pub file: PathBuf, } #[derive(ValueEnum, Clone, Copy, Debug, PartialEq)] pub enum CompletionShell { /// Bash shell Bash, /// Zsh shell Zsh, /// Fish shell Fish, /// PowerShell PowerShell, /// Elvish shell Elvish, } #[derive(Parser, Debug)] pub struct CompletionArgs { /// Shell to generate completions for #[arg(value_enum)] pub shell: CompletionShell, /// Print completions to stdout instead of installing #[arg(short, long)] pub print: bool, }