From 7728856ad83e61a61cff3de4ceb59b24510b84eb Mon Sep 17 00:00:00 2001 From: Heinz Gies Date: Wed, 22 May 2024 15:57:34 +0200 Subject: [PATCH 1/3] API update Signed-off-by: Heinz Gies --- .github/workflows/ci.yaml | 25 +++ Cargo.toml | 28 +-- README.md | 10 +- examples/fmt/main.rs | 2 +- examples/layers/main.rs | 63 +++---- examples/noenv/main.rs | 22 +-- examples/simple/main.rs | 6 +- src/builder.rs | 370 +++++++++++++++----------------------- src/error.rs | 8 +- src/lib.rs | 38 +++- 10 files changed, 257 insertions(+), 315 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 957461c..be7a7c3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -72,6 +72,31 @@ jobs: AXIOM_DATASET: _traces with: command: test + + validate-crate: + name: Clippy check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Install clippy + run: rustup component add clippy + - name: Run cargo test + uses: actions-rs/cargo@v1 + env: + AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }} + AXIOM_URL: https://cloud.dev.axiomtestlabs.co + AXIOM_DATASET: _traces + with: + command: publish --dry-run + + publish_on_crates_io: name: Publish on crates.io runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 68ef100..a899615 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,13 +25,9 @@ include = [ resolver = "2" [dependencies] -opentelemetry = { version = "0.22" } -opentelemetry-otlp = { version = "0.15", features = [ - "prost", - "tokio", - "http-proto", - "reqwest-client", -] } +url = "2.4.1" +thiserror = "1" + tracing-core = { version = "0.1", default-features = false, features = ["std"] } tracing-opentelemetry = { version = "0.23", default-features = false } tracing-subscriber = { version = "0.3", default-features = false, features = [ @@ -41,18 +37,22 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "json", ] } + + reqwest = { version = "0.11", default-features = false } -thiserror = "1" -opentelemetry-semantic-conventions = "0.14" -url = "2.4.1" -opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] } +opentelemetry = { version = "0.22" } +opentelemetry-otlp = { version = "0.15", features = [ + "prost", + "tokio", + "http-proto", + "reqwest-client", +] } +opentelemetry-semantic-conventions = "0.15" +opentelemetry_sdk = { version = "0.22", features = ["rt-tokio"] } [dev-dependencies] tokio = { version = "1", features = ["full", "tracing"] } tracing = { version = "0.1", features = ["log"] } -opentelemetry = { version = "0.22" } -anyhow = "1.0.80" -uuid = { version = "1", features = ["v4"] } tracing-subscriber = { version = "0.3", default-features = false, features = [ "smallvec", "std", diff --git a/README.md b/README.md index 87f46db..e8f467b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ For more information check out the [official documentation](https://axiom.co/doc ## Usage -Add the following to your Cargo.toml: +Add the following to your `Cargo.toml`: ```toml [dependencies] @@ -36,14 +36,12 @@ Then create an API token with ingest permission into that dataset in Now you can set up tracing in one line like this: ```rust,no_run +use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, Registry}; #[tokio::main] async fn main() -> Result<(), Box> { - tracing_axiom::init()?; + let axiom_layer = tracing_axiom::default("readme")?; // Set AXIOM_DATASET and AXIOM_TOKEN in your env! + Registry::default().with(axiom_layer).init(); say_hello(); - - // Ensure that the tracing provider is shutdown correctly - opentelemetry::global::shutdown_tracer_provider(); - Ok(()) } diff --git a/examples/fmt/main.rs b/examples/fmt/main.rs index 353b1c0..a6b2ca2 100644 --- a/examples/fmt/main.rs +++ b/examples/fmt/main.rs @@ -3,7 +3,7 @@ use tracing_subscriber::prelude::*; #[tokio::main] async fn main() -> Result<(), Box> { - let axiom_layer = tracing_axiom::builder().with_service_name("fmt").layer()?; + let axiom_layer = tracing_axiom::builder("fmt").build()?; let fmt_layer = tracing_subscriber::fmt::layer().pretty(); tracing_subscriber::registry() .with(fmt_layer) diff --git a/examples/layers/main.rs b/examples/layers/main.rs index 77013a5..7fd9ec0 100644 --- a/examples/layers/main.rs +++ b/examples/layers/main.rs @@ -1,48 +1,34 @@ use opentelemetry::global; use opentelemetry_sdk::propagation::TraceContextPropagator; use tracing::{info, instrument}; -use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::Registry; -use uuid::Uuid; +use tracing_subscriber::{prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt}; #[instrument] -fn say_hi(id: Uuid, name: impl Into + std::fmt::Debug) { +fn say_hi(id: u64, name: impl Into + std::fmt::Debug) { info!(?id, "Hello, {}!", name.into()); } -fn setup_tracing(otel_is_configured: bool, tags: &[(&str, &str)]) -> Result<(), anyhow::Error> { - if otel_is_configured { - info!("Axiom OpenTelemetry tracing endpoint is configured:"); - // Setup an AWS CloudWatch compatible tracing layer - let cloudwatch_layer = tracing_subscriber::fmt::layer() - .json() - .with_ansi(false) - .without_time() - .with_target(false); - - // Setup an Axiom OpenTelemetry compatible tracing layer - let axiom_layer = tracing_axiom::builder() - .with_service_name("layers") - .with_tags(tags) - .layer()?; - - // Setup our multi-layered tracing subscriber - Registry::default() - .with(axiom_layer) - .with(cloudwatch_layer) - .init(); - } else { - info!("OpenTelemetry is not configured: Using AWS CloudWatch savvy format",); - tracing_subscriber::fmt() - .json() - .with_max_level(tracing::Level::INFO) - .with_current_span(false) - .with_ansi(false) - .without_time() - .with_target(false) - .init(); - }; +fn setup_tracing(tags: &[(&'static str, &'static str)]) -> Result<(), tracing_axiom::Error> { + info!("Axiom OpenTelemetry tracing endpoint is configured:"); + // Setup an AWS CloudWatch compatible tracing layer + let cloudwatch_layer = tracing_subscriber::fmt::layer() + .json() + .with_ansi(false) + .without_time() + .with_target(false); + + // Setup an Axiom OpenTelemetry compatible tracing layer + let tag_iter = tags.iter().copied(); + let axiom_layer = tracing_axiom::builder("layers") + .with_tags(tag_iter) + .build()?; + + // Setup our multi-layered tracing subscriber + Registry::default() + .with(axiom_layer) + .with(cloudwatch_layer) + .init(); global::set_text_map_propagator(TraceContextPropagator::new()); @@ -55,10 +41,9 @@ const TAGS: &[(&str, &str)] = &[ #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<(), Box> { - setup_tracing(true, TAGS)?; // NOTE we depend on environment variable + setup_tracing(TAGS)?; // NOTE we depend on environment variable - let uuid = Uuid::new_v4(); - say_hi(uuid, "world"); + say_hi(42, "world"); // do something with result ... diff --git a/examples/noenv/main.rs b/examples/noenv/main.rs index a7910ad..59222c3 100644 --- a/examples/noenv/main.rs +++ b/examples/noenv/main.rs @@ -1,23 +1,23 @@ use tracing::{info, instrument}; -use uuid::Uuid; +use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, Registry}; #[instrument] -fn say_hi(id: Uuid, name: impl Into + std::fmt::Debug) { +fn say_hi(id: u64, name: impl Into + std::fmt::Debug) { info!(?id, "Hello, {}!", name.into()); } #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<(), Box> { - tracing_axiom::builder() - .with_service_name("noenv") - .with_tags(&[("aws_region", "us-east-1")]) // Set otel tags - .with_dataset("tracing-axiom-examples") // Set dataset - .with_token("xaat-some-valid-token") // Set API token - .with_url("http://localhost:4318") // Set URL, can be changed to any OTEL endpoint - .init()?; // Initialize tracing + let axiom_layer = tracing_axiom::builder("noenv") + .with_tags([("aws_region", "us-east-1")].iter().copied()) // Set otel tags + .with_dataset("tracing-axiom-examples")? // Set dataset + .with_token("xaat-some-valid-token")? // Set API token + .with_url("http://localhost:4318")? // Set URL, can be changed to any OTEL endpoint + .build()?; // Initialize tracing - let uuid = Uuid::new_v4(); - say_hi(uuid, "world"); + Registry::default().with(axiom_layer).init(); + + say_hi(42, "world"); // do something with result ... diff --git a/examples/simple/main.rs b/examples/simple/main.rs index c8ad18b..e823528 100644 --- a/examples/simple/main.rs +++ b/examples/simple/main.rs @@ -1,4 +1,5 @@ use tracing::{error, instrument}; +use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, Registry}; #[instrument] fn say_hello() { @@ -7,7 +8,10 @@ fn say_hello() { #[tokio::main] async fn main() -> Result<(), Box> { - tracing_axiom::init()?; + let axiom_layer = tracing_axiom::default("simple")?; + + Registry::default().with(axiom_layer).init(); + say_hello(); // Ensure that the tracing provider is shutdown correctly diff --git a/src/builder.rs b/src/builder.rs index 731d361..5c7ba57 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,5 +1,5 @@ use crate::Error; -use opentelemetry::{Key, KeyValue}; +use opentelemetry::{Key, KeyValue, Value}; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::{ trace::{Config as TraceConfig, Tracer}, @@ -12,41 +12,14 @@ use reqwest::Url; use std::{ collections::HashMap, env::{self, VarError}, - marker::PhantomData, time::Duration, }; use tracing_core::Subscriber; use tracing_opentelemetry::OpenTelemetryLayer; -use tracing_subscriber::{ - layer::{Layered, SubscriberExt}, - registry::LookupSpan, - util::SubscriberInitExt, - Layer, Registry, -}; +use tracing_subscriber::registry::LookupSpan; const CLOUD_URL: &str = "https://api.axiom.co"; -/// A layer that sends traces to Axiom via the `OpenTelemetry` protocol. -/// The layer cleans up the `OpenTelemetry` global tracer provider on drop. -type AxiomOpenTelemetryComposedLayer = - Layered, AxiomOpenTelemetryLayer, S>; - -/// A layer that sends traces to Axiom via the `OpenTelemetry` protocol. -/// The layer cleans up the `OpenTelemetry` global tracer provider on drop. -pub struct AxiomOpenTelemetryLayer(PhantomData); -impl Default for AxiomOpenTelemetryLayer { - fn default() -> Self { - Self(PhantomData) - } -} - -impl Layer for AxiomOpenTelemetryLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, - Self: 'static, -{ -} - /// Builder for creating a tracing tracer, a layer or a subscriber that sends traces to /// Axiom via the `OpenTelemetry` protocol. The API token is read from the `AXIOM_TOKEN` /// environment variable. The dataset name is read from the `AXIOM_DATASET` environment @@ -57,78 +30,64 @@ where pub struct Builder { dataset_name: Option, token: Option, - url: Option, + url: Option, tags: Vec, trace_config: Option, service_name: Option, - no_env: bool, + timeout: Option, } -#[allow(clippy::match_same_arms)] // We want clarity here -fn resolve_configurable( - should_check_environment: bool, - env_var_name: &'static str, - explicit_var: &Option, - predicate_check: fn(value: &Option) -> Result, -) -> Result { - match ( - should_check_environment, - env::var(env_var_name), - explicit_var, - ) { - // If we're skipping the environment variables, we need to have an explicit var - (false, _, maybe_ok_var) => match predicate_check(maybe_ok_var) { - Ok(valid_var) => Ok(valid_var), - Err(err) => Err(err), - }, - // If we respect the environment variables, and token is not set explicitly, use them - (true, Ok(maybe_ok_var), _) => match predicate_check(&Some(maybe_ok_var)) { - Ok(valid_var) => Ok(valid_var), - Err(err) => Err(err), - }, - // If env or programmatic token are invalid, fail and bail - (true, Err(VarError::NotPresent), &None) => Err(Error::EnvVarMissing(env_var_name)), - (true, Err(VarError::NotPresent), maybe_ok_var) => match predicate_check(maybe_ok_var) { - Ok(valid_var) => Ok(valid_var), - Err(err) => Err(err), - }, - (true, Err(VarError::NotUnicode(_)), _) => { - Err(Error::EnvVarNotUnicode(env_var_name.to_string())) - } +fn get_env(env_var_name: &'static str) -> Result, Error> { + match env::var(env_var_name) { + Ok(maybe_ok_var) => Ok(Some(maybe_ok_var)), + Err(VarError::NotPresent) => Ok(None), + Err(VarError::NotUnicode(_)) => Err(Error::EnvVarNotUnicode(env_var_name.to_string())), } } impl Builder { - /// Create a new Builder. - #[must_use] - pub fn new() -> Self { - Self { - url: Some(CLOUD_URL.to_string()), - ..Default::default() - } - } - /// Set the Axiom dataset name to use. The dataset name is the name of the /// persistent dataset in Axiom cloud that will store the traces and make /// them available for querying using APL, the Axiom SDK or the Axiom CLI. + /// + /// # Errors + /// If the dataset name is empty. #[must_use] - pub fn with_dataset(mut self, dataset_name: impl Into) -> Self { - self.dataset_name = Some(dataset_name.into()); - self + pub fn with_dataset(mut self, dataset_name: impl Into) -> Result { + let dataset_name: String = dataset_name.into(); + if dataset_name.is_empty() { + Err(Error::EmptyDataset) + } else { + self.dataset_name = Some(dataset_name); + Ok(self) + } } /// Set the Axiom API token to use. + /// + /// # Errors + /// If the token is empty or does not start with `xaat-` (aka is not a api token). #[must_use] - pub fn with_token(mut self, token: impl Into) -> Self { - self.token = Some(token.into()); - self + pub fn with_token(mut self, token: impl Into) -> Result { + let token: String = token.into(); + if token.is_empty() { + Err(Error::EmptyToken) + } else if !token.starts_with("xaat-") { + Err(Error::InvalidToken) + } else { + self.token = Some(token); + Ok(self) + } } - /// Set the Axiom API URL to use. Defaults to Axiom Cloud. + /// Set the Axiom API URL to use. Defaults to Axiom Cloud. When not set Axiom Cloud is used. + /// + /// # Errors + /// If the URL is not a valid URL. #[must_use] - pub fn with_url(mut self, url: impl Into) -> Self { - self.url = Some(url.into()); - self + pub fn with_url(mut self, url: &str) -> Result { + self.url = Some(url.parse()?); + Ok(self) } /// Set the trace config. @@ -146,102 +105,77 @@ impl Builder { self } - /// Don't fall back to environment variables. + /// Set the resource tags for the open telemetry tracer that publishes to Axiom. + /// These tags will be added to all spans. #[must_use] - pub fn no_env(mut self) -> Self { - self.no_env = true; + pub fn with_tags(mut self, tags: T) -> Self + where + K: Into, + V: Into, + T: Iterator, + { + self.tags = tags.map(|(k, v)| KeyValue::new(k, v)).collect::>(); self } - /// Set the resource tags for the open telemetry tracer that publishes to Axiom. - /// These tags will be added to all spans. - /// - /// # Errors + /// Sets the collector timeout for the OTLP exporter. + /// The default is 3 seconds. /// - /// Returns an error if a key is invalid. #[must_use] - pub fn with_tags(mut self, tags: &[(&str, &str)]) -> Self { - self.tags = tags - .iter() - .map(|(k, v)| KeyValue::new(Key::from((*k).to_string()), (*v).to_string())) - .collect::>(); + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); self } - /// Initialize the global subscriber. This panics if the initialization was - /// unsuccessful, likely because a global subscriber was already installed or - /// `AXIOM_TOKEN` is not set or invalid. + /// Load defaults from environment variables, if variables were set before this call they will not be replaced. /// - /// # Errors - /// - /// Returns an error if the initialization was unsuccessful, likely because - /// a global subscriber was already installed or `AXIOM_TOKEN` is not set or - /// invalid. + /// The following environment variables are used: + /// - `AXIOM_TOKEN` + /// - `AXIOM_DATASET` + /// - `AXIOM_URL` /// - pub fn init(self) -> Result<(), Error> { - let layer = self.layer()?; - Registry::default().with(layer).try_init()?; - Ok(()) + /// # Errors + /// If an environment variable is not valid UTF8, or any of their values are invalid. + #[must_use] + pub fn with_env(mut self) -> Result { + if self.token.is_none() { + if let Some(t) = get_env("AXIOM_TOKEN")? { + self = self.with_token(t)? + } + }; + + if self.dataset_name.is_none() { + if let Some(d) = get_env("AXIOM_DATASET")? { + self = self.with_dataset(d)? + } + }; + if self.url.is_none() { + if let Some(u) = get_env("AXIOM_URL")? { + self = self.with_url(&u)? + } + }; + + Ok(self) } - /// Create a layer which sends traces to Axiom and a Guard which will shut - /// down the tracer provider on drop. + /// Create a layer which sends traces to Axiom that can be added to the tracing layers. /// /// # Errors /// - /// Returns an error if the initialization was unsuccessful, likely because - /// a global subscriber was already installed or `AXIOM_TOKEN` is not set or - /// invalid. - /// - pub fn layer(self) -> Result, Error> + /// Returns an error if any of the settings are not valid + pub fn build(self) -> Result, Error> where S: Subscriber + for<'span> LookupSpan<'span>, { - let tracer = self.tracer()?; - let inner_layer: OpenTelemetryLayer = - tracing_opentelemetry::layer().with_tracer(tracer); - let layer = AxiomOpenTelemetryLayer::default().and_then(inner_layer); - Ok(layer) - } - - fn resolve_token(&self) -> Result { - let token = &self.token; - resolve_configurable(!self.no_env, "AXIOM_TOKEN", token, |token| match token { - Some(token) if token.is_empty() => Err(Error::EmptyToken), - Some(token) if !token.starts_with("xaat-") => Err(Error::InvalidToken), - Some(token) => Ok(token.clone()), - None => Err(Error::MissingToken), - }) - } - - fn resolve_dataset_name(&self) -> Result { - let dataset_name = &self.dataset_name; - resolve_configurable( - !self.no_env, - "AXIOM_DATASET", - dataset_name, - |dataset_name| match dataset_name { - Some(dataset_name) if dataset_name.is_empty() => Err(Error::EmptyDatasetName), - Some(dataset_name) => Ok(dataset_name.clone()), - None => Err(Error::MissingDatasetName), - }, - ) - } - - fn resolve_axiom_url(&self) -> Result { - let url = &self.url; - resolve_configurable(!self.no_env, "AXIOM_URL", url, |url| match url { - Some(url) => Ok(url.clone()), - None => Ok(CLOUD_URL.to_string()), - }) + Ok(tracing_opentelemetry::layer().with_tracer(self.tracer()?)) } fn tracer(self) -> Result { - let token = self.resolve_token()?; - let dataset_name = self.resolve_dataset_name()?; - let url = self.resolve_axiom_url()?; - - let url = url.parse::()?; + let token = self.token.ok_or(Error::MissingToken)?; + let dataset_name = self.dataset_name.ok_or(Error::MissingDataset)?; + let url = self + .url + .unwrap_or_else(|| CLOUD_URL.to_string().parse().unwrap()); let mut headers = HashMap::with_capacity(2); headers.insert("Authorization".to_string(), format!("Bearer {token}")); @@ -268,16 +202,15 @@ impl Builder { .unwrap_or_default() .with_resource(Resource::new(tags)); + let pipeline = opentelemetry_otlp::new_exporter() + .http() + .with_http_client(reqwest::Client::new()) + .with_endpoint(url) + .with_headers(headers) + .with_timeout(self.timeout.unwrap_or(Duration::from_secs(3))); let tracer = opentelemetry_otlp::new_pipeline() .tracing() - .with_exporter( - opentelemetry_otlp::new_exporter() - .http() - .with_http_client(reqwest::Client::new()) - .with_endpoint(url) - .with_headers(headers) - .with_timeout(Duration::from_secs(3)), - ) + .with_exporter(pipeline) .with_trace_config(trace_config) .install_batch(opentelemetry_sdk::runtime::Tokio)?; Ok(tracer) @@ -286,8 +219,10 @@ impl Builder { #[cfg(test)] mod tests { - use super::*; + use tracing_subscriber::Registry; + + use super::{Error, *}; fn cache_axiom_env() -> Result, Box> { let mut saved_env = std::env::vars().collect::>(); // Cache AXIOM env vars and remove from env for test @@ -309,34 +244,31 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_no_env_skips_env_variables() -> Result<(), Error> { - let builder = Builder::new().no_env(); - assert!(builder.no_env); + let builder = Builder::default(); assert_eq!(builder.token, None); assert_eq!(builder.dataset_name, None); - assert_eq!(builder.url, Some("https://api.axiom.co".into())); + assert_eq!(builder.url, None); - let err: Result = Builder::new().no_env().tracer(); + let err: Result = Builder::default().tracer(); matches!(err, Err(Error::MissingToken)); - let mut builder = Builder::new().no_env(); - builder.token = Some("xaat-snot".into()); - let err = builder.tracer(); - matches!(err, Err(Error::MissingDatasetName)); + let builder = Builder::default() + .with_token("xaat-snot")? + .with_dataset("test")?; + matches!(builder.tracer(), Err(Error::MissingDataset)); - let mut builder = Builder::new().no_env(); - builder.token = Some("xaat-snot".into()); - builder.dataset_name = Some("test".into()); - let ok = builder.tracer(); - assert!(ok.is_ok()); + let builder = Builder::default() + .with_token("xaat-snot")? + .with_dataset("test")?; + matches!(builder.tracer(), Err(Error::MissingDataset)); + + let builder = Builder::default() + .with_token("xaat-snot")? + .with_dataset("test")?; + assert!(builder.tracer().is_ok()); - let mut builder = Builder::new().no_env(); - builder.token = Some("xaat-snot".into()); - builder.dataset_name = Some("test".into()); - builder.url = Some("".into()); - let err = builder.tracer(); - assert!(err.is_err()); matches!( - err, + Builder::default().with_url(""), Err(Error::InvalidUrl(url::ParseError::RelativeUrlWithoutBase)) ); @@ -347,18 +279,15 @@ mod tests { async fn with_env_respects_env_variables() -> Result<(), Box> { let cached_env = cache_axiom_env()?; - let builder = Builder::new(); - assert!(!builder.no_env); - - let err = Builder::new().tracer(); + let err = Builder::default().tracer(); matches!(err, Err(Error::EnvVarMissing("AXIOM_TOKEN"))); std::env::set_var("AXIOM_TOKEN", "xaat-snot"); - let err = Builder::new().tracer(); + let err = Builder::default().tracer(); matches!(err, Err(Error::EnvVarMissing("AXIOM_DATASET"))); std::env::set_var("AXIOM_DATASET", "test"); - let ok = Builder::new().tracer(); + let ok = Builder::default().with_env()?.tracer(); assert!(ok.is_ok()); // NOTE We let this hang wet rather than fake the endpoint as @@ -375,85 +304,66 @@ mod tests { #[test] fn test_missing_token() { - matches!(Builder::new().no_env().init(), Err(Error::MissingToken)); + matches!(Builder::default().tracer(), Err(Error::MissingToken)); } #[test] fn test_empty_token() { - matches!( - Builder::new().no_env().with_token("").init(), - Err(Error::EmptyToken) - ); + matches!(Builder::default().with_token(""), Err(Error::EmptyToken)); } #[test] fn test_invalid_token() { matches!( - Builder::new().no_env().with_token("invalid").init(), + Builder::default().with_token("invalid"), Err(Error::InvalidToken) ); } #[test] - fn test_invalid_url() { + fn test_invalid_url() -> Result<(), Error> { matches!( - Builder::new() - .no_env() - .with_token("xaat-123456789") - .with_dataset("test") - .with_url("") - .init(), + Builder::default() + .with_token("xaat-123456789")? + .with_dataset("test")? + .with_url(""), Err(Error::InvalidUrl(_)) ); + Ok(()) } #[tokio::test(flavor = "multi_thread")] - async fn test_valid_token() { + async fn test_valid_token() -> Result<(), Error> { // Note that we can't test the init/try_init funcs here because OTEL // gets confused with the global subscriber. - let result = Builder::new() - .no_env() - .with_dataset("test") - .with_token("xaat-123456789") - .layer::(); + let result = Builder::default() + .with_dataset("test")? + .with_token("xaat-123456789")? + .build::(); assert!(result.is_ok(), "{:?}", result.err()); + Ok(()) } #[tokio::test(flavor = "multi_thread")] - async fn test_valid_token_env() { + async fn test_valid_token_env() -> Result<(), Error> { // Note that we can't test the init/try_init funcs here because OTEL // gets confused with the global subscriber. let env_backup = env::var("AXIOM_TOKEN"); env::set_var("AXIOM_TOKEN", "xaat-1234567890"); - let result = Builder::new().with_dataset("test").layer::(); + let result = Builder::default() + .with_dataset("test")? + .with_env()? + .build::(); if let Ok(token) = env_backup { env::set_var("AXIOM_TOKEN", token); } assert!(result.is_ok(), "{:?}", result.err()); - } - - #[test] - #[cfg(feature = "unstable")] - fn test_env_var() { - use std::ffi::OsStr; - let result = resolve_configurable(true, "BAD_ENV_VAR", &None, |_| Ok("ok".to_string())); - assert_eq!(Err(Error::EnvVarMissing("BAD_ENV_VAR".to_string())), result); - // NOTE unstable feature - so we cannot assert this on stable yet - let non_unicode_utf8_str = - unsafe { OsStr::from_encoded_bytes_unchecked(b"\xFF\xFE\x41\x42snot") }; // No NUL bytes! - env::set_var("BAD_ENV_VAR", non_unicode_utf8_str); - let result = resolve_configurable(true, "BAD_ENV_VAR", &Some("ok".to_string()), |_| { - Ok("ok".to_string()) - }); - assert_eq!( - Err(Error::EnvVarNotUnicode("BAD_ENV_VAR".to_string())), - result - ); + Ok(()) } } diff --git a/src/error.rs b/src/error.rs index 866dd1a..590ff84 100644 --- a/src/error.rs +++ b/src/error.rs @@ -25,12 +25,12 @@ pub enum Error { InvalidToken, /// The required Axiom dataset name is missing. - #[error("Dataset name is missing")] - MissingDatasetName, + #[error("Dataset is missing")] + MissingDataset, /// The required Axiom dataset name is empty. - #[error("Dataset name is empty")] - EmptyDatasetName, + #[error("Dataset is empty")] + EmptyDataset, /// The required Axiom dataset name is invalid. #[error("Invalid URL: {0}")] diff --git a/src/lib.rs b/src/lib.rs index ba3108d..a460fd0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,9 +18,11 @@ //! configure it like this: //! //! ```rust,no_run +//! use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, Registry}; //! #[tokio::main] //! async fn main() -> Result<(), Box> { -//! tracing_axiom::init()?; // Set AXIOM_DATASET and AXIOM_TOKEN in your env! +//! let axiom_layer = tracing_axiom::default("doctests")?; // Set AXIOM_DATASET and AXIOM_TOKEN in your env! +//! Registry::default().with(axiom_layer).init(); //! say_hello(); //! Ok(()) //! } @@ -39,28 +41,46 @@ mod error; pub use builder::Builder; pub use error::Error; +use opentelemetry_sdk::trace::Tracer; +use tracing_core::Subscriber; +use tracing_opentelemetry::OpenTelemetryLayer; +use tracing_subscriber::registry::LookupSpan; #[cfg(doctest)] #[doc = include_str!("../README.md")] pub struct ReadmeDoctests; -/// Initialize a global subscriber which sends traces to Axiom. +/// Creates a default [`OpenTelemetryLayer`] with a [`Tracer`] that sends traces to Axiom. /// -/// It uses the environment variables `AXIOM_TOKEN` and optionally `AXIOM_URL` +/// It uses the environment variables `AXIOM_TOKEN` and optionally `AXIOM_URL` and `AXIOM_DATASET` /// to configure the endpoint. -/// If you want to manually set these, see [`Builder`]. +/// If you want to manually set these or other attributres, use `builder()` or `builder_with_env()`. /// /// # Errors /// /// Errors if the initialization was unsuccessful, likely because a global /// subscriber was already installed or `AXIOM_TOKEN` and/or `AXIOM_DATASET` /// is not set or invalid. -pub fn init() -> Result<(), Error> { - builder().init() +pub fn default(service_name: &str) -> Result, Error> +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + builder_with_env(service_name)?.build() } -/// Create a new [`Builder`]. +/// Create a new [`Builder`] and set the configuratuin from the environment. +/// +/// # Errors +/// If any of the environment variables are invalid, missing variables are not causing errors as +/// they can be set later. +pub fn builder_with_env(service_name: &str) -> Result { + Ok(Builder::default() + .with_env()? + .with_service_name(service_name)) +} + +/// Create a new [`Builder`] with no defaults set. #[must_use] -pub fn builder() -> Builder { - Builder::new() +pub fn builder(service_name: &str) -> Builder { + Builder::default().with_service_name(service_name) } From f84b1ddb6deea7f35a827b8e723e9f2ed2437e6f Mon Sep 17 00:00:00 2001 From: Heinz Gies Date: Wed, 22 May 2024 16:53:46 +0200 Subject: [PATCH 2/3] Obey clippy Signed-off-by: Heinz Gies --- src/builder.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 5c7ba57..e7c28e1 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -52,7 +52,6 @@ impl Builder { /// /// # Errors /// If the dataset name is empty. - #[must_use] pub fn with_dataset(mut self, dataset_name: impl Into) -> Result { let dataset_name: String = dataset_name.into(); if dataset_name.is_empty() { @@ -67,7 +66,6 @@ impl Builder { /// /// # Errors /// If the token is empty or does not start with `xaat-` (aka is not a api token). - #[must_use] pub fn with_token(mut self, token: impl Into) -> Result { let token: String = token.into(); if token.is_empty() { @@ -84,7 +82,6 @@ impl Builder { /// /// # Errors /// If the URL is not a valid URL. - #[must_use] pub fn with_url(mut self, url: &str) -> Result { self.url = Some(url.parse()?); Ok(self) @@ -136,22 +133,21 @@ impl Builder { /// /// # Errors /// If an environment variable is not valid UTF8, or any of their values are invalid. - #[must_use] pub fn with_env(mut self) -> Result { if self.token.is_none() { if let Some(t) = get_env("AXIOM_TOKEN")? { - self = self.with_token(t)? + self = self.with_token(t)?; } }; if self.dataset_name.is_none() { if let Some(d) = get_env("AXIOM_DATASET")? { - self = self.with_dataset(d)? + self = self.with_dataset(d)?; } }; if self.url.is_none() { if let Some(u) = get_env("AXIOM_URL")? { - self = self.with_url(&u)? + self = self.with_url(&u)?; } }; @@ -175,7 +171,7 @@ impl Builder { let dataset_name = self.dataset_name.ok_or(Error::MissingDataset)?; let url = self .url - .unwrap_or_else(|| CLOUD_URL.to_string().parse().unwrap()); + .unwrap_or_else(|| CLOUD_URL.to_string().parse().expect("this is a valid URL")); let mut headers = HashMap::with_capacity(2); headers.insert("Authorization".to_string(), format!("Bearer {token}")); From bf82372b8a977a74547a8555b9965f3a3e9851f7 Mon Sep 17 00:00:00 2001 From: Heinz Gies Date: Wed, 22 May 2024 16:56:19 +0200 Subject: [PATCH 3/3] Fix CI Signed-off-by: Heinz Gies --- .github/workflows/ci.yaml | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index be7a7c3..f88dc98 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,6 +1,6 @@ name: CI -on: +on: pull_request: branches: - main @@ -23,10 +23,6 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install rust - uses: actions-rs/toolchain@v1 - with: - components: rustfmt - name: Run cargo fmt uses: actions-rs/cargo@v1 with: @@ -62,19 +58,15 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install rust - uses: actions-rs/toolchain@v1 - name: Run cargo test - uses: actions-rs/cargo@v1 env: AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }} AXIOM_URL: https://cloud.dev.axiomtestlabs.co AXIOM_DATASET: _traces - with: - command: test + run: cargo test validate-crate: - name: Clippy check + name: Validate crate runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -85,17 +77,8 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install clippy - run: rustup component add clippy - - name: Run cargo test - uses: actions-rs/cargo@v1 - env: - AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }} - AXIOM_URL: https://cloud.dev.axiomtestlabs.co - AXIOM_DATASET: _traces - with: - command: publish --dry-run - + - name: Test publish + run: cargo publish --dry-run publish_on_crates_io: name: Publish on crates.io @@ -107,9 +90,7 @@ jobs: - test steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - - uses: actions-rs/cargo@v1 - with: - command: publish + - name: Publish on crates.io env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish