use std::time::Duration;
use serde::Deserialize;
use rskit_errors::{AppError, AppResult, ErrorCode};
use crate::tracer::{OtlpProtocol, SERVICE_NAME};
#[derive(Debug, Clone, Deserialize)]
pub struct LogsConfig {
pub service_name: String,
pub otlp_endpoint: Option<String>,
pub protocol: OtlpProtocol,
pub export_timeout: Duration,
}
#[derive(Clone)]
pub struct LogsHandle {
provider: opentelemetry_sdk::logs::SdkLoggerProvider,
}
impl LogsHandle {
#[must_use]
pub fn provider(&self) -> opentelemetry_sdk::logs::SdkLoggerProvider {
self.provider.clone()
}
pub fn shutdown(&self) -> AppResult<()> {
self.provider
.shutdown()
.map_err(|e| AppError::new(ErrorCode::Internal, format!("OTLP log shutdown: {e}")))
}
}
pub fn init_logs(cfg: &LogsConfig) -> AppResult<LogsHandle> {
use opentelemetry::KeyValue;
use opentelemetry_sdk::{Resource, logs::SdkLoggerProvider};
let resource = Resource::builder_empty()
.with_attributes([KeyValue::new(SERVICE_NAME, cfg.service_name.clone())])
.build();
let builder = SdkLoggerProvider::builder().with_resource(resource);
if let Some(endpoint) = &cfg.otlp_endpoint {
#[cfg(not(feature = "otlp"))]
{
let _ = endpoint;
Err(AppError::new(
ErrorCode::InvalidInput,
"OTLP log exporter requires the `otlp` feature",
))?;
}
#[cfg(feature = "otlp")]
{
use opentelemetry_otlp::{LogExporter, WithExportConfig};
let exporter = match cfg.protocol {
OtlpProtocol::Grpc => LogExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.with_timeout(cfg.export_timeout)
.build(),
OtlpProtocol::HttpBinary => LogExporter::builder()
.with_http()
.with_protocol(opentelemetry_otlp::Protocol::HttpBinary)
.with_endpoint(endpoint)
.with_timeout(cfg.export_timeout)
.build(),
}
.map_err(|e| AppError::new(ErrorCode::Internal, format!("OTLP log exporter: {e}")))?;
let builder = builder.with_batch_exporter(exporter);
return Ok(LogsHandle {
provider: builder.build(),
});
}
}
Ok(LogsHandle {
provider: builder.build(),
})
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
fn config(endpoint: Option<&str>) -> LogsConfig {
LogsConfig {
service_name: "logs-test".to_string(),
otlp_endpoint: endpoint.map(str::to_string),
protocol: OtlpProtocol::Grpc,
export_timeout: Duration::from_millis(10),
}
}
#[test]
fn no_export_pipeline_builds_and_shuts_down_cleanly() {
let handle = init_logs(&config(None)).expect("build no-export logger provider");
let _provider = handle.provider();
handle.shutdown().expect("shutdown no-export provider");
}
#[cfg(not(feature = "otlp"))]
#[test]
fn otlp_endpoint_requires_feature() {
let error = match init_logs(&config(Some("http://127.0.0.1:4317"))) {
Ok(_) => panic!("otlp disabled should reject log exporter configuration"),
Err(error) => error,
};
assert_eq!(error.code(), ErrorCode::InvalidInput);
assert!(error.message().contains("otlp"));
}
#[cfg(feature = "otlp")]
#[test]
fn otlp_pipeline_builds_for_http_protocol_without_connecting() {
let mut cfg = config(Some("http://127.0.0.1:4318/v1/logs"));
cfg.protocol = OtlpProtocol::HttpBinary;
let handle = init_logs(&cfg).expect("build otlp logger provider");
let _provider = handle.provider();
}
}