diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..93d474d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,54 @@ +name: Sanity Check codebase + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + node: [stable, beta, nightly] + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.node }} + override: true + + - name: Run cargo check + uses: actions-rs/cargo@v1 + with: + command: check + + - name: Run cargo test + run: make check + + lints: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1b67eb5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "opentelemetry-common" +version = "0.1.0" +edition = "2021" + +[dependencies] +opentelemetry = { version = "0.25", features = ["logs"] } +opentelemetry-appender-log = { version = "0.25", default-features = false } +opentelemetry_sdk = { version = "0.25", features = [ "logs", "rt-tokio" ] } +opentelemetry-otlp = { version = "0.25", features = [ "http-proto", "reqwest-client", "reqwest-rustls", "logs" ] } +opentelemetry-semantic-conventions = { version = "0.25.0" } +anyhow = "^1" +log = { version = "0.4", features = ["std"] } + +[dev-dependencies] +clap = { version = "4.0.26", features = ["derive"] } +tokio = { version = "^1.29.1", features = ["rt-multi-thread", "parking_lot"] } +env_logger = "0.11.3" diff --git a/examples/main.rs b/examples/main.rs new file mode 100644 index 0000000..62fd641 --- /dev/null +++ b/examples/main.rs @@ -0,0 +1,44 @@ +use clap::Parser; + +use opentelemetry_common::Opentelemetry; + +#[derive(Debug, Parser)] +#[clap(name = "opentelemetry.rs")] +pub struct Args { + #[clap(short, long, value_parser)] + pub url: String, + #[clap(short, long, value_parser)] + pub message: String, + #[clap(short, long)] + pub level: String, +} + +#[macro_export] +macro_rules! async_run { + ($rt:expr, $expr:expr) => {{ + $rt.block_on($expr) + }}; + ($expr:expr) => {{ + let rt = tokio::runtime::Runtime::new().unwrap(); + $crate::async_run!(rt, $expr) + }}; +} + +// the async main is not required by our application +// but the opentelemetry app is requiring to be +// in an async context, so we use this +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let url = args.url; + + let mut manager = Opentelemetry::new(); + manager.init_log("example", &args.level, &url)?; + + match args.level.as_str() { + "info" => log::info!("{}", args.message), + "debug" => log::debug!("{}", args.message), + _ => anyhow::bail!("level `{}` not found", args.level), + } + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..40e8233 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,43 @@ +pub mod log; +pub use anyhow; + +use std::sync::Arc; + +use opentelemetry::global; +use opentelemetry_sdk::logs as sdklogs; + +#[derive(Debug, Clone)] +pub struct Opentelemetry { + pub(crate) logger: Option>, +} + +impl Default for Opentelemetry { + fn default() -> Self { + Self::new() + } +} + +impl Opentelemetry { + pub fn new() -> Self { + Opentelemetry { logger: None } + } + + pub fn init_log( + &mut self, + tag: &str, + level: &str, + exporter_endpoint: &str, + ) -> anyhow::Result<()> { + log::init(self, tag.to_owned(), level, exporter_endpoint)?; + Ok(()) + } +} + +impl Drop for Opentelemetry { + fn drop(&mut self) { + let Some(Err(err)) = self.logger.as_ref().map(|log| log.shutdown()) else { + return; + }; + panic!("Failed to shutdown logger: {:?}", err); + } +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..e3a359e --- /dev/null +++ b/src/log.rs @@ -0,0 +1,47 @@ +//! Logging module. +use std::str::FromStr; +use std::sync::Arc; + +use opentelemetry::KeyValue; +use opentelemetry_appender_log::OpenTelemetryLogBridge; +use opentelemetry_otlp::HttpExporterBuilder; +use opentelemetry_otlp::Protocol; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::Resource; + +use crate::Opentelemetry; + +/// Initialize a new logger exported with open telemetry. +pub fn init( + manager: &mut Opentelemetry, + tag: String, + level: &str, + exporter_endpoint: &str, +) -> anyhow::Result<()> { + let logger_provider = opentelemetry_otlp::new_pipeline() + .logging() + .with_resource(Resource::new(vec![KeyValue::new( + opentelemetry_semantic_conventions::resource::SERVICE_NAME, + tag, + )])) + .with_exporter( + http_exporter() + .with_protocol(Protocol::HttpBinary) //can be changed to `Protocol::HttpJson` to export in JSON format + .with_endpoint(format!("{exporter_endpoint}/v1/logs")), + ) + .install_batch(opentelemetry_sdk::runtime::Tokio)?; + manager.logger = Some(Arc::new(logger_provider.clone())); + + // Setup Log Appender for the log crate. + let otel_log_appender = OpenTelemetryLogBridge::new(&logger_provider); + + // the install method set a global provider, that we can use now + log::set_boxed_logger(Box::new(otel_log_appender)).map_err(|err| anyhow::anyhow!("{err}"))?; + let level = log::Level::from_str(level)?; + log::set_max_level(level.to_level_filter()); + Ok(()) +} + +fn http_exporter() -> HttpExporterBuilder { + opentelemetry_otlp::new_exporter().http() +}