blueprint_qos/logging/loki.rs
1use blueprint_core::error;
2use std::collections::HashMap;
3use tracing_loki::url::Url;
4use tracing_subscriber::EnvFilter;
5use tracing_subscriber::{Registry, layer::SubscriberExt};
6
7// Default values for LokiConfig
8const DEFAULT_LOKI_LABEL_SERVICE_KEY: &str = "service";
9const DEFAULT_LOKI_LABEL_SERVICE_VALUE: &str = "blueprint";
10const DEFAULT_LOKI_LABEL_ENVIRONMENT_KEY: &str = "environment";
11const DEFAULT_LOKI_LABEL_ENVIRONMENT_VALUE: &str = "development";
12const DEFAULT_LOKI_URL: &str = "http://localhost:3100";
13const DEFAULT_LOKI_BATCH_SIZE: usize = 100;
14const DEFAULT_LOKI_TIMEOUT_SECS: u64 = 5;
15
16use crate::error::{Error, Result};
17
18/// Configuration for Loki log aggregation integration.
19///
20/// This structure defines settings for connecting to and sending logs to a Loki server,
21/// which is part of the Grafana observability stack. Loki is designed for storing and
22/// querying log data, providing an efficient way to centralize logs from Blueprint services.
23/// The configuration includes connection details, authentication, log batching parameters,
24/// and custom labels that will be attached to all logs sent to Loki.
25#[derive(Clone, Debug)]
26pub struct LokiConfig {
27 /// Loki server URL
28 pub url: String,
29
30 /// Basic auth username (optional)
31 pub username: Option<String>,
32
33 /// Basic auth password (optional)
34 pub password: Option<String>,
35
36 /// Labels to attach to all logs
37 pub labels: HashMap<String, String>,
38
39 /// Batch size for sending logs
40 pub batch_size: usize,
41
42 /// Timeout for sending logs
43 pub timeout_secs: u64,
44
45 /// OpenTelemetry configuration
46 pub otel_config: Option<OtelConfig>,
47}
48
49impl Default for LokiConfig {
50 fn default() -> Self {
51 let mut labels = HashMap::new();
52 labels.insert(
53 DEFAULT_LOKI_LABEL_SERVICE_KEY.to_string(),
54 DEFAULT_LOKI_LABEL_SERVICE_VALUE.to_string(),
55 );
56 labels.insert(
57 DEFAULT_LOKI_LABEL_ENVIRONMENT_KEY.to_string(),
58 DEFAULT_LOKI_LABEL_ENVIRONMENT_VALUE.to_string(),
59 );
60
61 Self {
62 url: DEFAULT_LOKI_URL.to_string(),
63 username: None,
64 password: None,
65 labels,
66 batch_size: DEFAULT_LOKI_BATCH_SIZE,
67 timeout_secs: DEFAULT_LOKI_TIMEOUT_SECS,
68 otel_config: None,
69 }
70 }
71}
72
73/// OpenTelemetry configuration
74#[derive(Clone, Debug)]
75pub struct OtelConfig {
76 /// Maximum attributes per span
77 pub max_attributes_per_span: Option<u32>,
78}
79
80/// Initializes Loki logging with the provided configuration.
81///
82/// This function sets up the tracing infrastructure to send logs to a Loki server,
83/// creating a background task that collects logs and sends them in batches. It configures
84/// labels, authentication, and connection details according to the provided configuration.
85/// The function integrates with Rust's tracing ecosystem for seamless log forwarding.
86///
87/// # Parameters
88/// * `config` - Configuration settings for the Loki connection and log processing
89///
90/// # Errors
91/// Returns an error if the Loki layer initialization fails, including URL parsing errors,
92/// label configuration errors, or connection setup failures
93pub fn init_loki_logging(config: LokiConfig) -> Result<()> {
94 // Parse the Loki URL
95 let url = Url::parse(&config.url)
96 .map_err(|e| Error::Other(format!("Failed to parse Loki URL: {}", e)))?;
97
98 // Create a builder for the Loki layer
99 let mut builder = tracing_loki::builder();
100
101 // Add labels
102 for (key, value) in config.labels {
103 builder = match builder.label(key.clone(), value.clone()) {
104 Ok(b) => b,
105 Err(e) => {
106 error!("Failed to add label to Loki layer: {}", e);
107 return Err(Error::Other(format!(
108 "Failed to add label to Loki layer: {}",
109 e
110 )));
111 }
112 };
113 }
114
115 // Build the Loki layer with URL
116 let (loki_layer, task) = match builder.build_url(url) {
117 Ok((layer, task)) => (layer, task),
118 Err(e) => {
119 error!("Failed to build Loki layer: {}", e);
120 return Err(Error::Other(format!("Failed to build Loki layer: {}", e)));
121 }
122 };
123
124 // Spawn the background task
125 tokio::spawn(task);
126
127 // Create a subscriber with the Loki layer
128 let _subscriber = Registry::default()
129 .with(EnvFilter::from_default_env())
130 .with(loki_layer);
131
132 // TODO: Fix loki logging
133 // // Set the subscriber as the global default
134 // match blueprint_core::subscriber::set_global_default(subscriber) {
135 // Ok(()) => {
136 // info!("Initialized Loki logging");
137 // Ok(())
138 // }
139 // Err(e) => {
140 // error!("Failed to set global subscriber: {}", e);
141 // Err(Error::Other(format!(
142 // "Failed to set global subscriber: {}",
143 // e
144 // )))
145 // }
146 // }
147 Ok(())
148}
149
150/// Initializes Loki logging with OpenTelemetry integration for distributed tracing.
151///
152/// This function sets up both Loki logging and OpenTelemetry tracing, creating an
153/// integrated observability pipeline. OpenTelemetry traces can be correlated with
154/// logs, providing a unified view of request flows across distributed systems. The
155/// integration enriches logs with trace context and enables more powerful debugging
156/// and monitoring capabilities.
157///
158/// # Parameters
159/// * `loki_config` - Configuration for the Loki connection and log processing
160/// * `service_name` - Name of the service, used for identifying the source in traces and logs
161///
162/// # Errors
163/// Returns an error if the Loki layer or OpenTelemetry initialization fails, including
164/// configuration errors, connection issues, or pipeline setup problems
165pub fn init_loki_with_opentelemetry(loki_config: &LokiConfig, service_name: &str) -> Result<()> {
166 // Parse the Loki URL
167 let url = Url::parse(&loki_config.url)
168 .map_err(|e| Error::Other(format!("Failed to parse Loki URL: {}", e)))?;
169
170 // Create a builder for the Loki layer
171 let mut builder = tracing_loki::builder();
172
173 // Add labels
174 for (key, value) in &loki_config.labels {
175 builder = match builder.label(key.clone(), value.clone()) {
176 Ok(b) => b,
177 Err(e) => {
178 error!("Failed to add label to Loki layer: {}", e);
179 return Err(Error::Other(format!(
180 "Failed to add label to Loki layer: {}",
181 e
182 )));
183 }
184 };
185 }
186
187 // Build the Loki layer with URL
188 let (loki_layer, task) = match builder.build_url(url) {
189 Ok((layer, task)) => (layer, task),
190 Err(e) => {
191 error!("Failed to build Loki layer: {}", e);
192 return Err(Error::Other(format!("Failed to build Loki layer: {}", e)));
193 }
194 };
195
196 // Spawn the background task
197 tokio::spawn(task);
198
199 init_otel_tracer(loki_config, service_name)?;
200
201 let opentelemetry_layer = tracing_opentelemetry::layer();
202 let _subscriber = Registry::default()
203 .with(EnvFilter::from_default_env())
204 .with(loki_layer)
205 .with(opentelemetry_layer);
206
207 // TODO: Fix loki logging
208 // // Set the subscriber as the global default
209 // match tracing::subscriber::set_global_default(subscriber) {
210 // Ok(()) => {
211 // info!("Initialized Loki logging with OpenTelemetry");
212 // Ok(())
213 // }
214 // Err(e) => {
215 // error!("Failed to set global subscriber: {}", e);
216 // Err(Error::Other(format!(
217 // "Failed to set global subscriber: {}",
218 // e
219 // )))
220 // }
221 // }
222 Ok(())
223}
224
225/// Initializes an OpenTelemetry tracer with Loki integration for trace export.
226///
227/// This function configures and starts an OpenTelemetry tracer that sends trace
228/// data to Loki. It sets up the trace context propagation, sampling strategies,
229/// and export pipelines according to the provided configuration. The tracer captures
230/// distributed tracing information that can be viewed alongside logs in Grafana.
231///
232/// # Parameters
233/// * `loki_config` - Configuration for the Loki connection including OpenTelemetry settings
234/// * `service_name` - Name of the service to identify the source of traces
235///
236/// # Errors
237/// Returns an error if the OpenTelemetry tracer initialization fails, such as
238/// invalid configuration, resource allocation issues, or export pipeline setup failures
239pub fn init_otel_tracer(loki_config: &LokiConfig, service_name: &str) -> Result<()> {
240 use opentelemetry::KeyValue;
241
242 let service_name_owned = service_name.to_string();
243
244 let resource = opentelemetry_sdk::Resource::builder()
245 .with_attributes(vec![
246 KeyValue::new("service.name", service_name_owned.clone()),
247 KeyValue::new("service.version", env!("CARGO_PKG_VERSION").to_string()),
248 ])
249 .build();
250
251 // Apply settings from config if present
252 if let Some(_otel_cfg) = &loki_config.otel_config {}
253
254 let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
255 .with_resource(resource)
256 .build();
257
258 // Set as global provider
259 opentelemetry::global::set_tracer_provider(provider);
260
261 Ok(())
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 /// Tests that the `LokiConfig` default implementation returns a valid configuration.
269 ///
270 /// ```
271 /// LokiConfig::default() -> Valid config
272 /// ```
273 ///
274 /// Expected outcome: Default config has reasonable values
275 #[test]
276 fn test_loki_config_default() {
277 let config = LokiConfig::default();
278 assert_eq!(config.url, "http://localhost:3100");
279 assert_eq!(config.batch_size, 100);
280 }
281}