git.delta.rocks / jrsonnet / refs/commits / cf89cc01f4f6

difftreelog

feat extract otel autoconfig from pusher

wvtzsrnqYaroslav Bolyukin2025-09-18parent: #b6dd2cb.patch.diff
in: trunk

6 files changed

modifiedCargo.lockdiffbeforeafterboth
--- 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",
 ]
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- 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 = []
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
before · cmds/fleet/src/main.rs
1#![recursion_limit = "512"]23pub(crate) mod cmds;4// pub(crate) mod command;5pub(crate) mod extra_args;67use std::{ffi::OsString, process::ExitCode};89use anyhow::{Result, bail};10use clap::{CommandFactory, Parser};11use cmds::{12	build_systems::{BuildSystems, Deploy},13	complete::Complete,14	info::Info,15	rollback::RollbackSingle,16	secrets::Secret,17	tf::Tf,18};19use fleet_base::{host::Config, opts::FleetOpts};20use futures::{TryStreamExt, future::LocalBoxFuture, stream::FuturesUnordered};21// use host::Config;22#[cfg(feature = "indicatif")]23use human_repr::HumanCount;24#[cfg(feature = "indicatif")]25use indicatif::{ProgressState, ProgressStyle};26use nix_eval::{gc_register_my_thread, gc_unregister_my_thread, init_libraries};27use tracing::{Instrument, error, info, info_span};28#[cfg(feature = "indicatif")]29use tracing_indicatif::IndicatifLayer;30use tracing_subscriber::{fmt::format::Format, prelude::*, EnvFilter};3132#[derive(Parser)]33struct Prefetch {}34impl Prefetch {35	async fn run(&self, config: &Config) -> Result<()> {36		let mut prefetch_dir = config.directory.to_path_buf();37		prefetch_dir.push("prefetch");38		if !prefetch_dir.is_dir() {39			info!("nothing to prefetch: no prefetch directory");40			return Ok(());41		}42		let tasks = <FuturesUnordered<LocalBoxFuture<Result<()>>>>::new();43		for entry in std::fs::read_dir(&prefetch_dir)? {44			tasks.push(Box::pin(async {45				let entry = entry?;46				if !entry.metadata()?.is_file() {47					bail!("only files should exist in prefetch directory");48				}49				let span = info_span!(50					"prefetching",51					name = entry.file_name().to_string_lossy().as_ref()52				);53				let mut path = OsString::new();54				path.push("file://");55				path.push(entry.path());5657				let mut status = config.local_host().cmd("nix").await?;58				status.args(&config.nix_args);59				status.arg("store").arg("prefetch-file").arg(path);60				status.run_nix_string().instrument(span).await?;61				Ok(())62			}));63		}64		tasks.try_collect::<Vec<()>>().await?;65		Ok(())66	}67}6869#[derive(Parser)]70enum Opts {71	/// Build system closures72	BuildSystems(BuildSystems),73	/// Upload and switch system closures74	Deploy(Deploy),75	/// Rollback remote machine by redeploying old generation as the new one76	RollbackSingle(RollbackSingle),77	/// Secret management78	#[clap(subcommand)]79	Secret(Secret),80	/// Upload prefetch directory to the nix store81	Prefetch(Prefetch),82	/// Config parsing83	Info(Info),84	/// Command completions85	#[clap(hide(true))]86	Complete(Complete),87	/// Compile and evaluate terranix configuration88	Tf(Tf),89}9091#[derive(Parser)]92#[clap(version, author)]93struct RootOpts {94	#[clap(flatten)]95	fleet_opts: FleetOpts,96	#[clap(subcommand)]97	command: Opts,98}99100async fn run_command(config: &Config, opts: FleetOpts, command: Opts) -> Result<()> {101	match command {102		Opts::BuildSystems(c) => c.run(config, &opts).await?,103		Opts::Deploy(d) => d.run(config, &opts).await?,104		Opts::RollbackSingle(r) => r.run(config, &opts).await?,105		Opts::Secret(s) => s.run(config, &opts).await?,106		Opts::Info(i) => i.run(config).await?,107		Opts::Prefetch(p) => p.run(config).await?,108		Opts::Tf(t) => t.run(config).await?,109		// TODO: actually parse commands before starting the async runtime110		Opts::Complete(c) => {111			tokio::task::spawn_blocking(move || c.run(RootOpts::command())).await?112		}113	};114	Ok(())115}116117fn setup_logging() {118	#[cfg(feature = "indicatif")]119	let indicatif_layer = {120		use std::time::Duration;121122		IndicatifLayer::new().with_progress_style(123			ProgressStyle::with_template(124				"{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{download_progress} {elapsed}{color_end}",125			)126				.unwrap()127				.with_key("download_progress", |state: &ProgressState, writer: &mut dyn std::fmt::Write| {128					let Some(len) = state.len() else {129						return;130					};131					let pos = state.pos();132					if pos > len {133						let _ = write!(writer, "{}", pos.human_count_bare());134					} else {135						let _ = write!(writer, "{} / {}", pos.human_count_bare(), len.human_count_bare());136					}137				})138				.with_key(139					"color_start",140					|state: &ProgressState, writer: &mut dyn std::fmt::Write| {141						let elapsed = state.elapsed();142143						if elapsed > Duration::from_secs(60) {144							// Red145							let _ = write!(writer, "\x1b[{}m", 1 + 30);146						} else if elapsed > Duration::from_secs(30) {147							// Yellow148							let _ = write!(writer, "\x1b[{}m", 3 + 30);149						}150					},151				)152				.with_key(153					"color_end",154					|state: &ProgressState, writer: &mut dyn std::fmt::Write| {155						if state.elapsed() > Duration::from_secs(30) {156							let _ = write!(writer, "\x1b[0m");157						}158					},159				),160		)161	};162163	let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));164165	let reg = tracing_subscriber::registry().with({166		let sub = tracing_subscriber::fmt::layer()167			.without_time()168			.with_target(false);169		#[cfg(feature = "indicatif")]170		let sub = sub.with_writer(indicatif_layer.get_stderr_writer());171		sub.with_filter(filter) // .without,172	});173	// #[cfg(feature = "indicatif")]174	#[cfg(feature = "indicatif")]175	let reg = reg.with(indicatif_layer);176	reg.init();177}178179fn main() -> ExitCode {180	let opts = RootOpts::parse();181	if let Opts::Complete(c) = &opts.command {182		c.run(RootOpts::command());183		return ExitCode::SUCCESS;184	}185186	setup_logging();187188	init_libraries();189190	tokio::runtime::Builder::new_multi_thread()191		.enable_all()192		.on_thread_start(|| {193			gc_register_my_thread();194		})195		.on_thread_stop(|| {196			gc_unregister_my_thread();197		})198		.build()199		.expect("failed to build runtime")200		.block_on(async {201			if let Err(e) = main_real(opts).await {202				error!("{e:#}");203				ExitCode::FAILURE204			} else {205				ExitCode::SUCCESS206			}207		})208	// async_main(opts)209}210211async fn main_real(opts: RootOpts) -> Result<()> {212	let nix_args = std::env::var_os("NIX_ARGS")213		.map(|a| extra_args::parse_os(&a))214		.transpose()?215		.unwrap_or_default();216	let config = opts217		.fleet_opts218		.build(219			nix_args,220			matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),221		)222		.await?;223224	match run_command(&config, opts.fleet_opts, opts.command).await {225		Ok(()) => {226			config.save()?;227			Ok(())228		}229		Err(e) => {230			let _ = config.save();231			Err(e)232		}233	}234}235236#[cfg(test)]237mod tests {238	use super::*;239240	#[test]241	fn verify_command() {242		use clap::CommandFactory;243		RootOpts::command().debug_assert();244	}245}
after · cmds/fleet/src/main.rs
1#![recursion_limit = "512"]23pub(crate) mod cmds;4// pub(crate) mod command;5pub(crate) mod extra_args;67use std::{env, ffi::OsString, process::ExitCode};89use anyhow::{Result, bail};10use clap::{CommandFactory, Parser};11use cmds::{12	build_systems::{BuildSystems, Deploy},13	complete::Complete,14	info::Info,15	rollback::RollbackSingle,16	secrets::Secret,17	tf::Tf,18};19use fleet_base::{host::Config, opts::FleetOpts};20use futures::{TryStreamExt, future::LocalBoxFuture, stream::FuturesUnordered};21// use host::Config;22#[cfg(feature = "indicatif")]23use human_repr::HumanCount;24#[cfg(feature = "indicatif")]25use indicatif::{ProgressState, ProgressStyle};26use nix_eval::{gc_register_my_thread, gc_unregister_my_thread, init_libraries};27use tracing::{Instrument, error, info, info_span};28#[cfg(feature = "indicatif")]29use tracing_indicatif::IndicatifLayer;30use tracing_subscriber::{EnvFilter, fmt::format::Format, prelude::*};3132#[derive(Parser)]33struct Prefetch {}34impl Prefetch {35	async fn run(&self, config: &Config) -> Result<()> {36		let mut prefetch_dir = config.directory.to_path_buf();37		prefetch_dir.push("prefetch");38		if !prefetch_dir.is_dir() {39			info!("nothing to prefetch: no prefetch directory");40			return Ok(());41		}42		let tasks = <FuturesUnordered<LocalBoxFuture<Result<()>>>>::new();43		for entry in std::fs::read_dir(&prefetch_dir)? {44			tasks.push(Box::pin(async {45				let entry = entry?;46				if !entry.metadata()?.is_file() {47					bail!("only files should exist in prefetch directory");48				}49				let span = info_span!(50					"prefetching",51					name = entry.file_name().to_string_lossy().as_ref()52				);53				let mut path = OsString::new();54				path.push("file://");55				path.push(entry.path());5657				let mut status = config.local_host().cmd("nix").await?;58				status.args(&config.nix_args);59				status.arg("store").arg("prefetch-file").arg(path);60				status.run_nix_string().instrument(span).await?;61				Ok(())62			}));63		}64		tasks.try_collect::<Vec<()>>().await?;65		Ok(())66	}67}6869#[derive(Parser)]70enum Opts {71	/// Build system closures72	BuildSystems(BuildSystems),73	/// Upload and switch system closures74	Deploy(Deploy),75	/// Rollback remote machine by redeploying old generation as the new one76	RollbackSingle(RollbackSingle),77	/// Secret management78	#[clap(subcommand)]79	Secret(Secret),80	/// Upload prefetch directory to the nix store81	Prefetch(Prefetch),82	/// Config parsing83	Info(Info),84	/// Command completions85	#[clap(hide(true))]86	Complete(Complete),87	/// Compile and evaluate terranix configuration88	Tf(Tf),89}9091#[derive(Parser)]92#[clap(version, author)]93struct RootOpts {94	#[clap(flatten)]95	fleet_opts: FleetOpts,96	#[clap(subcommand)]97	command: Opts,98}99100async fn run_command(config: &Config, opts: FleetOpts, command: Opts) -> Result<()> {101	match command {102		Opts::BuildSystems(c) => c.run(config, &opts).await?,103		Opts::Deploy(d) => d.run(config, &opts).await?,104		Opts::RollbackSingle(r) => r.run(config, &opts).await?,105		Opts::Secret(s) => s.run(config, &opts).await?,106		Opts::Info(i) => i.run(config).await?,107		Opts::Prefetch(p) => p.run(config).await?,108		Opts::Tf(t) => t.run(config).await?,109		// TODO: actually parse commands before starting the async runtime110		Opts::Complete(c) => {111			tokio::task::spawn_blocking(move || c.run(RootOpts::command())).await?112		}113	};114	Ok(())115}116117fn setup_logging() {118	#[cfg(feature = "indicatif")]119	let indicatif_layer = {120		use std::time::Duration;121122		IndicatifLayer::new().with_progress_style(123			ProgressStyle::with_template(124				"{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{download_progress} {elapsed}{color_end}",125			)126				.unwrap()127				.with_key("download_progress", |state: &ProgressState, writer: &mut dyn std::fmt::Write| {128					let Some(len) = state.len() else {129						return;130					};131					let pos = state.pos();132					if pos > len {133						let _ = write!(writer, "{}", pos.human_count_bare());134					} else {135						let _ = write!(writer, "{} / {}", pos.human_count_bare(), len.human_count_bare());136					}137				})138				.with_key(139					"color_start",140					|state: &ProgressState, writer: &mut dyn std::fmt::Write| {141						let elapsed = state.elapsed();142143						if elapsed > Duration::from_secs(60) {144							// Red145							let _ = write!(writer, "\x1b[{}m", 1 + 30);146						} else if elapsed > Duration::from_secs(30) {147							// Yellow148							let _ = write!(writer, "\x1b[{}m", 3 + 30);149						}150					},151				)152				.with_key(153					"color_end",154					|state: &ProgressState, writer: &mut dyn std::fmt::Write| {155						if state.elapsed() > Duration::from_secs(30) {156							let _ = write!(writer, "\x1b[0m");157						}158					},159				),160		)161	};162163	let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));164165	let reg = tracing_subscriber::registry().with({166		let sub = tracing_subscriber::fmt::layer()167			.without_time()168			.with_target(false);169		#[cfg(feature = "indicatif")]170		let sub = sub.with_writer(indicatif_layer.get_stderr_writer());171		sub.with_filter(filter) // .without,172	});173174	if env::var_os("FLEET_OTEL").is_some() {}175176	// #[cfg(feature = "indicatif")]177	#[cfg(feature = "indicatif")]178	let reg = reg.with(indicatif_layer);179	reg.init();180}181182fn main() -> ExitCode {183	let opts = RootOpts::parse();184	if let Opts::Complete(c) = &opts.command {185		c.run(RootOpts::command());186		return ExitCode::SUCCESS;187	}188189	setup_logging();190191	init_libraries();192193	tokio::runtime::Builder::new_multi_thread()194		.enable_all()195		.on_thread_start(|| {196			gc_register_my_thread();197		})198		.on_thread_stop(|| {199			gc_unregister_my_thread();200		})201		.build()202		.expect("failed to build runtime")203		.block_on(async {204			if let Err(e) = main_real(opts).await {205				error!("{e:#}");206				ExitCode::FAILURE207			} else {208				ExitCode::SUCCESS209			}210		})211	// async_main(opts)212}213214async fn main_real(opts: RootOpts) -> Result<()> {215	let nix_args = std::env::var_os("NIX_ARGS")216		.map(|a| extra_args::parse_os(&a))217		.transpose()?218		.unwrap_or_default();219	let config = opts220		.fleet_opts221		.build(222			nix_args,223			matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),224		)225		.await?;226227	match run_command(&config, opts.fleet_opts, opts.command).await {228		Ok(()) => {229			config.save()?;230			Ok(())231		}232		Err(e) => {233			let _ = config.save();234			Err(e)235		}236	}237}238239#[cfg(test)]240mod tests {241	use super::*;242243	#[test]244	fn verify_command() {245		use clap::CommandFactory;246		RootOpts::command().debug_assert();247	}248}
addedcrates/opentelemetry-exporter-env/Cargo.tomldiffbeforeafterboth
--- /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"]
addedcrates/opentelemetry-exporter-env/src/lib.rsdiffbeforeafterboth
--- /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<T>(env: &'static str) -> Result<Option<T>, Error>
+where
+	T: FromStr,
+	Error: From<(&'static str, <T as FromStr>::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<Self, Error> {
+				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<Self, Self::Err> {
+				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<Compression> 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<OtlpProtocol> 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<T, E = ProviderError> = Result<T, E>;
addedcrates/opentelemetry-exporter-env/src/otlp.rsdiffbeforeafterboth
--- /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<std::str::Split<'a, char>, 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<String, String> {
+	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<LogExporter> {
+	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<MetricExporter> {
+	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<SpanExporter> {
+	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()?)
+		}
+	}
+}