From cf89cc01f4f6a5e607c229d4c4930c53540f4410 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Wed, 10 Sep 2025 01:14:56 +0000 Subject: [PATCH] feat: extract otel autoconfig from pusher --- --- a/Cargo.lock +++ b/Cargo.lock @@ -673,6 +673,15 @@ ] [[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] name = "crossterm" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1000,6 +1009,16 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] name = "fleet" version = "0.2.0" dependencies = [ @@ -1025,6 +1044,8 @@ "nixlike", "nom 8.0.0", "openssh", + "opentelemetry", + "opentelemetry_sdk", "owo-colors", "peg", "regex", @@ -1038,6 +1059,7 @@ "tokio-util", "tracing", "tracing-indicatif", + "tracing-opentelemetry", "tracing-subscriber", ] @@ -1168,6 +1190,15 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1492,6 +1523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1499,7 +1531,9 @@ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.6.0", "tokio", @@ -1598,6 +1632,113 @@ ] [[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1684,6 +1825,22 @@ checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" [[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] name = "is-terminal" version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1799,6 +1956,12 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] name = "litrs" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2050,6 +2213,93 @@ ] [[package]] +name = "opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-exporter-env" +version = "0.1.0" +dependencies = [ + "clap", + "opentelemetry-otlp", + "thiserror 2.0.16", +] + +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +dependencies = [ + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tonic 0.13.1", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +dependencies = [ + "base64 0.22.1", + "hex", + "opentelemetry", + "opentelemetry_sdk", + "prost", + "serde", + "tonic 0.13.1", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.2", + "serde_json", + "thiserror 2.0.16", +] + +[[package]] name = "owo-colors" version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2251,6 +2501,15 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2484,6 +2743,40 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-http 0.6.6", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2817,6 +3110,18 @@ ] [[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2937,6 +3242,12 @@ ] [[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2983,8 +3294,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "tabled" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3103,9 +3428,9 @@ "tokio", "tokio-stream", "tokio-util", - "tonic", + "tonic 0.12.3", "tonic-build", - "tower-http", + "tower-http 0.5.2", "tracing", "tracing-subscriber", ] @@ -3306,6 +3631,33 @@ ] [[package]] +name = "tonic" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "flate2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] name = "tonic-build" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3347,10 +3699,15 @@ dependencies = [ "futures-core", "futures-util", + "indexmap 2.11.0", "pin-project-lite", + "slab", "sync_wrapper", + "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3371,6 +3728,24 @@ ] [[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3438,6 +3813,24 @@ ] [[package]] +name = "tracing-opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] name = "tracing-serde" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3567,6 +3960,24 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3691,6 +4102,19 @@ ] [[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3723,6 +4147,16 @@ ] [[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "web-time" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3997,6 +4431,12 @@ checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" [[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] name = "wsl" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4024,6 +4464,30 @@ ] [[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] name = "z85" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4054,8 +4518,23 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] [[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4076,10 +4555,34 @@ ] [[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] name = "zerovec" version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ + "yoke", "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] --- a/cmds/fleet/Cargo.toml +++ b/cmds/fleet/Cargo.toml @@ -46,6 +46,9 @@ indicatif = { version = "0.18", optional = true } nom = "8.0.0" tracing-indicatif = { version = "0.3", optional = true } +tracing-opentelemetry = "0.31.0" +opentelemetry = "0.30.0" +opentelemetry_sdk = "0.30.0" [features] default = [] --- a/cmds/fleet/src/main.rs +++ b/cmds/fleet/src/main.rs @@ -4,7 +4,7 @@ // pub(crate) mod command; pub(crate) mod extra_args; -use std::{ffi::OsString, process::ExitCode}; +use std::{env, ffi::OsString, process::ExitCode}; use anyhow::{Result, bail}; use clap::{CommandFactory, Parser}; @@ -27,7 +27,7 @@ use tracing::{Instrument, error, info, info_span}; #[cfg(feature = "indicatif")] use tracing_indicatif::IndicatifLayer; -use tracing_subscriber::{fmt::format::Format, prelude::*, EnvFilter}; +use tracing_subscriber::{EnvFilter, fmt::format::Format, prelude::*}; #[derive(Parser)] struct Prefetch {} @@ -170,6 +170,9 @@ let sub = sub.with_writer(indicatif_layer.get_stderr_writer()); sub.with_filter(filter) // .without, }); + + if env::var_os("FLEET_OTEL").is_some() {} + // #[cfg(feature = "indicatif")] #[cfg(feature = "indicatif")] let reg = reg.with(indicatif_layer); --- /dev/null +++ b/crates/opentelemetry-exporter-env/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "opentelemetry-exporter-env" +version.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +clap = { workspace = true, optional = true } +opentelemetry-otlp = { version = "0.30.0", features = ["grpc-tonic", "gzip-tonic", "http-json"], optional = true } +thiserror.workspace = true + +[features] +default = ["clap", "otlp"] +clap = ["dep:clap"] +otlp = ["dep:opentelemetry-otlp"] --- /dev/null +++ b/crates/opentelemetry-exporter-env/src/lib.rs @@ -0,0 +1,264 @@ +use std::collections::HashMap; +use std::convert::Infallible; +use std::env::{self, VarError}; +use std::ffi::OsString; +use std::num::ParseIntError; +use std::str::FromStr; +use std::time::Duration; + +use clap::Parser; +#[cfg(feature = "otlp")] +use opentelemetry_otlp::tonic_types::metadata::MetadataMap; +#[cfg(feature = "otlp")] +use opentelemetry_otlp::{ + ExporterBuildError, LogExporter, MetricExporter, SpanExporter, WithExportConfig, + WithHttpConfig, WithTonicConfig, +}; + +#[cfg(feature = "otlp")] +mod otlp; + +pub enum Error { + InvalidUtf8 { + env: &'static str, + value: OsString, + }, + EnvParseError { + env: &'static str, + value: String, + error: &'static str, + }, + EnvParseIntError { + env: &'static str, + value: String, + error: ParseIntError, + }, +} +impl From<(&'static str, &'static str, String)> for Error { + fn from((env, error, value): (&'static str, &'static str, String)) -> Self { + Self::EnvParseError { env, value, error } + } +} +impl From<(&'static str, ParseIntError, String)> for Error { + fn from((env, error, value): (&'static str, ParseIntError, String)) -> Self { + Self::EnvParseIntError { env, value, error } + } +} +impl From<(&'static str, Infallible, String)> for Error { + fn from(_v: (&'static str, Infallible, String)) -> Self { + unreachable!() + } +} + +fn load_env(env: &'static str) -> Result, Error> +where + T: FromStr, + Error: From<(&'static str, ::Err, String)>, +{ + match env::var(env) { + Ok(v) => Ok(Some(T::from_str(&v).map_err(|err| (env, err, v))?)), + Err(VarError::NotPresent) => Ok(None), + Err(VarError::NotUnicode(value)) => Err(Error::InvalidUtf8 { env, value }), + } +} + +macro_rules! impl_settings { + ( + #[name($env_prefix:literal, $long_prefix:literal)] + struct $id:ident { + $( + $(#[doc = $doc:literal])* + #[name($env:literal, $long:literal)] + $(#[arg($($tt:tt)*)])? + $name:ident: $ty:ty, + )* + }) => { + #[derive(Parser)] + pub struct $id { + $( + $(#[doc = $doc])* + #[arg(long = concat!("otel-exporter-otlp-", $long_prefix, $long), env = concat!("OTEL_EXPORTER_OTLP_", $env_prefix, $env), $($($tt)*)?)] + pub $name: Option<$ty>, + )* + } + impl $id { + pub fn from_env() -> Result { + Ok(Self { + $( + $name: load_env(concat!("OTEL_EXPORTER_OTLP_", $env_prefix, $env))?, + )* + }) + } + } + } +} +macro_rules! impl_enum { + (enum $id:ident { + $( + #[name = $value:literal] + $var:ident, + )* + }) => { + #[derive(Clone, Copy)] + #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] + pub enum $id { + $( + #[cfg_attr(feature = "clap", value(name = $value))] + $var, + )* + } + impl FromStr for $id { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + $( + $value => Self::$var, + )* + _ => return Err("unsupported value, supported are") + }) + } + } + }; +} + +impl_enum! { + enum Compression { + #[name = "gzip"] + Gzip, + #[name = "zstd"] + Zstd, + } +} +#[cfg(feature = "otlp")] +impl From for opentelemetry_otlp::Compression { + fn from(value: Compression) -> Self { + match value { + Compression::Gzip => opentelemetry_otlp::Compression::Gzip, + Compression::Zstd => opentelemetry_otlp::Compression::Zstd, + } + } +} + +impl_enum! { + enum OtlpProtocol { + #[name = "grpc"] + Grpc, + #[name = "http/protobuf"] + HttpProtobuf, + #[name = "http/json"] + HttpJson, + } +} +#[cfg(feature = "otlp")] +impl From for opentelemetry_otlp::Protocol { + fn from(value: OtlpProtocol) -> Self { + match value { + OtlpProtocol::Grpc => opentelemetry_otlp::Protocol::Grpc, + OtlpProtocol::HttpProtobuf => opentelemetry_otlp::Protocol::HttpBinary, + OtlpProtocol::HttpJson => opentelemetry_otlp::Protocol::HttpJson, + } + } +} + +impl_settings! { + #[name("", "")] + struct OtlpBaseSettings { + /// Specifies the OTLP transport compression to be used for all telemetry data. + #[name("COMPRESSION", "compression")] + #[arg(value_enum)] + compression: Compression, + /// A base endpoint URL for any signal type, with an optionally-specified port number. Helpful for when you’re sending more than one signal to the same endpoint and want one environment variable to control the endpoint. + #[name("ENDPOINT", "endpoint")] + endpoint: String, + /// A list of headers to apply to all outgoing data (traces, metrics, and logs). + #[name("HEADERS", "headers")] + headers: String, + /// Specifies the OTLP transport protocol to be used for all telemetry data. + #[name("PROTOCOL", "protocol")] + #[arg(value_enum)] + protocol: OtlpProtocol, + /// The timeout value for all outgoing data (traces, metrics, and logs) in milliseconds. + #[name("TIMEOUT", "timeout")] + timeout: u64, + } +} +impl_settings! { + #[name("LOGS_", "logs-")] + struct OtlpLogsSettings { + /// Specifies the OTLP transport compression to be used for log data. + #[name("COMPRESSION", "compression")] + #[arg(value_enum)] + compression: Compression, + /// Endpoint URL for log data only, with an optionally-specified port number. Typically ends with `v1/logs` when using OTLP/HTTP. + #[name("ENDPOINT", "endpoint")] + endpoint: String, + /// A list of headers to apply to all outgoing logs. + #[name("HEADERS", "headers")] + headers: String, + /// Specifies the OTLP transport protocol to be used for log data. + #[name("PROTOCOL", "protocol")] + #[arg(value_enum)] + protocol: OtlpProtocol, + /// The timeout value for all outgoing logs in milliseconds. + #[name("TIMEOUT", "timeout")] + timeout: u64, + } +} +impl_settings! { + #[name("METRICS_", "metrics-")] + struct OtlpMetricsSettings { + /// Specifies the OTLP transport compression to be used for metrics data. + #[name("COMPRESSION", "compression")] + #[arg(value_enum)] + compression: Compression, + /// Endpoint URL for metric data only, with an optionally-specified port number. Typically ends with `v1/metrics` when using OTLP/HTTP. + #[name("ENDPOINT", "endpoint")] + endpoint: String, + /// A list of headers to apply to all outgoing metrics. + #[name("HEADERS", "headers")] + headers: String, + /// Specifies the OTLP transport protocol to be used for metrics data. + #[name("PROTOCOL", "protocol")] + #[arg(value_enum)] + protocol: OtlpProtocol, + /// The timeout value for all outgoing metrics in milliseconds. + #[name("TIMEOUT", "timeout")] + timeout: u64, + } +} + +impl_settings! { + #[name("TRACES_", "traces-")] + struct OtlpTracesSettings { + /// Specifies the OTLP transport compression to be used for trace data. + #[name("COMPRESSION", "compression")] + #[arg(value_enum)] + compression: Compression, + /// Endpoint URL for trace data only, with an optionally-specified port number. Typically ends with `v1/traces` when using OTLP/HTTP. + #[name("ENDPOINT", "endpoint")] + endpoint: String, + /// A list of headers to apply to all outgoing traces. + #[name("HEADERS", "headers")] + headers: String, + /// Specifies the OTLP transport protocol to be used for trace data. + #[name("PROTOCOL", "protocol")] + #[arg(value_enum)] + protocol: OtlpProtocol, + /// The timeout value for all outgoing traces in milliseconds. + #[name("TIMEOUT", "timeout")] + timeout: u64, + } +} + +#[derive(thiserror::Error, Debug)] +enum ProviderError { + #[error("protocol is not set")] + UnsetProtocol, + #[error("endpoint is not set")] + EndpointUnset, + #[cfg(feature = "otlp")] + #[error("failed to build exporter: {0}")] + Exporter(#[from] ExporterBuildError), +} +type ProviderResult = Result; --- /dev/null +++ b/crates/opentelemetry-exporter-env/src/otlp.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; +use std::time::Duration; + +use opentelemetry_otlp::tonic_types::metadata::MetadataMap; +use opentelemetry_otlp::{ + LogExporter, MetricExporter, SpanExporter, WithExportConfig as _, WithHttpConfig as _, + WithTonicConfig as _, +}; + +use crate::{ + OtlpBaseSettings, OtlpLogsSettings, OtlpMetricsSettings, OtlpProtocol, ProviderError, + ProviderResult, +}; + +fn parse_headers<'a>( + headers: &'a str, +) -> std::iter::Map, impl FnMut(&'a str) -> (&'a str, &'a str)> { + headers.split(',').map(|header| { + let mut parts = header.splitn(2, '='); + let key = parts.next().unwrap(); + let value = parts.next().unwrap_or(""); + (key, value) + }) +} + +fn parse_headers_metadata_map(headers: Option<&str>) -> MetadataMap { + headers + .map(|headers| { + MetadataMap::from_headers( + parse_headers(headers) + .map(|(key, value)| (key.parse().unwrap(), value.parse().unwrap())) + .collect(), + ) + }) + .unwrap_or_default() +} +fn parse_headers_hashmap(headers: Option<&str>) -> HashMap { + headers + .map(|headers| { + parse_headers(headers) + .map(|(key, value)| (key.into(), value.into())) + .collect() + }) + .unwrap_or_default() +} + +fn logger_exporter(base: &OtlpBaseSettings, log: &OtlpLogsSettings) -> ProviderResult { + let endpoint = log + .endpoint + .clone() + .or_else(|| Some(format!("{}/v1/logs", base.endpoint.as_ref()?))) + .ok_or(ProviderError::EndpointUnset)?; + let headers = log.headers.as_deref().or(base.headers.as_deref()); + let timeout = Duration::from_millis(log.timeout.or(base.timeout).unwrap_or(10000)); + + let protocol = log + .protocol + .or(base.protocol) + .ok_or(ProviderError::UnsetProtocol)?; + + match protocol { + OtlpProtocol::Grpc => { + let mut builder = LogExporter::builder() + .with_tonic() + .with_endpoint(endpoint) + .with_metadata(parse_headers_metadata_map(headers)) + .with_protocol(protocol.into()) + .with_timeout(timeout); + let compression = log.compression.or(base.compression); + if let Some(compression) = compression { + builder = builder.with_compression(compression.into()); + } + + Ok(builder.build()?) + } + OtlpProtocol::HttpProtobuf | OtlpProtocol::HttpJson => { + let builder = LogExporter::builder() + .with_http() + .with_endpoint(endpoint) + .with_headers(parse_headers_hashmap(headers)) + .with_protocol(protocol.into()) + .with_timeout(timeout); + + Ok(builder.build()?) + } + } +} +fn metric_exporter( + base: &OtlpBaseSettings, + metric: &OtlpMetricsSettings, +) -> ProviderResult { + let endpoint = metric + .endpoint + .clone() + .or_else(|| Some(format!("{}/v1/metrics", base.endpoint.as_ref()?))) + .ok_or(ProviderError::EndpointUnset)?; + let headers = metric.headers.as_deref().or(base.headers.as_deref()); + let timeout = Duration::from_millis(metric.timeout.or(base.timeout).unwrap_or(10000)); + + let protocol = metric + .protocol + .or(base.protocol) + .ok_or(ProviderError::UnsetProtocol)?; + + match protocol { + OtlpProtocol::Grpc => { + let mut builder = MetricExporter::builder() + .with_tonic() + .with_endpoint(endpoint) + .with_metadata(parse_headers_metadata_map(headers)) + .with_protocol(protocol.into()) + .with_timeout(timeout); + let compression = metric.compression.or(base.compression); + if let Some(compression) = compression { + builder = builder.with_compression(compression.into()); + } + + Ok(builder.build()?) + } + OtlpProtocol::HttpProtobuf | OtlpProtocol::HttpJson => { + let builder = MetricExporter::builder() + .with_http() + .with_endpoint(endpoint) + .with_headers(parse_headers_hashmap(headers)) + .with_protocol(protocol.into()) + .with_timeout(timeout); + + Ok(builder.build()?) + } + } +} +fn span_exporter( + base: &OtlpBaseSettings, + trace: &OtlpMetricsSettings, +) -> ProviderResult { + let endpoint = trace + .endpoint + .clone() + .or_else(|| Some(format!("{}/v1/traces", base.endpoint.as_ref()?))) + .ok_or(ProviderError::EndpointUnset)?; + let headers = trace.headers.as_deref().or(base.headers.as_deref()); + let timeout = Duration::from_millis(trace.timeout.or(base.timeout).unwrap_or(10000)); + + let protocol = trace + .protocol + .or(base.protocol) + .ok_or(ProviderError::UnsetProtocol)?; + + match protocol { + OtlpProtocol::Grpc => { + let mut builder = SpanExporter::builder() + .with_tonic() + .with_endpoint(endpoint) + .with_metadata(parse_headers_metadata_map(headers)) + .with_protocol(protocol.into()) + .with_timeout(timeout); + let compression = trace.compression.or(base.compression); + if let Some(compression) = compression { + builder = builder.with_compression(compression.into()); + } + + Ok(builder.build()?) + } + OtlpProtocol::HttpProtobuf | OtlpProtocol::HttpJson => { + let builder = SpanExporter::builder() + .with_http() + .with_endpoint(endpoint) + .with_headers(parse_headers_hashmap(headers)) + .with_protocol(protocol.into()) + .with_timeout(timeout); + + Ok(builder.build()?) + } + } +} -- gitstuff