Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add explore subcommand #89

Merged
merged 6 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
script from explorer (#84)
- Shorthand notion for endpoints defined within the config file (`am.toml`) is now
allowed (#85)
- Added new subcommand `explore` which opens up explorer in the browser (#89)

## [0.1.0]

Expand Down
5 changes: 5 additions & 0 deletions src/bin/am/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use indicatif::MultiProgress;
use std::path::PathBuf;
use tracing::info;

mod explore;
pub mod start;
pub mod system;

Expand Down Expand Up @@ -39,6 +40,9 @@ pub enum SubCommands {
/// Prometheus, Pushgateway installs.
System(system::Arguments),

/// Open up the existing Explorer
Explore(explore::CliArguments),

/// Open the Fiberplane discord to receive help, send suggestions or
/// discuss various things related to Autometrics and the `am` CLI
Discord,
Expand All @@ -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, config).await,
SubCommands::Discord => {
const URL: &str = "https://discord.gg/kHtwcH8As9";

Expand Down
69 changes: 69 additions & 0 deletions src/bin/am/commands/explore.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use anyhow::Result;
use autometrics_am::config::{endpoints_from_first_input, AmConfig, Endpoint};
use autometrics_am::parser::endpoint_parser;
use clap::Parser;
use tracing::info;
use url::Url;

#[derive(Parser, Clone)]
pub struct CliArguments {
/// The endpoint(s) that will be passed to Explorer
///
/// Multiple endpoints can be specified by separating them with a space.
/// The endpoint can be provided in the following formats:
/// - `:3000`. Defaults to `http`, `localhost` and `/metrics`.
/// - `localhost:3000`. Defaults to `http`, and `/metrics`.
/// - `https://localhost:3000`. Defaults to `/metrics`.
/// - `https://localhost:3000/api/metrics`. No defaults.
#[clap(value_parser = endpoint_parser, verbatim_doc_comment)]
metrics_endpoints: Vec<Url>,
mellowagain marked this conversation as resolved.
Show resolved Hide resolved

/// Which endpoint to open in the browser
#[clap(long, env)]
explorer_endpoint: Option<Url>,
}

#[derive(Debug, Clone)]
struct Arguments {
metrics_endpoints: Vec<Endpoint>,
explorer_endpoint: Url,
}

impl Arguments {
fn new(args: CliArguments, config: AmConfig) -> Self {
Arguments {
metrics_endpoints: endpoints_from_first_input(args.metrics_endpoints, config.endpoints),
explorer_endpoint: args
.explorer_endpoint
.or(config.explorer_endpoint)
.unwrap_or_else(|| Url::parse("http://localhost:6789/explorer").unwrap()), // .unwrap is safe because we control the input
}
}
}

pub async fn handle_command(args: CliArguments, config: AmConfig) -> Result<()> {
let mut args = Arguments::new(args, config);

let query: String = args
.metrics_endpoints
.into_iter()
.map(|e| format!("_prometheusUrl={}", e.url))
.collect::<Vec<_>>()
.join("&");

let url = &mut args.explorer_endpoint;
url.set_query(if !query.is_empty() {
Some(query.as_str())
} else {
None
});

if open::that(url.as_str()).is_err() {
info!(
"Unable to open browser, open the following URL in your browser: {}",
url.as_str()
);
}

Ok(())
}
57 changes: 21 additions & 36 deletions src/bin/am/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -93,39 +92,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)
})
.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),
)
})
.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
Expand All @@ -139,7 +110,7 @@ impl Arguments {
}

#[derive(Debug, Clone)]
struct Endpoint {
pub struct Endpoint {
url: Url,
job_name: String,
honor_labels: bool,
Expand All @@ -155,6 +126,20 @@ impl Endpoint {
}
}

impl TryFrom<autometrics_am::config::Endpoint> for Endpoint {
type Error = anyhow::Error;

fn try_from(value: autometrics_am::config::Endpoint) -> std::result::Result<Self, Self::Error> {
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),
})
}
}

impl From<Endpoint> for ScrapeConfig {
/// Convert an InnerEndpoint to a Prometheus ScrapeConfig.
///
Expand Down
41 changes: 41 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::parser::endpoint_parser;
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use std::sync::atomic::{AtomicUsize, Ordering};
use url::Url;

/// This struct represents the am.toml configuration. Most properties in here
Expand All @@ -13,6 +14,9 @@ pub struct AmConfig {
#[serde(rename = "endpoint")]
pub endpoints: Option<Vec<Endpoint>>,

/// Endpoint where explorer lives at
pub explorer_endpoint: Option<Url>,

/// Startup the pushgateway.
pub pushgateway_enabled: Option<bool>,
}
Expand All @@ -30,3 +34,40 @@ fn parse_maybe_shorthand<'de, D: Deserializer<'de>>(input: D) -> Result<Url, D::
let input_str: String = Deserialize::deserialize(input)?;
endpoint_parser(&input_str).map_err(Error::custom)
}

/// 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.
pub fn endpoints_from_first_input(args: Vec<Url>, config: Option<Vec<Endpoint>>) -> Vec<Endpoint> {
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),
}
})
.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,
}
})
.collect()
} else {
Vec::new()
}
}