rskit_observability/
logs.rs1use std::time::Duration;
2
3use serde::Deserialize;
4
5use rskit_errors::{AppError, AppResult, ErrorCode};
6
7use crate::tracer::{OtlpProtocol, SERVICE_NAME};
8
9#[derive(Debug, Clone, Deserialize)]
11pub struct LogsConfig {
12 pub service_name: String,
14 pub otlp_endpoint: Option<String>,
16 pub protocol: OtlpProtocol,
18 pub export_timeout: Duration,
20}
21
22#[derive(Clone)]
24pub struct LogsHandle {
25 provider: opentelemetry_sdk::logs::SdkLoggerProvider,
26}
27
28impl LogsHandle {
29 #[must_use]
31 pub fn provider(&self) -> opentelemetry_sdk::logs::SdkLoggerProvider {
32 self.provider.clone()
33 }
34
35 pub fn shutdown(&self) -> AppResult<()> {
37 self.provider
38 .shutdown()
39 .map_err(|e| AppError::new(ErrorCode::Internal, format!("OTLP log shutdown: {e}")))
40 }
41}
42
43pub fn init_logs(cfg: &LogsConfig) -> AppResult<LogsHandle> {
48 use opentelemetry::KeyValue;
49 use opentelemetry_sdk::{Resource, logs::SdkLoggerProvider};
50
51 let resource = Resource::builder_empty()
52 .with_attributes([KeyValue::new(SERVICE_NAME, cfg.service_name.clone())])
53 .build();
54 let builder = SdkLoggerProvider::builder().with_resource(resource);
55
56 if let Some(endpoint) = &cfg.otlp_endpoint {
57 #[cfg(not(feature = "otlp"))]
58 {
59 let _ = endpoint;
60 Err(AppError::new(
61 ErrorCode::InvalidInput,
62 "OTLP log exporter requires the `otlp` feature",
63 ))?;
64 }
65
66 #[cfg(feature = "otlp")]
67 {
68 use opentelemetry_otlp::{LogExporter, WithExportConfig};
69
70 let exporter = match cfg.protocol {
71 OtlpProtocol::Grpc => LogExporter::builder()
72 .with_tonic()
73 .with_endpoint(endpoint)
74 .with_timeout(cfg.export_timeout)
75 .build(),
76 OtlpProtocol::HttpBinary => LogExporter::builder()
77 .with_http()
78 .with_protocol(opentelemetry_otlp::Protocol::HttpBinary)
79 .with_endpoint(endpoint)
80 .with_timeout(cfg.export_timeout)
81 .build(),
82 }
83 .map_err(|e| AppError::new(ErrorCode::Internal, format!("OTLP log exporter: {e}")))?;
84 let builder = builder.with_batch_exporter(exporter);
85 return Ok(LogsHandle {
86 provider: builder.build(),
87 });
88 }
89 }
90
91 Ok(LogsHandle {
92 provider: builder.build(),
93 })
94}
95
96#[cfg(test)]
97mod tests {
98 use std::time::Duration;
99
100 use super::*;
101
102 fn config(endpoint: Option<&str>) -> LogsConfig {
103 LogsConfig {
104 service_name: "logs-test".to_string(),
105 otlp_endpoint: endpoint.map(str::to_string),
106 protocol: OtlpProtocol::Grpc,
107 export_timeout: Duration::from_millis(10),
108 }
109 }
110
111 #[test]
112 fn no_export_pipeline_builds_and_shuts_down_cleanly() {
113 let handle = init_logs(&config(None)).expect("build no-export logger provider");
114 let _provider = handle.provider();
115
116 handle.shutdown().expect("shutdown no-export provider");
117 }
118
119 #[cfg(not(feature = "otlp"))]
120 #[test]
121 fn otlp_endpoint_requires_feature() {
122 let error = match init_logs(&config(Some("http://127.0.0.1:4317"))) {
123 Ok(_) => panic!("otlp disabled should reject log exporter configuration"),
124 Err(error) => error,
125 };
126
127 assert_eq!(error.code(), ErrorCode::InvalidInput);
128 assert!(error.message().contains("otlp"));
129 }
130
131 #[cfg(feature = "otlp")]
132 #[test]
133 fn otlp_pipeline_builds_for_http_protocol_without_connecting() {
134 let mut cfg = config(Some("http://127.0.0.1:4318/v1/logs"));
135 cfg.protocol = OtlpProtocol::HttpBinary;
136
137 let handle = init_logs(&cfg).expect("build otlp logger provider");
138 let _provider = handle.provider();
139 }
140}