Skip to content

Commit

Permalink
feat(commands): add send-metric command
Browse files Browse the repository at this point in the history
Add CLI command that can emit metrics.

Fixes GH-2001
  • Loading branch information
Elias Ram committed May 15, 2024
1 parent 554f4fc commit b19bcb9
Show file tree
Hide file tree
Showing 69 changed files with 1,599 additions and 30 deletions.
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Adding new commands with Derive API
It is recommended to use clap's [Derive API](https://docs.rs/clap/latest/clap/_derive/index.html)
as opposed to the [Builder API](https://docs.rs/clap/latest/clap/_tutorial/index.html).
The reason for this is that the Derive API makes it:
- Easier to read, write, and modify.
- Easier to keep the argument declaration and reading of argument in sync.
- Easier to reuse shared arguments.

An existing example of how to use the Derive API in `sentry-cli` is the `send-metric`command.

# Integration Tests

Integration tests are written using `trycmd` crate. Consult the docs in case you need to understand how it works https://docs.rs/trycmd/latest/trycmd/.
Expand Down
107 changes: 83 additions & 24 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ bytecount = "0.6.3"
chardet = "0.2.4"
chrono = { version = "0.4.31", features = ["serde"] }
clap = { version = "4.1.6", default-features = false, features = [
"derive",
"std",
"suggestions",
"wrap_help",
Expand All @@ -26,6 +27,7 @@ clap = { version = "4.1.6", default-features = false, features = [
] }
clap_complete = "4.4.3"
console = "0.15.5"
crc32fast = "1.4.0"
curl = { version = "0.4.44", features = ["static-curl", "static-ssl"] }
dirs = "4.0.0"
dotenv = "0.15.0"
Expand Down Expand Up @@ -58,10 +60,11 @@ regex = "1.7.3"
runas = "1.0.0"
rust-ini = "0.18.0"
semver = "1.0.16"
sentry = { version = "0.32.2", default-features = false, features = [
sentry = { version = "0.32.3", default-features = false, features = [
"anyhow",
"curl",
"contexts",
"UNSTABLE_metrics",
] }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
Expand Down
45 changes: 45 additions & 0 deletions src/api/envelopes_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use super::{
errors::{ApiErrorKind, ApiResult},
Api, ApiResponse, Method,
};
use crate::constants::USER_AGENT;
use log::debug;
use sentry::{protocol::EnvelopeItem, types::Dsn, Envelope};
use std::sync::Arc;

pub struct EnvelopesApi {
api: Arc<Api>,
dsn: Dsn,
}

impl EnvelopesApi {
pub fn try_new() -> ApiResult<EnvelopesApi> {
let api = Api::current();
match api.config.get_dsn() {
Ok(dsn) => Ok(EnvelopesApi { api, dsn }),
Err(_) => Err(ApiErrorKind::DsnMissing.into()),
}
}

pub fn send_item(&self, item: EnvelopeItem) -> ApiResult<ApiResponse> {
let mut envelope = Envelope::new();
envelope.add_item(item);
self.send_envelope(envelope)
}

pub fn send_envelope(&self, envelope: Envelope) -> ApiResult<ApiResponse> {
let mut body: Vec<u8> = Vec::new();
envelope
.to_writer(&mut body)
.map_err(|_| ApiErrorKind::CannotSerializeEnvelope)?;
debug!("Sending envelope:\n{}", String::from_utf8_lossy(&body));
let url = self.dsn.envelope_api_url();
let auth = self.dsn.to_auth(Some(USER_AGENT));
self.api
.request(Method::Post, url.as_str(), None)?
.with_header("X-Sentry-Auth", &auth.to_string())?
.with_body(body)?
.send()?
.into_result()
}
}
7 changes: 7 additions & 0 deletions src/api/errors/api_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub struct ApiError {
pub(in crate::api) enum ApiErrorKind {
#[error("could not serialize value as JSON")]
CannotSerializeAsJson,
#[error("could not serialize envelope")]
CannotSerializeEnvelope,
#[error("could not parse JSON response")]
BadJson,
#[error("not a JSON response")]
Expand All @@ -38,6 +40,11 @@ pub(in crate::api) enum ApiErrorKind {
"Auth token is required for this request. Please run `sentry-cli login` and try again!"
)]
AuthMissing,
#[error(
"DSN not found. To configure your DSN, export the environment variable SENTRY_DSN or \
add it to your ~/.sentryclirc config as follows:\n[auth]\ndsn = <DSN>"
)]
DsnMissing,
}

impl fmt::Display for ApiError {
Expand Down
7 changes: 7 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! to the GitHub API to figure out if there are new releases of the
//! sentry-cli tool.
pub mod envelopes_api;

mod connection_manager;
mod encoding;
mod errors;
Expand Down Expand Up @@ -1746,6 +1748,11 @@ impl ApiRequest {
Ok(self)
}

pub fn with_body(mut self, body: Vec<u8>) -> ApiResult<Self> {
self.body = Some(body);
Ok(self)
}

/// attaches some form data to the request.
pub fn with_form_data(mut self, form: curl::easy::Form) -> ApiResult<Self> {
debug!("sending form data");
Expand Down
36 changes: 36 additions & 0 deletions src/commands/derive_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::utils::auth_token::AuthToken;
use crate::utils::value_parsers::kv_parser;
use clap::{command, value_parser, ArgAction::SetTrue, Parser, Subcommand};

use super::send_metric::SendMetricArgs;

#[derive(Parser)]
pub(super) struct SentryCLI {
#[command(subcommand)]
pub(super) command: SentryCLICommand,

#[arg(global=true, long="header", value_name="KEY:VALUE", value_parser=kv_parser)]
#[arg(help = "Custom headers that should be attached to all requests{n}in key:value format")]
pub(super) headers: Vec<(String, String)>,

#[arg(global=true, long, value_parser=value_parser!(AuthToken))]
#[arg(help = "Use the given Sentry auth token")]
pub(super) auth_token: Option<AuthToken>,

#[arg(global=true, ignore_case=true, value_parser=["trace", "debug", "info", "warn", "error"])]
#[arg(long, help = "Set the log output verbosity")]
pub(super) log_level: Option<String>,

#[arg(global=true, action=SetTrue, visible_alias="silent", long)]
#[arg(help = "Do not print any output while preserving correct exit code. \
This flag is currently implemented only for selected subcommands")]
pub(super) quiet: bool,

#[arg(global=true, action=SetTrue, long, hide=true, help="Always return 0 exit code")]
pub(super) allow_failure: bool,
}

#[derive(Subcommand)]
pub(super) enum SentryCLICommand {
SendMetric(SendMetricArgs),
}
Loading

0 comments on commit b19bcb9

Please sign in to comment.