diff --git a/Cargo.toml b/Cargo.toml index c3a7d15..d7c9689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,20 +15,60 @@ include = ["src/**/*.rs", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] resolver = "2" [dependencies] -opentelemetry = { version = "0.20", features = ["rt-tokio"] } -opentelemetry-otlp = { version = "0.13", features = ["prost", "tokio", "http-proto", "reqwest-client" ] } +opentelemetry = { version = "0.22" } +opentelemetry-otlp = { version = "0.15", features = [ + "prost", + "tokio", + "http-proto", + "reqwest-client", +] } tracing-core = { version = "0.1", default-features = false, features = ["std"] } -tracing-opentelemetry = { version = "0.21", default-features = false } -tracing-subscriber = { version = "0.3", default-features = false, features = ["smallvec", "std", "registry", "fmt", "json" ] } +tracing-opentelemetry = { version = "0.23", default-features = false } +tracing-subscriber = { version = "0.3", default-features = false, features = [ + "smallvec", + "std", + "registry", + "fmt", + "json", +] } reqwest = { version = "0.11", default-features = false } thiserror = "1" -opentelemetry-semantic-conventions = "0.12" +opentelemetry-semantic-conventions = "0.14" url = "2.4.1" +opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] } [dev-dependencies] -tokio = "1" -tracing = { version = "0.1", features = [ "log" ] } -opentelemetry = { version = "0.20", features = ["rt-tokio"] } +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", + "registry", + "fmt", + "json", + "ansi", +] } + +# Example that demonstrates how to use the `tracing-axiom` layer with the `tracing-subscriber` crate. +[[example]] +name = "layers" + +# Simple most example use of `tracing-axiom`. +[[example]] +name = "simple" + +# Example that demonstrates using a nice color and formating +[[example]] +name = "fmt" + + +# Demonstrate setting config in the code +[[example]] +name = "noenv" + [features] default = ["default-tls"] diff --git a/README.md b/README.md index 6baa6cb..87f46db 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ Now you can set up tracing in one line like this: async fn main() -> Result<(), Box> { tracing_axiom::init()?; say_hello(); + + // Ensure that the tracing provider is shutdown correctly + opentelemetry::global::shutdown_tracer_provider(); + Ok(()) } @@ -52,7 +56,7 @@ pub fn say_hello() { For further examples, head over to the [examples](examples) directory. > **Note**: Due to a limitation of an underlying library, [events outside of a -> span are not recorded](https://docs.rs/tracing-opentelemetry/0.17.4/src/tracing_opentelemetry/layer.rs.html#807). +> span are not recorded](https://docs.rs/tracing-opentelemetry/latest/src/tracing_opentelemetry/layer.rs.html#807). ## Features @@ -66,6 +70,7 @@ that can be enabled or disabled: - **rustls-tls**: Enables TLS functionality provided by `rustls`. ## FAQ & Troubleshooting + ### How do I log traces to the console in addition to Axiom? You can use this library to get a [`tracing-subscriber::layer`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html) and combine it with other layers, for example one that prints traces to the diff --git a/examples/fmt/Cargo.toml b/examples/fmt/Cargo.toml deleted file mode 100644 index f17cad4..0000000 --- a/examples/fmt/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "fmt" -version = "0.1.0" -edition = "2021" - -[dependencies] -tokio = "1" -tracing = "0.1" -tracing-axiom = { path = "../../" } -tracing-subscriber = "0.3" - diff --git a/examples/fmt/src/main.rs b/examples/fmt/main.rs similarity index 80% rename from examples/fmt/src/main.rs rename to examples/fmt/main.rs index c693d41..353b1c0 100644 --- a/examples/fmt/src/main.rs +++ b/examples/fmt/main.rs @@ -12,6 +12,9 @@ async fn main() -> Result<(), Box> { say_hello(); + // Ensure that the tracing provider is shutdown correctly + opentelemetry::global::shutdown_tracer_provider(); + Ok(()) } diff --git a/examples/layers/Cargo.toml b/examples/layers/Cargo.toml deleted file mode 100644 index f51dc0d..0000000 --- a/examples/layers/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "layers" -version = "0.1.0" -edition = "2021" - -[dependencies] -tokio = "1" -tracing = "0.1" -tracing-axiom = { path = "../../" } -uuid = { version = "1", features = ["v4"] } -tracing-subscriber = "0.3.17" -opentelemetry-otlp = "0.13.0" -tracing-opentelemetry = "0.21.0" -anyhow = "1.0.75" -opentelemetry = "0.20.0" diff --git a/examples/layers/src/main.rs b/examples/layers/main.rs similarity index 91% rename from examples/layers/src/main.rs rename to examples/layers/main.rs index 9f02fa9..082b6c0 100644 --- a/examples/layers/src/main.rs +++ b/examples/layers/main.rs @@ -1,5 +1,5 @@ use opentelemetry::global; -use opentelemetry::sdk::propagation::TraceContextPropagator; +use opentelemetry_sdk::propagation::TraceContextPropagator; use tracing::{info, instrument}; use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -59,5 +59,8 @@ async fn main() -> Result<(), Box> { // do something with result ... + // Ensure that the tracing provider is shutdown correctly + opentelemetry::global::shutdown_tracer_provider(); + Ok(()) } diff --git a/examples/noenv/Cargo.toml b/examples/noenv/Cargo.toml deleted file mode 100644 index 778ad53..0000000 --- a/examples/noenv/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "noenv" -version = "0.1.0" -edition = "2021" - -[dependencies] -tokio = "1" -tracing = "0.1" -tracing-axiom = { path = "../../" } -uuid = { version = "1", features = ["v4"] } diff --git a/examples/noenv/src/main.rs b/examples/noenv/main.rs similarity index 68% rename from examples/noenv/src/main.rs rename to examples/noenv/main.rs index 2b5669d..074c94d 100644 --- a/examples/noenv/src/main.rs +++ b/examples/noenv/main.rs @@ -10,9 +10,9 @@ fn say_hi(id: Uuid, name: impl Into + std::fmt::Debug) { async fn main() -> Result<(), Box> { tracing_axiom::builder() .with_tags(&[("aws_region", "us-east-1")]) // Set otel tags - .with_dataset("tracing-axiom-examples".to_string()) // Set dataset + .with_dataset("tracing-axiom-examples") // Set dataset .with_token("xaat-some-valid-token") // Set API token - .with_url("Some valid URL other than default") // Set URL + .with_url("http://localhost:4318") // Set URL, can be changed to any OTEL endpoint .init()?; // Initialize tracing let uuid = Uuid::new_v4(); @@ -20,5 +20,8 @@ async fn main() -> Result<(), Box> { // do something with result ... + // Ensure that the tracing provider is shutdown correctly + opentelemetry::global::shutdown_tracer_provider(); + Ok(()) } diff --git a/examples/simple/Cargo.toml b/examples/simple/Cargo.toml deleted file mode 100644 index 8f02c9a..0000000 --- a/examples/simple/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "simple" -version = "0.1.0" -edition = "2021" - -[dependencies] -tokio = "1" -tracing = "0.1" -tracing-axiom = { path = "../../" } - diff --git a/examples/simple/main.rs b/examples/simple/main.rs new file mode 100644 index 0000000..c8ad18b --- /dev/null +++ b/examples/simple/main.rs @@ -0,0 +1,17 @@ +use tracing::{error, instrument}; + +#[instrument] +fn say_hello() { + error!("hello world") +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_axiom::init()?; + say_hello(); + + // Ensure that the tracing provider is shutdown correctly + opentelemetry::global::shutdown_tracer_provider(); + + Ok(()) +} diff --git a/examples/simple/src/main.rs b/examples/simple/src/main.rs deleted file mode 100644 index e89d390..0000000 --- a/examples/simple/src/main.rs +++ /dev/null @@ -1,14 +0,0 @@ -use tracing::{info, instrument}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_axiom::init()?; - say_hello(); - - Ok(()) -} - -#[instrument] -fn say_hello() { - info!("hello world") -} diff --git a/src/builder.rs b/src/builder.rs index 7ee53b0..f36002b 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,57 +1,42 @@ -use opentelemetry::{ - sdk::{ - trace::{Config as TraceConfig, Tracer}, - Resource, - }, - Key, KeyValue, -}; +use crate::Error; +use opentelemetry::{Key, KeyValue}; use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::{ + trace::{Config as TraceConfig, Tracer}, + Resource, +}; use opentelemetry_semantic_conventions::resource::{ SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION, }; +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::SubscriberExt, registry::LookupSpan, util::SubscriberInitExt, Layer, Registry, + layer::{Layered, SubscriberExt}, + registry::LookupSpan, + util::SubscriberInitExt, + Layer, Registry, }; -use crate::Error; -use reqwest::Url; - -const CLOUD_URL: &str = "https://cloud.axiom.co"; +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. -pub struct AxiomOpenTelemetryLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, - Self: 'static, -{ - pub(crate) inner: OpenTelemetryLayer, -} - -impl AxiomOpenTelemetryLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, - Self: 'static, -{ - fn with_inner(layer: OpenTelemetryLayer) -> Self { - Self { inner: layer } - } -} +type AxiomOpenTelemetryComposedLayer = + Layered, AxiomOpenTelemetryLayer, S>; -impl Drop for AxiomOpenTelemetryLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, - Self: 'static, -{ - fn drop(&mut self) { - opentelemetry::global::shutdown_tracer_provider(); +/// 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) } } @@ -60,29 +45,6 @@ where S: Subscriber + for<'span> LookupSpan<'span>, Self: 'static, { - fn on_enter( - &self, - id: &tracing_core::span::Id, - ctx: tracing_subscriber::layer::Context<'_, S>, - ) { - self.inner.on_enter(id, ctx); - } - - fn on_exit(&self, id: &tracing_core::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) { - self.inner.on_exit(id, ctx); - } - - fn on_close(&self, id: tracing_core::span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) { - self.inner.on_close(id, ctx); - } - - fn on_event( - &self, - event: &tracing_core::Event<'_>, - ctx: tracing_subscriber::layer::Context<'_, S>, - ) { - self.inner.on_event(event, ctx); - } } /// Builder for creating a tracing tracer, a layer or a subscriber that sends traces to @@ -105,7 +67,7 @@ pub struct Builder { #[allow(clippy::match_same_arms)] // We want clarity here fn resolve_configurable( should_check_environment: bool, - env_var_name: &str, + env_var_name: &'static str, explicit_var: &Option, predicate_check: fn(value: &Option) -> Result, ) -> Result { @@ -120,14 +82,12 @@ fn resolve_configurable( Err(err) => Err(err), }, // If we respect the environment variables, and token is not set explicitly, use them - (true, Ok(maybe_ok_var), &None) => match predicate_check(&Some(maybe_ok_var)) { + (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.to_string())) - } + (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), @@ -135,11 +95,6 @@ fn resolve_configurable( (true, Err(VarError::NotUnicode(_)), _) => { Err(Error::EnvVarNotUnicode(env_var_name.to_string())) } - // Heuston, we have two tokens! Use the explicit override over the env variable - (true, Ok(_), maybe_ok_var) => match predicate_check(maybe_ok_var) { - Ok(valid_var) => Ok(valid_var), - Err(err) => Err(err), - }, } } @@ -238,13 +193,14 @@ impl Builder { /// a global subscriber was already installed or `AXIOM_TOKEN` is not set or /// invalid. /// - pub fn layer(self) -> Result, Error> + pub fn layer(self) -> Result, Error> where S: Subscriber + for<'span> LookupSpan<'span>, { let tracer = self.tracer()?; - let inner_layer = tracing_opentelemetry::layer().with_tracer(tracer); - let layer = AxiomOpenTelemetryLayer::with_inner(inner_layer); + let inner_layer: OpenTelemetryLayer = + tracing_opentelemetry::layer().with_tracer(tracer); + let layer = AxiomOpenTelemetryLayer::default().and_then(inner_layer); Ok(layer) } @@ -285,7 +241,7 @@ impl Builder { let dataset_name = self.resolve_dataset_name()?; let url = self.resolve_axiom_url()?; - let url = url.parse::()?.join("/api/v1/traces")?; + let url = url.parse::()?; let mut headers = HashMap::with_capacity(2); headers.insert("Authorization".to_string(), format!("Bearer {token}")); @@ -323,7 +279,7 @@ impl Builder { .with_timeout(Duration::from_secs(3)), ) .with_trace_config(trace_config) - .install_batch(opentelemetry::runtime::Tokio)?; + .install_batch(opentelemetry_sdk::runtime::Tokio)?; Ok(tracer) } } @@ -345,30 +301,27 @@ mod tests { Ok(saved_env) } - fn restore_axiom_env(saved_env: HashMap) -> Result<(), Error> { + fn restore_axiom_env(saved_env: HashMap) { for (key, value) in saved_env { std::env::set_var(key, value); } - Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_no_env_skips_env_variables() -> Result<(), Error> { let builder = Builder::new().no_env(); - assert_eq!(builder.no_env, true); + assert!(builder.no_env); assert_eq!(builder.token, None); assert_eq!(builder.dataset_name, None); - assert_eq!(builder.url, Some("https://cloud.axiom.co".into())); + assert_eq!(builder.url, Some("https://api.axiom.co".into())); - let err = Builder::new().no_env().tracer(); - assert!(err.is_err()); - assert_eq!(err.unwrap_err(), Error::MissingToken); + let err: Result = Builder::new().no_env().tracer(); + matches!(err, Err(Error::MissingToken)); let mut builder = Builder::new().no_env(); builder.token = Some("xaat-snot".into()); let err = builder.tracer(); - assert!(err.is_err()); - assert_eq!(err.unwrap_err(), Error::MissingDatasetName); + matches!(err, Err(Error::MissingDatasetName)); let mut builder = Builder::new().no_env(); builder.token = Some("xaat-snot".into()); @@ -382,35 +335,27 @@ mod tests { builder.url = Some("".into()); let err = builder.tracer(); assert!(err.is_err()); - assert_eq!( - err.unwrap_err(), - Error::InvalidUrl(url::ParseError::RelativeUrlWithoutBase) + matches!( + err, + Err(Error::InvalidUrl(url::ParseError::RelativeUrlWithoutBase)) ); Ok(()) } #[tokio::test] - async fn with_env_respects_env_variables() -> Result<(), Error> { - let cached_env = cache_axiom_env().unwrap(); + async fn with_env_respects_env_variables() -> Result<(), Box> { + let cached_env = cache_axiom_env()?; let builder = Builder::new(); - assert_eq!(builder.no_env, false); + assert!(!builder.no_env); let err = Builder::new().tracer(); - assert!(err.is_err()); - assert_eq!( - err.unwrap_err(), - Error::EnvVarMissing("AXIOM_TOKEN".to_string()) - ); + matches!(err, Err(Error::EnvVarMissing("AXIOM_TOKEN"))); std::env::set_var("AXIOM_TOKEN", "xaat-snot"); let err = Builder::new().tracer(); - assert!(err.is_err()); - assert_eq!( - err.unwrap_err(), - Error::EnvVarMissing("AXIOM_DATASET".to_string()) - ); + matches!(err, Err(Error::EnvVarMissing("AXIOM_DATASET"))); std::env::set_var("AXIOM_DATASET", "test"); let ok = Builder::new().tracer(); @@ -424,46 +369,42 @@ mod tests { // let ok = Builder::new().tracer(); // assert!(ok.is_ok()); - restore_axiom_env(cached_env)?; + restore_axiom_env(cached_env); Ok(()) } #[test] fn test_missing_token() { - match Builder::new().no_env().init() { - Err(Error::MissingToken) => {} - result => panic!("expected MissingToken, got {:?}", result), - }; + matches!(Builder::new().no_env().init(), Err(Error::MissingToken)); } #[test] fn test_empty_token() { - match Builder::new().no_env().with_token("").init() { - Err(Error::EmptyToken) => {} - result => panic!("expected EmptyToken, got {:?}", result), - }; + matches!( + Builder::new().no_env().with_token("").init(), + Err(Error::EmptyToken) + ); } #[test] fn test_invalid_token() { - match Builder::new().no_env().with_token("invalid").init() { - Err(Error::InvalidToken) => {} - result => panic!("expected InvalidToken, got {:?}", result), - }; + matches!( + Builder::new().no_env().with_token("invalid").init(), + Err(Error::InvalidToken) + ); } #[test] fn test_invalid_url() { - match Builder::new() - .no_env() - .with_token("xaat-123456789") - .with_dataset("test") - .with_url("") - .init() - { - Err(Error::InvalidUrl(_)) => {} - result => panic!("expected InvalidUrl, got {:?}", result), - }; + matches!( + Builder::new() + .no_env() + .with_token("xaat-123456789") + .with_dataset("test") + .with_url("") + .init(), + Err(Error::InvalidUrl(_)) + ); } #[tokio::test(flavor = "multi_thread")] @@ -471,10 +412,12 @@ mod tests { // Note that we can't test the init/try_init funcs here because OTEL // gets confused with the global subscriber. - let result: Result, Error> = Builder::new() + let result = Builder::new() + .no_env() .with_dataset("test") .with_token("xaat-123456789") - .layer(); + .layer::(); + assert!(result.is_ok(), "{:?}", result.err()); } @@ -486,8 +429,7 @@ mod tests { let env_backup = env::var("AXIOM_TOKEN"); env::set_var("AXIOM_TOKEN", "xaat-1234567890"); - let result: Result, Error> = - Builder::new().with_dataset("test").layer(); + let result = Builder::new().with_dataset("test").layer::(); if let Ok(token) = env_backup { env::set_var("AXIOM_TOKEN", token); diff --git a/src/error.rs b/src/error.rs index 4f986c2..866dd1a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -42,25 +42,5 @@ pub enum Error { /// The environment variable is not present. #[error("Environment variable {0} is required but missing")] - EnvVarMissing(String), -} - -impl PartialEq for Error { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - // Coerce partial_eq as inner error is not comparable - (Error::TraceError(_), Error::TraceError(_)) - | (Error::InitErr(_), Error::InitErr(_)) - // Our errors are comparable, natch - | (Error::MissingToken, Error::MissingToken) - | (Error::EmptyToken, Error::EmptyToken) - | (Error::InvalidToken, Error::InvalidToken) - | (Error::MissingDatasetName, Error::MissingDatasetName) - | (Error::EmptyDatasetName, Error::EmptyDatasetName) => true, - (Error::InvalidUrl(lhs), Error::InvalidUrl(rhs)) => lhs == rhs, - (Error::EnvVarNotUnicode(lhs), Error::EnvVarNotUnicode(rhs)) - | (Error::EnvVarMissing(lhs), Error::EnvVarMissing(rhs)) => lhs == rhs, - _ => false, - } - } + EnvVarMissing(&'static str), }