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}