microclaw_engine/internal/observability/
logs.rs1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use opentelemetry::logs::{
6 AnyValue as OtelAnyValue, LogRecord, Logger, LoggerProvider, Severity as OtelSeverity,
7};
8use opentelemetry::KeyValue;
9use opentelemetry_otlp::{Protocol, WithExportConfig, WithHttpConfig};
10use opentelemetry_proto::tonic::logs::v1::SeverityNumber;
11use opentelemetry_sdk::logs::log_processor_with_async_runtime::BatchLogProcessor;
12use opentelemetry_sdk::logs::{BatchConfigBuilder, SdkLogger, SdkLoggerProvider};
13use opentelemetry_sdk::runtime;
14use serde_yaml::Value;
15use tracing::{debug, info, warn};
16
17use crate::internal::observability::sdk::{get_bool, get_trimmed, parse_headers, OTelSdkContext};
18
19#[derive(Debug, Clone)]
20pub struct OtlpLogRecord {
21 pub timestamp_unix_nano: u64,
22 pub severity_number: SeverityNumber,
23 pub severity_text: String,
24 pub body: String,
25 pub attributes: Vec<KeyValue>,
26}
27
28#[derive(Clone)]
29pub struct OtlpLogExporter {
30 logger: Arc<SdkLogger>,
31 provider: Arc<SdkLoggerProvider>,
32}
33
34impl OtlpLogExporter {
35 pub fn from_observability(observability: Option<&Value>) -> Option<Arc<Self>> {
36 let map = observability?.as_mapping()?;
37 let enabled = get_bool(map, "otlp_logs_enabled").unwrap_or(false);
38 if !enabled {
39 debug!("otlp log exporter disabled by config");
40 return None;
41 }
42 let endpoint = get_trimmed(map, "otlp_logs_endpoint")?.to_string();
43 let sdk = OTelSdkContext::from_observability(map);
44 let headers = parse_headers(map);
45
46 let mut exporter_builder = opentelemetry_otlp::LogExporter::builder()
47 .with_http()
48 .with_protocol(Protocol::HttpBinary)
49 .with_http_client(reqwest::Client::new())
50 .with_endpoint(endpoint.clone());
51 if !headers.is_empty() {
52 exporter_builder =
53 exporter_builder.with_headers(headers.into_iter().collect::<HashMap<_, _>>());
54 }
55 let exporter = match exporter_builder.build() {
56 Ok(exporter) => exporter,
57 Err(err) => {
58 warn!(
59 endpoint = %endpoint,
60 "failed to build otlp log exporter: {}",
61 err
62 );
63 return None;
64 }
65 };
66 let batch_config = BatchConfigBuilder::default()
67 .with_max_queue_size(8192)
68 .with_max_export_batch_size(512)
69 .with_scheduled_delay(Duration::from_millis(1000))
70 .build();
71 let processor = BatchLogProcessor::builder(exporter, runtime::Tokio)
72 .with_batch_config(batch_config)
73 .build();
74 let provider = SdkLoggerProvider::builder()
75 .with_resource(sdk.resource)
76 .with_log_processor(processor)
77 .build();
78 let logger = provider.logger("microclaw.observability.logs");
79 info!(
80 endpoint = %endpoint,
81 service_name = %sdk.service_name,
82 "otlp log exporter initialized"
83 );
84
85 Some(Arc::new(Self {
86 logger: Arc::new(logger),
87 provider: Arc::new(provider),
88 }))
89 }
90
91 pub fn send_log(&self, record: OtlpLogRecord) {
92 let mut log_record = self.logger.create_log_record();
93 log_record.set_timestamp(unix_nano_to_time(record.timestamp_unix_nano));
94 log_record.set_observed_timestamp(SystemTime::now());
95 log_record.set_severity_number(to_otel_severity(record.severity_number));
96 log_record.set_body(OtelAnyValue::from(record.body));
97 if !record.severity_text.is_empty() {
98 log_record.add_attribute("severity.text", record.severity_text);
99 }
100 for kv in record.attributes {
101 log_record.add_attribute(kv.key, kv.value.to_string());
102 }
103 self.logger.emit(log_record);
104 debug!("log record submitted to otel sdk");
105 }
106
107 pub fn logger_provider(&self) -> Arc<SdkLoggerProvider> {
108 self.provider.clone()
109 }
110}
111
112impl Drop for OtlpLogExporter {
113 fn drop(&mut self) {
114 if let Err(err) = self.provider.force_flush() {
115 warn!("otlp log exporter force_flush failed: {}", err);
116 } else {
117 debug!("otlp log exporter force_flush succeeded");
118 }
119 }
120}
121
122fn to_otel_severity(severity: SeverityNumber) -> OtelSeverity {
123 match severity as i32 {
124 1..=4 => OtelSeverity::Trace,
125 5..=8 => OtelSeverity::Debug,
126 9..=12 => OtelSeverity::Info,
127 13..=16 => OtelSeverity::Warn,
128 17..=20 => OtelSeverity::Error,
129 21..=24 => OtelSeverity::Fatal,
130 _ => OtelSeverity::Info,
131 }
132}
133
134fn unix_nano_to_time(value: u64) -> SystemTime {
135 UNIX_EPOCH + Duration::from_nanos(value)
136}