aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--Cargo.lock187+182 −5
-rw-r--r--Cargo.toml34+31 −3
-rw-r--r--pkg/zorian.toml30+30 −0
-rw-r--r--src/config.rs116+112 −4
-rw-r--r--src/main.rs25+8 −17
-rw-r--r--src/telemetry.rs218+218 −0
6 files changed, 581 insertions, 29 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9499881..c3f41c1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -437,7 +437,7 @@ dependencies = [
"parking_lot",
"rayon",
"regex",
- "reqwest",
+ "reqwest 0.13.1",
"ring",
"rustls",
"rustsec",
@@ -457,9 +457,9 @@ dependencies = [
[[package]]
name = "cargo-llvm-cov"
-version = "0.6.23"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a19af1cb5512737613b5caa313dd14cd56459732867c53a755da4b967ffefe6"
+checksum = "f8132c5e1b6489a345e12bbc3deb7154cb8d8b51bd9d9b168505eff5e5ce88ee"
dependencies = [
"anyhow",
"camino",
@@ -1120,6 +1120,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1145,6 +1156,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-io",
+ "futures-macro",
"futures-sink",
"futures-task",
"memchr",
@@ -1857,7 +1869,7 @@ dependencies = [
"gix-quote",
"gix-sec",
"gix-url",
- "reqwest",
+ "reqwest 0.13.1",
"thiserror 2.0.17",
]
@@ -2959,6 +2971,100 @@ dependencies = [
]
[[package]]
+name = "opentelemetry"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "js-sys",
+ "pin-project-lite",
+ "thiserror 2.0.17",
+ "tracing",
+]
+
+[[package]]
+name = "opentelemetry-appender-tracing"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2"
+dependencies = [
+ "opentelemetry",
+ "tracing",
+ "tracing-core",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "opentelemetry-http"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "http",
+ "opentelemetry",
+ "reqwest 0.12.28",
+]
+
+[[package]]
+name = "opentelemetry-otlp"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf"
+dependencies = [
+ "http",
+ "opentelemetry",
+ "opentelemetry-http",
+ "opentelemetry-proto",
+ "opentelemetry_sdk",
+ "prost",
+ "reqwest 0.12.28",
+ "thiserror 2.0.17",
+ "tokio",
+ "tonic",
+ "tracing",
+]
+
+[[package]]
+name = "opentelemetry-proto"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"
+dependencies = [
+ "opentelemetry",
+ "opentelemetry_sdk",
+ "prost",
+ "tonic",
+ "tonic-prost",
+]
+
+[[package]]
+name = "opentelemetry-semantic-conventions"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846"
+
+[[package]]
+name = "opentelemetry_sdk"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd"
+dependencies = [
+ "futures-channel",
+ "futures-executor",
+ "futures-util",
+ "opentelemetry",
+ "percent-encoding",
+ "rand 0.9.2",
+ "thiserror 2.0.17",
+ "tokio",
+ "tokio-stream",
+]
+
+[[package]]
name = "os_pipe"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3359,6 +3465,40 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "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",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "reqwest"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
@@ -3461,6 +3601,7 @@ version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
dependencies = [
+ "log",
"once_cell",
"ring",
"rustls-pki-types",
@@ -4267,7 +4408,7 @@ dependencies = [
"libc",
"memchr",
"rayon",
- "reqwest",
+ "reqwest 0.13.1",
"rustc-stable-hash",
"semver",
"serde",
@@ -4570,9 +4711,11 @@ dependencies = [
"hyper-util",
"percent-encoding",
"pin-project",
+ "rustls-native-certs",
"socket2",
"sync_wrapper",
"tokio",
+ "tokio-rustls",
"tokio-stream",
"tower",
"tower-layer",
@@ -4581,6 +4724,17 @@ dependencies = [
]
[[package]]
+name = "tonic-prost"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6c55a2d6a14174563de34409c9f92ff981d006f56da9c6ecd40d9d4a31500b0"
+dependencies = [
+ "bytes",
+ "prost",
+ "tonic",
+]
+
+[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4705,6 +4859,22 @@ dependencies = [
]
[[package]]
+name = "tracing-opentelemetry"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc"
+dependencies = [
+ "js-sys",
+ "opentelemetry",
+ "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"
@@ -5570,6 +5740,11 @@ dependencies = [
"hyper-tls",
"hyper-util",
"minijinja",
+ "opentelemetry",
+ "opentelemetry-appender-tracing",
+ "opentelemetry-otlp",
+ "opentelemetry-semantic-conventions",
+ "opentelemetry_sdk",
"rustls",
"sd-notify",
"semver",
@@ -5579,10 +5754,12 @@ dependencies = [
"thiserror 2.0.17",
"tokio",
"toml",
+ "tonic",
"tower",
"tower-http",
"tower_governor",
"tracing",
+ "tracing-opentelemetry",
"tracing-subscriber",
"url",
"uuid",
diff --git a/Cargo.toml b/Cargo.toml
index 762b60d..3f2528a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,8 +10,14 @@ repository = "https://github.com/mrdimidium/Zorian"
[dependencies]
axum = { version = "0.8", features = ["http2", "macros", "multipart"] }
-axum-server = { version = "0.8", default-features = false, features = ["tls-rustls-no-provider"] }
-rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
+axum-server = { version = "0.8", default-features = false, features = [
+ "tls-rustls-no-provider",
+] }
+rustls = { version = "0.23", default-features = false, features = [
+ "ring",
+ "std",
+ "tls12",
+] }
axum-extra = { version = "0.12", features = [
"async-read-body",
"attachment",
@@ -68,8 +74,30 @@ uuid = { version = "1.19", features = ["v4", "v5"] }
[target.'cfg(target_os = "linux")'.dependencies]
sd-notify = { version = "0.4" }
+# OpenTelemetry
+opentelemetry = "0.31"
+opentelemetry-appender-tracing = "0.31"
+opentelemetry-otlp = { version = "0.31", features = [
+ "grpc-tonic",
+ "http-proto",
+ "logs",
+ "metrics",
+ "trace",
+ "tls",
+ "tls-roots",
+] }
+tonic = "0.14"
+opentelemetry-semantic-conventions = "0.31"
+opentelemetry_sdk = { version = "0.31", features = [
+ "rt-tokio",
+ "logs",
+ "metrics",
+ "trace",
+] }
+tracing-opentelemetry = "0.32"
+
[dev-dependencies]
cargo-deny = "0.19"
-cargo-llvm-cov = "0.6"
+cargo-llvm-cov = "0.8.2"
tempfile = "3.24"
url = "2.5"
diff --git a/pkg/zorian.toml b/pkg/zorian.toml
index 9612063..0fde186 100644
--- a/pkg/zorian.toml
+++ b/pkg/zorian.toml
@@ -27,3 +27,33 @@ hostnames = ["localhost", "127.0.0.1", "::1"]
# For HTTPS, set tls_cert and tls_key to PEM file paths.
# tls_crt = "/etc/zorian/cert.pem"
# tls_key = "/etc/zorian/key.pem"
+
+# Telemetry configuration
+# Logs to stdout (pretty or json format)
+[telemetry.stdout]
+enabled = true
+log_level = "info" # trace, debug, info, warning, error
+log_format = "pretty" # pretty, json
+
+# OpenTelemetry Collector (OTLP)
+# Uncomment to enable sending logs and traces to an OTLP collector
+# [telemetry.otelcol]
+# enabled = true
+# logs = true
+# traces = true
+# endpoint = "http://localhost:4318" # HTTP protocol (no TLS)
+# # endpoint = "grpc://localhost:4317" # gRPC protocol (requires TLS)
+# timeout = 10 # seconds
+# log_level = "info"
+#
+# # For gRPC with TLS, specify certificates:
+# # tls_ca = "/etc/zorian/otel-ca.crt"
+# # tls_crt = "/etc/zorian/otel-client.crt" # for mTLS
+# # tls_key = "/etc/zorian/otel-client.key" # for mTLS
+#
+# # HTTP headers for authentication:
+# # [telemetry.otelcol.headers]
+# # Authorization = "Bearer <token>"
+
+# Environment variable ZORIAN_LOG overrides log_level settings.
+# Example: ZORIAN_LOG=debug,sqlx=warn
diff --git a/src/config.rs b/src/config.rs
index 49b5c15..1e684bb 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
// SPDX-License-Identifier: AGPL-3.0-or-later
+use std::collections::HashMap;
use std::fs;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
@@ -113,7 +114,6 @@ pub struct ListenerConfig {
pub addr: SocketAddr,
/// Hostnames to accept for this listener. Empty means accept all.
- #[serde(default)]
pub hostnames: Vec<String>,
/// Path to TLS certificate file (PEM format). If set, tls_key must also be set.
@@ -138,15 +138,118 @@ impl Default for ListenerConfig {
}
}
+#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(rename_all = "lowercase")]
+pub enum LogLevel {
+ Trace,
+ Debug,
+ Info,
+ Warning,
+ Error,
+}
+
+#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+pub enum StdoutFormat {
+ #[default]
+ Pretty,
+ Json,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(default)]
+pub struct StdoutConfig {
+ /// Enables sending logs to the stdout
+ pub enabled: bool,
+
+ /// Controls which logs will be sent to stdout
+ pub log_level: LogLevel,
+
+ /// Controls the format of logs in stdout
+ pub log_format: StdoutFormat,
+}
+impl Default for StdoutConfig {
+ fn default() -> Self {
+ Self {
+ enabled: true,
+ log_level: LogLevel::Info,
+ log_format: StdoutFormat::Pretty,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(default)]
+pub struct OtelcolConfig {
+ /// Enables sending telemetry to the otlp collector
+ pub enabled: bool,
+
+ /// Send logs to OTLP at this level (None = disabled)
+ pub logs: bool,
+
+ /// Send traces to OTLP
+ pub traces: bool,
+
+ /// Send traces to OTLP
+ pub metrics: bool,
+
+ /// OTLP endpoint (grpc:// or http://)
+ pub endpoint: String,
+
+ /// Export timeout in seconds
+ #[serde(deserialize_with = "deserialize_duration_secs")]
+ pub timeout: Duration,
+
+ /// Controls which logs will be sent to otlp
+ pub log_level: LogLevel,
+
+ /// Path to CA certificate for TLS (required for grpcs://)
+ pub tls_ca: Option<PathBuf>,
+
+ /// Path to client certificate for mTLS
+ pub tls_crt: Option<PathBuf>,
+
+ /// Path to client key for mTLS
+ pub tls_key: Option<PathBuf>,
+
+ /// HTTP headers for authentication
+ pub headers: HashMap<String, String>,
+}
+
+impl Default for OtelcolConfig {
+ fn default() -> Self {
+ Self {
+ enabled: true,
+ logs: true,
+ traces: true,
+ metrics: true,
+ timeout: Duration::from_secs(10),
+ endpoint: "http://localhost:4317".into(),
+ log_level: LogLevel::Info,
+ tls_ca: None,
+ tls_crt: None,
+ tls_key: None,
+ headers: HashMap::new(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Deserialize, Default)]
+#[serde(default)]
+pub struct TelemetryConfig {
+ pub stdout: StdoutConfig,
+ pub otelcol: Option<OtelcolConfig>,
+}
+
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ConfigService {
appname: String,
dirname: PathBuf,
- server: ServerConfig,
listen: Vec<ListenerConfig>,
+ server: ServerConfig,
+ telemetry: TelemetryConfig,
}
-
impl Default for ConfigService {
fn default() -> Self {
Self {
@@ -154,10 +257,10 @@ impl Default for ConfigService {
dirname: PathBuf::from("./.zorian-state"),
server: ServerConfig::default(),
listen: vec![ListenerConfig::default()],
+ telemetry: TelemetryConfig::default(),
}
}
}
-
impl ConfigService {
pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
let content = fs::read_to_string(path)?;
@@ -227,6 +330,10 @@ impl ConfigService {
pub fn listeners(&self) -> &[ListenerConfig] {
&self.listen
}
+
+ pub fn telemetry(&self) -> &TelemetryConfig {
+ &self.telemetry
+ }
}
#[cfg(test)]
@@ -242,6 +349,7 @@ impl ConfigService {
tls_crt: None,
tls_key: None,
}],
+ telemetry: TelemetryConfig::default(),
}
}
}
diff --git a/src/main.rs b/src/main.rs
index d6281ff..7dd6c1f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,7 @@ mod backends;
mod config;
mod proxy;
mod storage;
+mod telemetry;
mod web;
use std::future::Future;
@@ -25,7 +26,7 @@ use axum_server::tls_rustls::RustlsConfig;
use sd_notify::NotifyState;
use tokio::signal;
use tracing::{error, info, trace};
-use tracing_subscriber::{layer::SubscriberExt, registry::LookupSpan, util::SubscriberInitExt};
+use tracing_subscriber::registry::LookupSpan;
use crate::backends::zig::ZigController;
use crate::web::WebController;
@@ -92,21 +93,6 @@ async fn main() {
}
}
- tracing_subscriber::registry()
- .with({
- #[cfg(debug_assertions)]
- let fmt = tracing_subscriber::fmt::layer().pretty();
- #[cfg(not(debug_assertions))]
- let fmt = tracing_subscriber::fmt::layer().json();
- fmt
- })
- .with(match std::env::var_os("ZORIAN_LOG") {
- None => tracing_subscriber::EnvFilter::new("info,tower_http=info"),
- Some(val) => tracing_subscriber::EnvFilter::try_new(val.to_string_lossy())
- .expect("Invalid ZORIAN_LOG"),
- })
- .init();
-
let config = Arc::new(match config_path {
Some(path) => {
info!("use config file from {}", path.to_str().unwrap());
@@ -122,10 +108,13 @@ async fn main() {
}
});
config.validate().unwrap_or_else(|e| {
- error!("invalid config: {e}");
+ eprintln!("invalid config: {e}");
std::process::exit(1);
});
+ let mut telemetry =
+ telemetry::TelemetryService::init(config.telemetry(), config.appname(), VERSION);
+
let storage = Arc::new(storage::StorageService::new(config.clone()).await.unwrap());
let upstream = Arc::new(proxy::ProxyService::new());
@@ -408,6 +397,8 @@ async fn main() {
} else {
info!("shutdown complete");
}
+
+ telemetry.shutdown();
}
/// Layer that validates the Host header against configured hostnames.
diff --git a/src/telemetry.rs b/src/telemetry.rs
new file mode 100644
--- /dev/null
+++ b/src/telemetry.rs
@@ -0,0 +1,218 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use opentelemetry::KeyValue;
+use opentelemetry::trace::TracerProvider as _;
+use opentelemetry_otlp::{
+ LogExporter, Protocol, SpanExporter, WithExportConfig, WithHttpConfig, WithTonicConfig,
+};
+use opentelemetry_sdk::Resource;
+use opentelemetry_sdk::logs::SdkLoggerProvider;
+use opentelemetry_sdk::trace::SdkTracerProvider;
+use tonic::metadata::{MetadataKey, MetadataMap, MetadataValue};
+use tonic::transport::{Certificate, ClientTlsConfig, Identity};
+use tracing_subscriber::Layer;
+use tracing_subscriber::layer::SubscriberExt;
+use tracing_subscriber::util::SubscriberInitExt;
+
+use crate::config::{LogLevel, OtelcolConfig, StdoutFormat, TelemetryConfig};
+
+macro_rules! build_exporter {
+ ($Exporter:ident, $cfg:expr, $url:expr, $tls:expr, $path:expr) => {
+ if $cfg.endpoint.starts_with("http://") {
+ // with_endpoint() requires full path for HTTP protocol
+ $Exporter::builder()
+ .with_http()
+ .with_protocol(Protocol::HttpBinary)
+ .with_endpoint(format!("{}{}", $url, $path))
+ .with_timeout($cfg.timeout)
+ .with_headers($cfg.headers.clone())
+ .build()
+ .expect(concat!("failed to build ", stringify!($Exporter)))
+ } else if $cfg.endpoint.starts_with("grpc://") {
+ $Exporter::builder()
+ .with_tonic()
+ .with_endpoint($url)
+ .with_timeout($cfg.timeout)
+ .with_tls_config($tls)
+ .with_metadata(build_metadata($cfg))
+ .build()
+ .expect(concat!("failed to build ", stringify!($Exporter)))
+ } else {
+ panic!(
+ "invalid OTLP endpoint: {}. Expected http:// or grpc://",
+ $cfg.endpoint
+ )
+ }
+ };
+}
+
+fn build_metadata(cfg: &OtelcolConfig) -> MetadataMap {
+ let mut metadata = MetadataMap::new();
+ for (key, value) in &cfg.headers {
+ if let (Ok(k), Ok(v)) = (
+ key.parse::<MetadataKey<_>>(),
+ value.parse::<MetadataValue<_>>(),
+ ) {
+ metadata.insert(k, v);
+ }
+ }
+ metadata
+}
+
+pub struct TelemetryService {
+ logger_provider: Option<SdkLoggerProvider>,
+ tracer_provider: Option<SdkTracerProvider>,
+}
+impl Drop for TelemetryService {
+ fn drop(&mut self) {
+ self.shutdown();
+ }
+}
+impl TelemetryService {
+ pub fn init(config: &TelemetryConfig, service_name: &str, service_version: &str) -> Self {
+ let env_filter = match std::env::var_os("ZORIAN_LOG") {
+ Some(val) => tracing_subscriber::EnvFilter::try_new(val.to_string_lossy())
+ .expect("Invalid ZORIAN_LOG"),
+ None => tracing_subscriber::EnvFilter::new(Self::log_level_to_filter(
+ config.stdout.log_level,
+ )),
+ };
+
+ let mut tracer_provider = None;
+ let mut logger_provider = None;
+
+ let stdout = config
+ .stdout
+ .enabled
+ .then(|| match config.stdout.log_format {
+ StdoutFormat::Json => tracing_subscriber::fmt::layer().json().boxed(),
+ StdoutFormat::Pretty => tracing_subscriber::fmt::layer().pretty().boxed(),
+ });
+
+ let (otel_logs, otel_traces) = match &config.otelcol {
+ Some(cfg) if cfg.enabled => {
+ let tls = Self::build_tls(cfg);
+ let url = Self::parse_endpoint(&cfg.endpoint);
+ let resource = Resource::builder()
+ .with_service_name(service_name.to_string())
+ .with_attribute(KeyValue::new(
+ opentelemetry_semantic_conventions::attribute::SERVICE_VERSION,
+ service_version.to_string(),
+ ))
+ .build();
+
+ let logs = cfg.logs.then(|| {
+ let provider = SdkLoggerProvider::builder()
+ .with_resource(resource.clone())
+ .with_batch_exporter(build_exporter!(
+ LogExporter,
+ cfg,
+ url.clone(),
+ tls.clone(),
+ "/v1/logs"
+ ))
+ .build();
+ let layer =
+ opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(
+ &provider,
+ )
+ .with_filter(Self::to_level_filter(cfg.log_level));
+ logger_provider = Some(provider);
+ layer
+ });
+
+ let traces = cfg.traces.then(|| {
+ let provider = SdkTracerProvider::builder()
+ .with_resource(resource.clone())
+ .with_batch_exporter(build_exporter!(
+ SpanExporter,
+ cfg,
+ url.clone(),
+ tls.clone(),
+ "/v1/traces"
+ ))
+ .build();
+ let layer = tracing_opentelemetry::layer()
+ .with_tracer(provider.tracer(service_name.to_string()));
+ tracer_provider = Some(provider);
+ layer
+ });
+
+ (logs, traces)
+ }
+ _ => (None, None),
+ };
+
+ tracing_subscriber::registry()
+ .with(stdout)
+ .with(otel_logs)
+ .with(otel_traces)
+ .with(env_filter)
+ .init();
+
+ Self {
+ tracer_provider,
+ logger_provider,
+ }
+ }
+
+ fn parse_endpoint(endpoint: &str) -> String {
+ if endpoint.starts_with("http://") {
+ endpoint.to_string()
+ } else if let Some(rest) = endpoint.strip_prefix("grpc://") {
+ format!("https://{rest}")
+ } else {
+ panic!("invalid OTLP endpoint: {endpoint}. Expected http:// or grpc://")
+ }
+ }
+
+ fn build_tls(cfg: &OtelcolConfig) -> ClientTlsConfig {
+ let mut tls = ClientTlsConfig::new().with_native_roots();
+ if let Some(path) = &cfg.tls_ca {
+ tls = tls.ca_certificate(Certificate::from_pem(
+ std::fs::read_to_string(path).expect("failed to read CA"),
+ ));
+ }
+ if let (Some(crt), Some(key)) = (&cfg.tls_crt, &cfg.tls_key) {
+ tls = tls.identity(Identity::from_pem(
+ std::fs::read_to_string(crt).expect("failed to read cert"),
+ std::fs::read_to_string(key).expect("failed to read key"),
+ ));
+ }
+ tls
+ }
+
+ fn to_level_filter(level: LogLevel) -> tracing_subscriber::filter::LevelFilter {
+ match level {
+ LogLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE,
+ LogLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG,
+ LogLevel::Info => tracing_subscriber::filter::LevelFilter::INFO,
+ LogLevel::Warning => tracing_subscriber::filter::LevelFilter::WARN,
+ LogLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR,
+ }
+ }
+
+ fn log_level_to_filter(level: LogLevel) -> &'static str {
+ match level {
+ LogLevel::Trace => "trace",
+ LogLevel::Debug => "debug",
+ LogLevel::Info => "info",
+ LogLevel::Warning => "warn",
+ LogLevel::Error => "error",
+ }
+ }
+
+ pub fn shutdown(&mut self) {
+ if let Some(p) = self.tracer_provider.take()
+ && let Err(e) = p.shutdown()
+ {
+ tracing::error!("failed to shutdown tracer provider: {e}");
+ }
+ if let Some(p) = self.logger_provider.take()
+ && let Err(e) = p.shutdown()
+ {
+ tracing::error!("failed to shutdown logger provider: {e}");
+ }
+ }
+}