use crate::console_display::Theme;
use clap::{
builder::TypedValueParser,
crate_authors,
crate_description,
error::ErrorKind,
ArgGroup,
Parser,
Subcommand,
ValueEnum, };
use clap_complete::Shell; use globset::{Glob, GlobSet, GlobSetBuilder};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Serialize, Deserialize, Default)]
pub enum ColoringMode {
#[default]
Service,
Span,
}
const USAGE_EXAMPLES: &str = "\
EXAMPLES:
# Tail logs from a CloudFormation stack (Live Tail mode)
livetrace --stack-name my-api-stack
# Tail logs matching a pattern and forward to a local OTLP collector
livetrace --log-group-pattern \"/aws/lambda/my-service-\" -e http://localhost:4318
# Poll logs every 30 seconds from a stack, showing only events, with a specific theme
livetrace --stack-name my-data-processing --poll-interval 30 --events-only --theme solarized
# Discover log groups by pattern and save the configuration to a profile named \"dev\"
livetrace --log-group-pattern \"/aws/lambda/user-service-\" --save-profile dev
# Load configuration from the \"dev\" profile and override the OTLP endpoint
livetrace --config-profile dev -e http://localhost:4319";
#[derive(Parser, Debug, Clone)] #[command(author = crate_authors!(", "), version, about = crate_description!(), long_about = None, after_help = USAGE_EXAMPLES)]
#[clap(group( // Add group to make poll/timeout mutually exclusive
ArgGroup::new("mode")
.required(false) // One or neither can be specified
.args(["poll_interval", "session_timeout"]),
))]
pub struct CliArgs {
#[arg(short = 'g', long = "log-group-pattern", num_args(1..))]
pub log_group_pattern: Option<Vec<String>>,
#[arg(short = 's', long = "stack-name")]
pub stack_name: Option<String>,
#[arg(short = 'e', long)]
pub otlp_endpoint: Option<String>,
#[arg(short = 'H', long = "otlp-header")]
pub otlp_headers: Vec<String>,
#[arg(short = 'r', long = "aws-region")]
pub aws_region: Option<String>,
#[arg(short = 'p', long = "aws-profile")]
pub aws_profile: Option<String>,
#[arg(short, long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long)]
pub forward_only: bool,
#[arg(long = "attrs")]
pub attrs: Option<String>,
#[arg(long, group = "mode")] pub poll_interval: Option<u64>,
#[arg(long, default_value_t = 30, group = "mode")] pub session_timeout: u64,
#[arg(long, default_value = "event.severity")]
pub event_severity_attribute: String,
#[arg(long)]
pub config_profile: Option<String>,
#[arg(long, value_name = "PROFILE_NAME")]
pub save_profile: Option<String>,
#[arg(
long,
default_value = "default",
value_parser = ThemeValueParser,
value_name = "THEME",
help_heading = "Display Options",
)]
pub theme: String,
#[arg(
long = "list-themes",
help_heading = "Display Options",
conflicts_with = "theme"
)]
pub list_themes: bool,
#[arg(
long = "color-by",
value_enum,
default_value_t = ColoringMode::Service,
help_heading = "Display Options",
)]
pub color_by: ColoringMode,
#[arg(long, help_heading = "Display Options")]
pub events_only: bool,
#[arg(long, default_value_t = 5, help_heading = "Processing Options")]
pub trace_timeout: u64,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
#[command(name = "generate-completions", hide = true)]
GenerateCompletions {
#[arg(value_enum)]
shell: Shell,
},
}
#[derive(Clone)]
struct ThemeValueParser;
impl TypedValueParser for ThemeValueParser {
type Value = String;
fn parse_ref(
&self,
_cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let valid_themes = [
"default - OpenTelemetry-inspired blue-purple palette",
"tableau - Tableau 12 color palette with distinct hues",
"colorbrewer - ColorBrewer Set3 palette (pastel colors)",
"material - Material Design palette with bright, modern colors",
"solarized - Solarized color scheme with muted tones",
"monochrome - Grayscale palette for minimal distraction",
];
let theme_str = value.to_string_lossy().to_string();
if theme_str.is_empty() {
print_available_themes();
return Ok("default".to_string());
}
if !Theme::is_valid_theme(&theme_str) {
let themes_list = valid_themes.join("\n * ");
let err = format!(
"Invalid theme '{}'. Available themes:\n * {}",
theme_str, themes_list
);
return Err(clap::Error::raw(ErrorKind::InvalidValue, err));
}
Ok(theme_str)
}
}
fn print_available_themes() {
println!("\nAvailable themes:");
println!(" * default - OpenTelemetry-inspired blue-purple palette");
println!(" * tableau - Tableau 12 color palette with distinct hues");
println!(" * colorbrewer - ColorBrewer Set3 palette (pastel colors)");
println!(" * material - Material Design palette with bright, modern colors");
println!(" * solarized - Solarized color scheme with muted tones");
println!(" * monochrome - Grayscale palette for minimal distraction");
println!("\nUsage: livetrace --theme <THEME>");
std::process::exit(0);
}
pub fn parse_attr_globs(patterns_opt: &Option<String>) -> Option<GlobSet> {
match patterns_opt.as_deref() {
Some(patterns_str) if !patterns_str.is_empty() => {
let mut builder = GlobSetBuilder::new();
for pattern in patterns_str.split(',') {
let trimmed_pattern = pattern.trim();
if !trimmed_pattern.is_empty() {
match Glob::new(trimmed_pattern) {
Ok(glob) => {
builder.add(glob);
}
Err(e) => {
tracing::warn!(pattern = trimmed_pattern, error = %e, "Invalid glob pattern for attribute filtering, skipping.");
}
}
}
}
match builder.build() {
Ok(glob_set) => Some(glob_set),
Err(e) => {
tracing::error!(error = %e, "Failed to build glob set for attributes");
None }
}
}
_ => None, }
}