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..6236577 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "opentelemetry-log" +version = "0.1.0" +edition = "2021" +authors = ["Vincent "] +description = "A common library for OpenTelemetry logging in Rust." +license = "GPL-2.0" +repository = "https://github.com/vincent/opentelemetry-log" +documentation = "https://docs.rs/opentelemetry-common" +homepage = "https://github.com/vincent/opentelemetry-log" +keywords = ["opentelemetry", "logging", "log", "rust"] +categories = ["development-tools"] + +[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/LICENSE b/LICENSE index d159169..2dd0860 100644 --- a/LICENSE +++ b/LICENSE @@ -290,8 +290,9 @@ to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + A minimal and simple opentelemetry log adapter that allow you to + export your rust log to an opentelemetry collector. + Copyright (C) 2024 Vincenzo Palazzo This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f392918 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +CC=cargo +FMT=fmt + +OPTIONS= + +default: fmt + $(CC) build + @make clippy + +fmt: + $(CC) fmt --all + +check: + $(CC) test --all + +example: + @echo "No example for the moment" + +clean: + $(CC) clean + +clippy: + $(CC) clippy --all --tests + +coffee: + $(CC) build --release diff --git a/README.md b/README.md index 0660678..a1cb504 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,43 @@ # opentelemetry-log -A minimal and simple opentelemetry log adapter that allow you to export your rust log to an opentelemetry collector + +[![CI](https://github.com/vincent/open-source/opentelemetry-log/actions/workflows/ci.yml/badge.svg)](https://github.com/vincent/open-source/opentelemetry-log/actions/workflows/ci.yml) +[![Latest Version](https://img.shields.io/crates/v/opentelemetry-log.svg)](https://crates.io/crates/opentelemetry-log) +[![License](https://img.shields.io/crates/l/opentelemetry-log.svg)](https://github.com/vincent/open-source/opentelemetry-log/blob/main/LICENSE) + +A minimal and simple OpenTelemetry log adapter that allows you to export your Rust logs to an OpenTelemetry collector. + +## Features + +- Export Rust logs to an OpenTelemetry collector +- Minimal and simple adapter +- Easy integration with existing logging (just `log` for now) frameworks + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +opentelemetry-log = "0.1" +``` + +## Usage + +```rust +use opentelemetry_common::Opentelemetry; + +fn main() { + let mut manager = Opentelemetry::new(); + manager.init_log("example", &args.level, &url)?; + // Your application code +} +``` + +## License + +This project is licensed under the GNU General Public License. See the [LICENSE](LICENSE) file for details. + +## How to Deploy OpenTelemetry with Grafana + +How to inspect the logs is something that is dependent on the user, but if you are starting from scratch and +you want to learn how to work with Grafana and OpenTelemetry, I suggest starting from here: https://github.com/grafana/docker-otel-lgtm \ No newline at end of file diff --git a/examples/main.rs b/examples/main.rs new file mode 100644 index 0000000..c3c8b70 --- /dev/null +++ b/examples/main.rs @@ -0,0 +1,33 @@ +use clap::Parser; + +use opentelemetry_log::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, +} + +// 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..acf3076 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,97 @@ +//! # OpenTelemetry Log Integration +//! +//! This crate provides integration with OpenTelemetry for logging purposes. It allows you to +//! initialize and manage loggers that are compatible with the OpenTelemetry SDK. +//! +//! ## Features +//! +//! - Initialize loggers with specific tags, levels, and exporter endpoints. +//! - Automatically manage the lifecycle of loggers, ensuring proper shutdown. +//! +//! ## Usage +//! +//! Add this crate to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! opentelemetry-log = "0.1" +//! ``` +//! +//! Import and use the `Opentelemetry` struct to manage your loggers: +//! +//! ```rust +//! use opentelemetry_log::Opentelemetry; +//! +//! #[tokio::main] +//! async fn main() { +//! let mut otel = Opentelemetry::new(); +//! otel.init_log("my_app", "info", "http://localhost:4317").unwrap(); // Please do not unwrap in production code +//! // Your application logic here +//! } +//! ``` +//! +//! ## Modules +//! +//! - `log`: Contains the log initialization logic. +//! +//! ## Structs +//! +//! - `Opentelemetry`: Main struct for managing OpenTelemetry loggers. +//! +//! ## Traits +//! +//! - `Default`: Provides a default implementation for `Opentelemetry`. +//! - `Drop`: Ensures proper shutdown of loggers when `Opentelemetry` instances are dropped. +//! +//! ## Errors +//! +//! This crate uses the `anyhow` crate for error handling. Ensure you handle errors appropriately +//! when initializing and using loggers. +pub mod log; +pub use anyhow; + +use std::sync::Arc; + +use opentelemetry_sdk::logs as sdklogs; + +/// Main struct for managing OpenTelemetry loggers, when you init the logger +/// remember to keep this alive for all the lifetime of the application. +/// +/// An example can be found in the `examples` directory. +#[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 } + } + + /// Initialize a new logger with the provided tag, level, and exporter endpoint. + /// this is assuming tat your application is using `log` crate + 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..98422b9 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,54 @@ +//! This module provides functionality to initialize and configure a logger that exports logs +//! using OpenTelemetry. The logger can be configured with different levels and exporter endpoints. +//! +//! # Functions +//! +//! - `init`: Initializes a new logger exported with OpenTelemetry. It sets up the logger provider, +//! configures the log appender, and sets the global logger. +//! - `http_exporter`: Creates a new HTTP exporter builder for OpenTelemetry. +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() +}