use std::{collections::BTreeMap, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum OtlpProtocol {
#[default]
Grpc,
HttpProtobuf,
HttpJson,
Stdout,
}
impl FromStr for OtlpProtocol {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
match s.to_ascii_lowercase().as_str() {
"grpc" => Ok(Self::Grpc),
"http/protobuf" | "http_protobuf" => Ok(Self::HttpProtobuf),
"http/json" | "http_json" => Ok(Self::HttpJson),
"stdout" | "debug" => Ok(Self::Stdout),
other => Err(format!("unknown OTLP protocol `{other}`")),
}
}
}
#[derive(Debug, Clone)]
pub struct OtlpEndpoint {
pub url: String,
pub protocol: OtlpProtocol,
pub headers: BTreeMap<String, String>,
pub compression: String,
pub timeout_ms: u64,
}
impl Default for OtlpEndpoint {
fn default() -> Self {
Self {
url: "http://localhost:4317".to_string(),
protocol: OtlpProtocol::default(),
headers: BTreeMap::new(),
compression: "gzip".to_string(),
timeout_ms: 10_000,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OtlpResourceAttrs {
pub service_name: String,
pub service_version: String,
pub service_namespace: String,
pub service_instance_id: String,
pub deployment_environment: String,
pub host_name: String,
pub host_arch: String,
pub extra: BTreeMap<String, String>,
}
impl From<&obs_core::ResourceAttrs> for OtlpResourceAttrs {
fn from(r: &obs_core::ResourceAttrs) -> Self {
Self {
service_name: r.service_name.clone(),
service_version: r.service_version.clone(),
service_namespace: r.service_namespace.clone(),
service_instance_id: r.service_instance_id.clone(),
deployment_environment: r.deployment_environment.clone(),
host_name: r.host_name.clone(),
host_arch: r.host_arch.clone(),
extra: r.extra.clone(),
}
}
}
impl OtlpResourceAttrs {
#[must_use]
pub fn to_semconv_map(&self) -> BTreeMap<String, String> {
let mut m = self.extra.clone();
if !self.service_name.is_empty() {
m.insert("service.name".to_string(), self.service_name.clone());
}
if !self.service_version.is_empty() {
m.insert("service.version".to_string(), self.service_version.clone());
}
if !self.service_namespace.is_empty() {
m.insert(
"service.namespace".to_string(),
self.service_namespace.clone(),
);
}
if !self.service_instance_id.is_empty() {
m.insert(
"service.instance.id".to_string(),
self.service_instance_id.clone(),
);
}
if !self.deployment_environment.is_empty() {
m.insert(
"deployment.environment".to_string(),
self.deployment_environment.clone(),
);
}
if !self.host_name.is_empty() {
m.insert("host.name".to_string(), self.host_name.clone());
}
if !self.host_arch.is_empty() {
m.insert("host.arch".to_string(), self.host_arch.clone());
}
m
}
}
#[must_use]
pub fn endpoint_from_env() -> OtlpEndpoint {
let mut e = OtlpEndpoint::default();
if let Ok(url) = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
&& !url.is_empty()
{
e.url = url;
}
if let Ok(proto) = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL")
&& let Ok(p) = OtlpProtocol::from_str(&proto)
{
e.protocol = p;
}
if let Ok(headers) = std::env::var("OTEL_EXPORTER_OTLP_HEADERS") {
for pair in headers.split(',') {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
if let Some((k, v)) = pair.split_once('=') {
e.headers.insert(k.trim().to_string(), v.trim().to_string());
}
}
}
if let Ok(c) = std::env::var("OTEL_EXPORTER_OTLP_COMPRESSION")
&& !c.is_empty()
{
e.compression = c;
}
if let Ok(t) = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT")
&& let Ok(ms) = t.parse::<u64>()
{
e.timeout_ms = ms;
}
e
}
#[must_use]
pub fn resource_from_env() -> OtlpResourceAttrs {
let mut r = OtlpResourceAttrs::default();
if let Ok(name) = std::env::var("OTEL_SERVICE_NAME") {
r.service_name = name;
}
if let Ok(extras) = std::env::var("OTEL_RESOURCE_ATTRIBUTES") {
for pair in extras.split(',') {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
if let Some((k, v)) = pair.split_once('=') {
let key = k.trim();
let val = v.trim().to_string();
match key {
"service.name" if r.service_name.is_empty() => r.service_name = val,
"service.version" if r.service_version.is_empty() => r.service_version = val,
"service.namespace" if r.service_namespace.is_empty() => {
r.service_namespace = val;
}
"service.instance.id" if r.service_instance_id.is_empty() => {
r.service_instance_id = val;
}
"deployment.environment" if r.deployment_environment.is_empty() => {
r.deployment_environment = val;
}
"host.name" if r.host_name.is_empty() => r.host_name = val,
"host.arch" if r.host_arch.is_empty() => r.host_arch = val,
_ => {
r.extra.insert(key.to_string(), val);
}
}
}
}
}
if r.host_arch.is_empty() {
r.host_arch = match std::env::consts::ARCH {
"x86_64" => "amd64".to_string(),
"aarch64" => "arm64".to_string(),
other => other.to_string(),
};
}
r
}
pub fn otlp_trio_from_env() -> Result<
(
super::OtlpLogSink,
super::OtlpMetricSink,
super::OtlpTraceSink,
),
String,
> {
let endpoint = endpoint_from_env();
let res = resource_from_env();
let logs = super::OtlpLogSink::builder()
.endpoint(endpoint.clone())
.resource(res.clone())
.build()
.map_err(|e| format!("{e}"))?;
let metrics = super::OtlpMetricSink::builder()
.endpoint(endpoint.clone())
.resource(res.clone())
.build()
.map_err(|e| format!("{e}"))?;
let traces = super::OtlpTraceSink::builder()
.endpoint(endpoint)
.resource(res)
.build()
.map_err(|e| format!("{e}"))?;
Ok((logs, metrics, traces))
}