1use std::convert::Infallible;2use std::env::{self, VarError};3use std::ffi::OsString;4use std::num::ParseIntError;5use std::str::FromStr;6use std::time::Duration;78#[cfg(feature = "otlp")]9mod otlp;1011#[derive(thiserror::Error, Debug)]12pub enum Error {13 #[error("environment variable {env} contains invalid UTF-8: {value:?}")]14 InvalidUtf8 {15 env: &'static str,16 value: OsString,17 },18 #[error("environment variable {env}={value:?}: {error}")]19 EnvParse {20 env: &'static str,21 value: String,22 error: &'static str,23 },24 #[error("environment variable {env}={value:?}: {error}")]25 EnvParseInt {26 env: &'static str,27 value: String,28 error: ParseIntError,29 },30 #[cfg(feature = "otlp")]31 #[error("failed to build exporter: {0}")]32 Exporter(#[from] opentelemetry_otlp::ExporterBuildError),33}3435impl From<(&'static str, &'static str, String)> for Error {36 fn from((env, error, value): (&'static str, &'static str, String)) -> Self {37 Self::EnvParse { env, value, error }38 }39}40impl From<(&'static str, ParseIntError, String)> for Error {41 fn from((env, error, value): (&'static str, ParseIntError, String)) -> Self {42 Self::EnvParseInt { env, value, error }43 }44}45impl From<(&'static str, Infallible, String)> for Error {46 fn from(_v: (&'static str, Infallible, String)) -> Self {47 unreachable!()48 }49}5051fn load_env<T>(env: &'static str) -> Result<Option<T>, Error>52where53 T: FromStr,54 Error: From<(&'static str, <T as FromStr>::Err, String)>,55{56 match env::var(env) {57 Ok(v) => Ok(Some(T::from_str(&v).map_err(|err| (env, err, v))?)),58 Err(VarError::NotPresent) => Ok(None),59 Err(VarError::NotUnicode(value)) => Err(Error::InvalidUtf8 { env, value }),60 }61}6263macro_rules! impl_enum {64 (enum $id:ident {65 $(66 #[name = $value:literal]67 $var:ident,68 )*69 }) => {70 #[derive(Clone, Copy)]71 #[cfg_attr(feature = "clap", derive(clap::ValueEnum))]72 pub enum $id {73 $(74 #[cfg_attr(feature = "clap", value(name = $value))]75 $var,76 )*77 }78 impl FromStr for $id {79 type Err = &'static str;8081 fn from_str(s: &str) -> Result<Self, Self::Err> {82 Ok(match s {83 $(84 $value => Self::$var,85 )*86 _ => return Err("unsupported value")87 })88 }89 }90 };91}9293impl_enum! {94 enum ExporterKind {95 #[name = "otlp"]96 Otlp,97 #[name = "none"]98 None,99 }100}101102#[derive(Default)]103#[cfg_attr(feature = "clap", derive(clap::Parser))]104pub struct SignalExporterSettings {105 106 #[cfg_attr(feature = "clap", arg(long = "otel-traces-exporter", env = "OTEL_TRACES_EXPORTER", value_enum))]107 pub traces: Option<ExporterKind>,108 109 #[cfg_attr(feature = "clap", arg(long = "otel-metrics-exporter", env = "OTEL_METRICS_EXPORTER", value_enum))]110 pub metrics: Option<ExporterKind>,111 112 #[cfg_attr(feature = "clap", arg(long = "otel-logs-exporter", env = "OTEL_LOGS_EXPORTER", value_enum))]113 pub logs: Option<ExporterKind>,114}115116impl SignalExporterSettings {117 pub fn from_env() -> Result<Self, Error> {118 Ok(Self {119 traces: load_env("OTEL_TRACES_EXPORTER")?,120 metrics: load_env("OTEL_METRICS_EXPORTER")?,121 logs: load_env("OTEL_LOGS_EXPORTER")?,122 })123 }124125 pub fn traces_enabled(&self) -> bool {126 !matches!(self.traces, Some(ExporterKind::None))127 }128 pub fn metrics_enabled(&self) -> bool {129 !matches!(self.metrics, Some(ExporterKind::None))130 }131 pub fn logs_enabled(&self) -> bool {132 !matches!(self.logs, Some(ExporterKind::None))133 }134}135136impl_enum! {137 enum Compression {138 #[name = "gzip"]139 Gzip,140 #[name = "zstd"]141 Zstd,142 }143}144#[cfg(feature = "otlp")]145impl From<Compression> for opentelemetry_otlp::Compression {146 fn from(value: Compression) -> Self {147 match value {148 Compression::Gzip => opentelemetry_otlp::Compression::Gzip,149 Compression::Zstd => opentelemetry_otlp::Compression::Zstd,150 }151 }152}153154impl_enum! {155 enum OtlpProtocol {156 #[name = "grpc"]157 Grpc,158 #[name = "http/protobuf"]159 HttpProtobuf,160 #[name = "http/json"]161 HttpJson,162 }163}164#[cfg(feature = "otlp")]165impl From<OtlpProtocol> for opentelemetry_otlp::Protocol {166 fn from(value: OtlpProtocol) -> Self {167 match value {168 OtlpProtocol::Grpc => opentelemetry_otlp::Protocol::Grpc,169 OtlpProtocol::HttpProtobuf => opentelemetry_otlp::Protocol::HttpBinary,170 OtlpProtocol::HttpJson => opentelemetry_otlp::Protocol::HttpJson,171 }172 }173}174175pub trait OtlpSignalSettings {176 fn compression(&self) -> Option<Compression>;177 fn endpoint(&self) -> Option<&str>;178 fn headers(&self) -> Option<&str>;179 fn protocol(&self) -> Option<OtlpProtocol>;180 fn timeout(&self) -> Option<u64>;181}182183macro_rules! impl_settings {184 (185 #[name($env_prefix:literal, $long_prefix:literal)]186 struct $id:ident {187 $(188 $(#[doc = $doc:literal])*189 #[name($env:literal, $long:literal)]190 $(#[arg($($tt:tt)*)])?191 $name:ident: $ty:ty,192 )*193 }) => {194 #[derive(Default)]195 #[cfg_attr(feature = "clap", derive(clap::Parser))]196 pub struct $id {197 $(198 $(#[doc = $doc])*199 #[cfg_attr(feature = "clap", arg(200 long = concat!("otel-exporter-otlp-", $long_prefix, $long),201 id = concat!("otel-exporter-otlp-", $long_prefix, $long),202 env = concat!("OTEL_EXPORTER_OTLP_", $env_prefix, $env)203 $(, $($tt)*)?)204 )]205 pub $name: Option<$ty>,206 )*207 }208 impl $id {209 pub fn from_env() -> Result<Self, Error> {210 Ok(Self {211 $(212 $name: load_env(concat!("OTEL_EXPORTER_OTLP_", $env_prefix, $env))?,213 )*214 })215 }216 }217 impl OtlpSignalSettings for $id {218 fn compression(&self) -> Option<Compression> { self.compression }219 fn endpoint(&self) -> Option<&str> { self.endpoint.as_deref() }220 fn headers(&self) -> Option<&str> { self.headers.as_deref() }221 fn protocol(&self) -> Option<OtlpProtocol> { self.protocol }222 fn timeout(&self) -> Option<u64> { self.timeout }223 }224 }225}226227impl_settings! {228 #[name("", "")]229 struct OtlpBaseSettings {230 231 #[name("COMPRESSION", "compression")]232 #[arg(value_enum)]233 compression: Compression,234 235 #[name("ENDPOINT", "endpoint")]236 endpoint: String,237 238 #[name("HEADERS", "headers")]239 headers: String,240 241 #[name("PROTOCOL", "protocol")]242 #[arg(value_enum)]243 protocol: OtlpProtocol,244 245 #[name("TIMEOUT", "timeout")]246 timeout: u64,247 }248}249impl_settings! {250 #[name("LOGS_", "logs-")]251 struct OtlpLogsSettings {252 253 #[name("COMPRESSION", "compression")]254 #[arg(value_enum)]255 compression: Compression,256 257 #[name("ENDPOINT", "endpoint")]258 endpoint: String,259 260 #[name("HEADERS", "headers")]261 headers: String,262 263 #[name("PROTOCOL", "protocol")]264 #[arg(value_enum)]265 protocol: OtlpProtocol,266 267 #[name("TIMEOUT", "timeout")]268 timeout: u64,269 }270}271impl_settings! {272 #[name("METRICS_", "metrics-")]273 struct OtlpMetricsSettings {274 275 #[name("COMPRESSION", "compression")]276 #[arg(value_enum)]277 compression: Compression,278 279 #[name("ENDPOINT", "endpoint")]280 endpoint: String,281 282 #[name("HEADERS", "headers")]283 headers: String,284 285 #[name("PROTOCOL", "protocol")]286 #[arg(value_enum)]287 protocol: OtlpProtocol,288 289 #[name("TIMEOUT", "timeout")]290 timeout: u64,291 }292}293impl_settings! {294 #[name("TRACES_", "traces-")]295 struct OtlpTracesSettings {296 297 #[name("COMPRESSION", "compression")]298 #[arg(value_enum)]299 compression: Compression,300 301 #[name("ENDPOINT", "endpoint")]302 endpoint: String,303 304 #[name("HEADERS", "headers")]305 headers: String,306 307 #[name("PROTOCOL", "protocol")]308 #[arg(value_enum)]309 protocol: OtlpProtocol,310 311 #[name("TIMEOUT", "timeout")]312 timeout: u64,313 }314}315316pub struct ResolvedOtlpSettings {317 pub compression: Option<Compression>,318 pub endpoint: String,319 pub headers: Option<String>,320 pub protocol: OtlpProtocol,321 pub timeout: Duration,322}323324impl ResolvedOtlpSettings {325 const DEFAULT_TIMEOUT_MS: u64 = 10000;326 const DEFAULT_GRPC_ENDPOINT: &str = "http://localhost:4317";327 const DEFAULT_HTTP_ENDPOINT: &str = "http://localhost:4318";328329 pub fn traces(330 base: &impl OtlpSignalSettings,331 signal: &impl OtlpSignalSettings,332 ) -> Result<Self, Error> {333 Self::resolve(base, signal, "/v1/traces")334 }335336 pub fn metrics(337 base: &impl OtlpSignalSettings,338 signal: &impl OtlpSignalSettings,339 ) -> Result<Self, Error> {340 Self::resolve(base, signal, "/v1/metrics")341 }342343 pub fn logs(344 base: &impl OtlpSignalSettings,345 signal: &impl OtlpSignalSettings,346 ) -> Result<Self, Error> {347 Self::resolve(base, signal, "/v1/logs")348 }349350 fn resolve(351 base: &impl OtlpSignalSettings,352 signal: &impl OtlpSignalSettings,353 signal_path: &str,354 ) -> Result<Self, Error> {355 let protocol = signal356 .protocol()357 .or_else(|| base.protocol())358 .unwrap_or(OtlpProtocol::HttpProtobuf);359360 let endpoint = if let Some(ep) = signal.endpoint() {361 ep.to_owned()362 } else if let Some(ep) = base.endpoint() {363 match protocol {364 OtlpProtocol::Grpc => ep.to_owned(),365 _ => format!("{ep}{signal_path}"),366 }367 } else {368 match protocol {369 OtlpProtocol::Grpc => Self::DEFAULT_GRPC_ENDPOINT.to_owned(),370 _ => format!("{}{signal_path}", Self::DEFAULT_HTTP_ENDPOINT),371 }372 };373374 Ok(Self {375 compression: signal.compression().or_else(|| base.compression()),376 endpoint,377 headers: signal378 .headers()379 .or_else(|| base.headers())380 .map(str::to_owned),381 protocol,382 timeout: Duration::from_millis(383 signal384 .timeout()385 .or_else(|| base.timeout())386 .unwrap_or(Self::DEFAULT_TIMEOUT_MS),387 ),388 })389 }390}
1use std::convert::Infallible;2use std::env::{self, VarError};3use std::ffi::OsString;4use std::num::ParseIntError;5use std::str::FromStr;6use std::time::Duration;78#[cfg(feature = "otlp")]9mod otlp;1011#[derive(thiserror::Error, Debug)]12pub enum Error {13 #[error("environment variable {env} contains invalid UTF-8: {value:?}")]14 InvalidUtf8 { env: &'static str, value: OsString },15 #[error("environment variable {env}={value:?}: {error}")]16 EnvParse {17 env: &'static str,18 value: String,19 error: &'static str,20 },21 #[error("environment variable {env}={value:?}: {error}")]22 EnvParseInt {23 env: &'static str,24 value: String,25 error: ParseIntError,26 },27 #[cfg(feature = "otlp")]28 #[error("failed to build exporter: {0}")]29 Exporter(#[from] opentelemetry_otlp::ExporterBuildError),30}3132impl From<(&'static str, &'static str, String)> for Error {33 fn from((env, error, value): (&'static str, &'static str, String)) -> Self {34 Self::EnvParse { env, value, error }35 }36}37impl From<(&'static str, ParseIntError, String)> for Error {38 fn from((env, error, value): (&'static str, ParseIntError, String)) -> Self {39 Self::EnvParseInt { env, value, error }40 }41}42impl From<(&'static str, Infallible, String)> for Error {43 fn from(_v: (&'static str, Infallible, String)) -> Self {44 unreachable!()45 }46}4748fn load_env<T>(env: &'static str) -> Result<Option<T>, Error>49where50 T: FromStr,51 Error: From<(&'static str, <T as FromStr>::Err, String)>,52{53 match env::var(env) {54 Ok(v) => Ok(Some(T::from_str(&v).map_err(|err| (env, err, v))?)),55 Err(VarError::NotPresent) => Ok(None),56 Err(VarError::NotUnicode(value)) => Err(Error::InvalidUtf8 { env, value }),57 }58}5960macro_rules! impl_enum {61 (enum $id:ident {62 $(63 #[name = $value:literal]64 $var:ident,65 )*66 }) => {67 #[derive(Clone, Copy)]68 #[cfg_attr(feature = "clap", derive(clap::ValueEnum))]69 pub enum $id {70 $(71 #[cfg_attr(feature = "clap", value(name = $value))]72 $var,73 )*74 }75 impl FromStr for $id {76 type Err = &'static str;7778 fn from_str(s: &str) -> Result<Self, Self::Err> {79 Ok(match s {80 $(81 $value => Self::$var,82 )*83 _ => return Err("unsupported value")84 })85 }86 }87 };88}8990impl_enum! {91 enum ExporterKind {92 #[name = "otlp"]93 Otlp,94 #[name = "none"]95 None,96 }97}9899#[derive(Default)]100#[cfg_attr(feature = "clap", derive(clap::Parser))]101pub struct SignalExporterSettings {102 103 #[cfg_attr(104 feature = "clap",105 arg(106 long = "otel-traces-exporter",107 env = "OTEL_TRACES_EXPORTER",108 value_enum109 )110 )]111 pub traces: Option<ExporterKind>,112 113 #[cfg_attr(114 feature = "clap",115 arg(116 long = "otel-metrics-exporter",117 env = "OTEL_METRICS_EXPORTER",118 value_enum119 )120 )]121 pub metrics: Option<ExporterKind>,122 123 #[cfg_attr(124 feature = "clap",125 arg(long = "otel-logs-exporter", env = "OTEL_LOGS_EXPORTER", value_enum)126 )]127 pub logs: Option<ExporterKind>,128}129130impl SignalExporterSettings {131 pub fn from_env() -> Result<Self, Error> {132 Ok(Self {133 traces: load_env("OTEL_TRACES_EXPORTER")?,134 metrics: load_env("OTEL_METRICS_EXPORTER")?,135 logs: load_env("OTEL_LOGS_EXPORTER")?,136 })137 }138139 pub fn traces_enabled(&self) -> bool {140 !matches!(self.traces, Some(ExporterKind::None))141 }142 pub fn metrics_enabled(&self) -> bool {143 !matches!(self.metrics, Some(ExporterKind::None))144 }145 pub fn logs_enabled(&self) -> bool {146 !matches!(self.logs, Some(ExporterKind::None))147 }148}149150impl_enum! {151 enum Compression {152 #[name = "gzip"]153 Gzip,154 #[name = "zstd"]155 Zstd,156 }157}158#[cfg(feature = "otlp")]159impl From<Compression> for opentelemetry_otlp::Compression {160 fn from(value: Compression) -> Self {161 match value {162 Compression::Gzip => opentelemetry_otlp::Compression::Gzip,163 Compression::Zstd => opentelemetry_otlp::Compression::Zstd,164 }165 }166}167168impl_enum! {169 enum OtlpProtocol {170 #[name = "grpc"]171 Grpc,172 #[name = "http/protobuf"]173 HttpProtobuf,174 #[name = "http/json"]175 HttpJson,176 }177}178#[cfg(feature = "otlp")]179impl From<OtlpProtocol> for opentelemetry_otlp::Protocol {180 fn from(value: OtlpProtocol) -> Self {181 match value {182 OtlpProtocol::Grpc => opentelemetry_otlp::Protocol::Grpc,183 OtlpProtocol::HttpProtobuf => opentelemetry_otlp::Protocol::HttpBinary,184 OtlpProtocol::HttpJson => opentelemetry_otlp::Protocol::HttpJson,185 }186 }187}188189pub trait OtlpSignalSettings {190 fn compression(&self) -> Option<Compression>;191 fn endpoint(&self) -> Option<&str>;192 fn headers(&self) -> Option<&str>;193 fn protocol(&self) -> Option<OtlpProtocol>;194 fn timeout(&self) -> Option<u64>;195}196197macro_rules! impl_settings {198 (199 #[name($env_prefix:literal, $long_prefix:literal)]200 struct $id:ident {201 $(202 $(#[doc = $doc:literal])*203 #[name($env:literal, $long:literal)]204 $(#[arg($($tt:tt)*)])?205 $name:ident: $ty:ty,206 )*207 }) => {208 #[derive(Default)]209 #[cfg_attr(feature = "clap", derive(clap::Parser))]210 pub struct $id {211 $(212 $(#[doc = $doc])*213 #[cfg_attr(feature = "clap", arg(214 long = concat!("otel-exporter-otlp-", $long_prefix, $long),215 id = concat!("otel-exporter-otlp-", $long_prefix, $long),216 env = concat!("OTEL_EXPORTER_OTLP_", $env_prefix, $env)217 $(, $($tt)*)?)218 )]219 pub $name: Option<$ty>,220 )*221 }222 impl $id {223 pub fn from_env() -> Result<Self, Error> {224 Ok(Self {225 $(226 $name: load_env(concat!("OTEL_EXPORTER_OTLP_", $env_prefix, $env))?,227 )*228 })229 }230 }231 impl OtlpSignalSettings for $id {232 fn compression(&self) -> Option<Compression> { self.compression }233 fn endpoint(&self) -> Option<&str> { self.endpoint.as_deref() }234 fn headers(&self) -> Option<&str> { self.headers.as_deref() }235 fn protocol(&self) -> Option<OtlpProtocol> { self.protocol }236 fn timeout(&self) -> Option<u64> { self.timeout }237 }238 }239}240241impl_settings! {242 #[name("", "")]243 struct OtlpBaseSettings {244 245 #[name("COMPRESSION", "compression")]246 #[arg(value_enum)]247 compression: Compression,248 249 #[name("ENDPOINT", "endpoint")]250 endpoint: String,251 252 #[name("HEADERS", "headers")]253 headers: String,254 255 #[name("PROTOCOL", "protocol")]256 #[arg(value_enum)]257 protocol: OtlpProtocol,258 259 #[name("TIMEOUT", "timeout")]260 timeout: u64,261 }262}263impl_settings! {264 #[name("LOGS_", "logs-")]265 struct OtlpLogsSettings {266 267 #[name("COMPRESSION", "compression")]268 #[arg(value_enum)]269 compression: Compression,270 271 #[name("ENDPOINT", "endpoint")]272 endpoint: String,273 274 #[name("HEADERS", "headers")]275 headers: String,276 277 #[name("PROTOCOL", "protocol")]278 #[arg(value_enum)]279 protocol: OtlpProtocol,280 281 #[name("TIMEOUT", "timeout")]282 timeout: u64,283 }284}285impl_settings! {286 #[name("METRICS_", "metrics-")]287 struct OtlpMetricsSettings {288 289 #[name("COMPRESSION", "compression")]290 #[arg(value_enum)]291 compression: Compression,292 293 #[name("ENDPOINT", "endpoint")]294 endpoint: String,295 296 #[name("HEADERS", "headers")]297 headers: String,298 299 #[name("PROTOCOL", "protocol")]300 #[arg(value_enum)]301 protocol: OtlpProtocol,302 303 #[name("TIMEOUT", "timeout")]304 timeout: u64,305 }306}307impl_settings! {308 #[name("TRACES_", "traces-")]309 struct OtlpTracesSettings {310 311 #[name("COMPRESSION", "compression")]312 #[arg(value_enum)]313 compression: Compression,314 315 #[name("ENDPOINT", "endpoint")]316 endpoint: String,317 318 #[name("HEADERS", "headers")]319 headers: String,320 321 #[name("PROTOCOL", "protocol")]322 #[arg(value_enum)]323 protocol: OtlpProtocol,324 325 #[name("TIMEOUT", "timeout")]326 timeout: u64,327 }328}329330pub struct ResolvedOtlpSettings {331 pub compression: Option<Compression>,332 pub endpoint: String,333 pub headers: Option<String>,334 pub protocol: OtlpProtocol,335 pub timeout: Duration,336}337338impl ResolvedOtlpSettings {339 const DEFAULT_TIMEOUT_MS: u64 = 10000;340 const DEFAULT_GRPC_ENDPOINT: &str = "http://localhost:4317";341 const DEFAULT_HTTP_ENDPOINT: &str = "http://localhost:4318";342343 pub fn traces(344 base: &impl OtlpSignalSettings,345 signal: &impl OtlpSignalSettings,346 ) -> Result<Self, Error> {347 Self::resolve(base, signal, "/v1/traces")348 }349350 pub fn metrics(351 base: &impl OtlpSignalSettings,352 signal: &impl OtlpSignalSettings,353 ) -> Result<Self, Error> {354 Self::resolve(base, signal, "/v1/metrics")355 }356357 pub fn logs(358 base: &impl OtlpSignalSettings,359 signal: &impl OtlpSignalSettings,360 ) -> Result<Self, Error> {361 Self::resolve(base, signal, "/v1/logs")362 }363364 fn resolve(365 base: &impl OtlpSignalSettings,366 signal: &impl OtlpSignalSettings,367 signal_path: &str,368 ) -> Result<Self, Error> {369 let protocol = signal370 .protocol()371 .or_else(|| base.protocol())372 .unwrap_or(OtlpProtocol::HttpProtobuf);373374 let endpoint = if let Some(ep) = signal.endpoint() {375 ep.to_owned()376 } else if let Some(ep) = base.endpoint() {377 match protocol {378 OtlpProtocol::Grpc => ep.to_owned(),379 _ => format!("{ep}{signal_path}"),380 }381 } else {382 match protocol {383 OtlpProtocol::Grpc => Self::DEFAULT_GRPC_ENDPOINT.to_owned(),384 _ => format!("{}{signal_path}", Self::DEFAULT_HTTP_ENDPOINT),385 }386 };387388 Ok(Self {389 compression: signal.compression().or_else(|| base.compression()),390 endpoint,391 headers: signal392 .headers()393 .or_else(|| base.headers())394 .map(str::to_owned),395 protocol,396 timeout: Duration::from_millis(397 signal398 .timeout()399 .or_else(|| base.timeout())400 .unwrap_or(Self::DEFAULT_TIMEOUT_MS),401 ),402 })403 }404}