diff --git a/CHANGELOG.md b/CHANGELOG.md index 90865ea..c9a8e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Shorthand notion for endpoints defined within the config file (`am.toml`) is now allowed (#85) - Allow user to specify the Prometheus scrape interval (#87) +- Added new subcommand `explore` which opens up explorer in the browser (#89) ## [0.1.0] diff --git a/src/bin/am/commands.rs b/src/bin/am/commands.rs index 55e71c2..e1f7002 100644 --- a/src/bin/am/commands.rs +++ b/src/bin/am/commands.rs @@ -5,6 +5,7 @@ use indicatif::MultiProgress; use std::path::PathBuf; use tracing::info; +mod explore; pub mod start; pub mod system; @@ -39,6 +40,9 @@ pub enum SubCommands { /// Prometheus, Pushgateway installs. System(system::Arguments), + /// Open up the existing Explorer + Explore(explore::Arguments), + /// Open the Fiberplane discord to receive help, send suggestions or /// discuss various things related to Autometrics and the `am` CLI Discord, @@ -51,6 +55,7 @@ pub async fn handle_command(app: Application, config: AmConfig, mp: MultiProgres match app.command { SubCommands::Start(args) => start::handle_command(args, config, mp).await, SubCommands::System(args) => system::handle_command(args, mp).await, + SubCommands::Explore(args) => explore::handle_command(args).await, SubCommands::Discord => { const URL: &str = "https://discord.gg/kHtwcH8As9"; diff --git a/src/bin/am/commands/explore.rs b/src/bin/am/commands/explore.rs new file mode 100644 index 0000000..9f6bc62 --- /dev/null +++ b/src/bin/am/commands/explore.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use clap::Parser; +use tracing::info; +use url::Url; + +#[derive(Parser, Clone)] +pub struct Arguments { + /// The Prometheus endpoint that will be passed to Explorer + #[clap(long, env)] + prometheus_endpoint: Option, + + /// Which endpoint to open in the browser + #[clap(long, env, default_value = "http://localhost:6789/explorer")] + explorer_endpoint: Url, +} + +pub async fn handle_command(mut args: Arguments) -> Result<()> { + let url = &mut args.explorer_endpoint; + + if let Some(prom_url) = args.prometheus_endpoint { + let query = format!("prometheusUrl={}", prom_url.as_str()); + url.set_query(Some(&query)); + } + + if open::that(url.as_str()).is_err() { + info!( + "Unable to open browser, open the following URL in your browser: {}", + url.as_str() + ); + } + + Ok(()) +} diff --git a/src/bin/am/commands/start.rs b/src/bin/am/commands/start.rs index ed168a0..4ad34a8 100644 --- a/src/bin/am/commands/start.rs +++ b/src/bin/am/commands/start.rs @@ -2,8 +2,8 @@ use crate::dir::AutoCleanupDir; use crate::downloader::{download_github_release, unpack, verify_checksum}; use crate::interactive; use crate::server::start_web_server; -use anyhow::{bail, Context, Result}; -use autometrics_am::config::AmConfig; +use anyhow::{anyhow, bail, Context, Result}; +use autometrics_am::config::{endpoints_from_first_input, AmConfig}; use autometrics_am::parser::endpoint_parser; use autometrics_am::prometheus; use autometrics_am::prometheus::ScrapeConfig; @@ -16,7 +16,6 @@ use std::fs::File; use std::io::{Seek, SeekFrom}; use std::net::SocketAddr; use std::path::Path; -use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; use std::{env, vec}; use tempfile::NamedTempFile; @@ -111,40 +110,11 @@ struct Arguments { impl Arguments { fn new(args: CliArguments, config: AmConfig) -> Self { - static COUNTER: AtomicUsize = AtomicUsize::new(0); - - // If the user specified an endpoint using args, then use those. - // Otherwise use the endpoint configured in the config file. And - // fallback to an empty list if neither are configured. - let metrics_endpoints = if !args.metrics_endpoints.is_empty() { - args.metrics_endpoints - .into_iter() - .map(|url| { - let num = COUNTER.fetch_add(1, Ordering::SeqCst); - Endpoint::new(url, format!("am_{num}"), false, None) - }) - .collect() - } else if let Some(endpoints) = config.endpoints { - endpoints - .into_iter() - .map(|endpoint| { - let job_name = endpoint.job_name.unwrap_or_else(|| { - format!("am_{num}", num = COUNTER.fetch_add(1, Ordering::SeqCst)) - }); - Endpoint::new( - endpoint.url, - job_name, - endpoint.honor_labels.unwrap_or(false), - endpoint.prometheus_scrape_interval, - ) - }) - .collect() - } else { - Vec::new() - }; - Arguments { - metrics_endpoints, + metrics_endpoints: endpoints_from_first_input(args.metrics_endpoints, config.endpoints) + .into_iter() + .filter_map(|e| e.try_into().ok()) + .collect(), prometheus_version: args.prometheus_version, listen_address: args.listen_address, pushgateway_enabled: args @@ -162,7 +132,7 @@ impl Arguments { } #[derive(Debug, Clone)] -struct Endpoint { +pub struct Endpoint { url: Url, job_name: String, honor_labels: bool, @@ -185,6 +155,21 @@ impl Endpoint { } } +impl TryFrom for Endpoint { + type Error = anyhow::Error; + + fn try_from(value: autometrics_am::config::Endpoint) -> Result { + Ok(Self { + url: value.url, + job_name: value + .job_name + .ok_or_else(|| anyhow!("TryFrom requires job_name"))?, + honor_labels: value.honor_labels.unwrap_or(false), + scrape_interval: value.prometheus_scrape_interval, + }) + } +} + impl From for ScrapeConfig { /// Convert an InnerEndpoint to a Prometheus ScrapeConfig. /// diff --git a/src/config.rs b/src/config.rs index b9d9adb..54699cd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::parser::endpoint_parser; use serde::de::Error; use serde::{Deserialize, Deserializer}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; use url::Url; @@ -45,3 +46,42 @@ fn parse_maybe_shorthand<'de, D: Deserializer<'de>>(input: D) -> Result, config: Option>) -> Vec { + static COUNTER: AtomicUsize = AtomicUsize::new(0); + + if !args.is_empty() { + args.into_iter() + .map(|url| { + let num = COUNTER.fetch_add(1, Ordering::SeqCst); + Endpoint { + url, + job_name: Some(format!("am_{num}")), + honor_labels: Some(false), + prometheus_scrape_interval: None, + } + }) + .collect() + } else if let Some(endpoints) = config { + endpoints + .into_iter() + .map(|endpoint| { + let job_name = endpoint.job_name.unwrap_or_else(|| { + format!("am_{num}", num = COUNTER.fetch_add(1, Ordering::SeqCst)) + }); + + Endpoint { + url: endpoint.url, + job_name: Some(job_name), + honor_labels: endpoint.honor_labels, + prometheus_scrape_interval: endpoint.prometheus_scrape_interval, + } + }) + .collect() + } else { + Vec::new() + } +}