timber_rust/service/loki/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Dante Doménech Martinez dante19031999@gmail.com
3
4#![cfg(feature = "loki")]
5#![cfg_attr(docsrs, doc(cfg(feature = "loki")))]
6
7use std::time::Duration;
8use crate::BasicAuth;
9
10/// Default app for loki streams.
11pub const LOKI_DEFAULT_APP: &str = "rust-app";
12/// Default job for loki streams.
13pub const LOKI_DEFAULT_JOB: &str = "rust-job";
14/// Default env for loki streams.
15pub const LOKI_DEFAULT_ENV: &str = "rust-env";
16/// Default retrie number for loki.
17pub const LOKI_DEFAULT_RETRIES: usize = 3;
18/// Default worker number for loki.
19pub const LOKI_DEFAULT_WORKERS: usize = 1;
20/// Default connection timeout for loki (1 second).
21pub const LOKI_DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(1);
22/// Default request timeout for loki (2 seconds).
23pub const LOKI_DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(2);
24
25/// Configuration parameters for connecting to a Grafana Loki instance.
26///
27/// This struct follows the **Builder Pattern**, allowing you to specify
28/// metadata (labels) that will be attached to every log stream sent to Loki.
29///
30/// Only available when the `loki` feature is enabled.
31///
32/// ### Example
33/// ```rust
34/// # use timber_rust::{BasicAuth, Logger};
35/// # use timber_rust::service::{LokiConfig};
36/// # use timber_rust::LokiLogger;
37/// let config = LokiConfig::new("[https://logs-prod-us-central1.grafana.net/loki/api/v1/push](https://logs-prod-us-central1.grafana.net/loki/api/v1/push)")
38///     .job("api-server")
39///     .app("billing-v2")
40///     .env("prod")
41///     .basic_auth(BasicAuth::some("12345", Some("your-api-key")))
42///     .worker_count(4);
43///
44/// let logger = LokiLogger::new(config);
45/// let logger = Logger::new(logger);
46/// ```
47#[derive(Clone)]
48pub struct Config {
49    pub(crate) url: String,
50    pub(crate) app: String,
51    pub(crate) job: String,
52    pub(crate) env: String,
53    pub(crate) basic_auth: Option<BasicAuth>,
54    pub(crate) bearer_auth: Option<String>,
55    pub(crate) connection_timeout: Duration,
56    pub(crate) request_timeout: Duration,
57    pub(crate) max_retries: usize,
58    pub(crate) worker_count: usize,
59}
60
61/// A network-based logging backend that pushes logs to Grafana Loki.
62///
63/// [`LokiService`][`crate::service::Loki`] transforms internal [`Message`][`crate::Message`] objects into Loki's
64/// JSON "Push" format. It uses a blocking HTTP client, which is intended
65/// to be executed within a dedicated background worker thread to avoid
66/// blocking the main application.
67///
68/// ### Stream Labels
69/// Every log sent via this service is tagged with the following labels:
70/// - `job`: The logical group (e.g., "logger-service").
71/// - `app`: The specific application name.
72/// - `env`: The deployment environment (e.g., "production", "dev").
73/// - `level`: The severity of the log (automatically extracted from the message).
74///
75/// ### Client Data
76/// - `url`: Base url for loki
77/// - `connection_timeout`: Connection timeout (how much time to wait for the connection to happen)
78/// - `request_timeout`: Request timeout (how much time to wait for the request's response)
79///
80/// ### Logger data:
81/// - `max_retries`: Maximum number of retries (only used in the [`LoggerFactory`][`crate::LoggerFactory`])
82/// - `worker_count`: Number of workers to use (only used in the [`LoggerFactory`][`crate::LoggerFactory`])
83impl Config {
84    /// Creates a new [`LokiConfig`][`Config`] with default settings.
85    ///
86    /// # Parameters
87    /// - `url`: Base url for loki.
88    ///
89    /// # Default Values:
90    /// - **App:** [`LOKI_DEFAULT_APP`]
91    /// - **Job:** [`LOKI_DEFAULT_JOB`]
92    /// - **Env:** [`LOKI_DEFAULT_ENV`]
93    /// - **Workers:** [`LOKI_DEFAULT_WORKERS`]
94    /// - **Connection timeout**: [`LOKI_DEFAULT_CONNECTION_TIMEOUT`]
95    /// - **Request timeout**: [`LOKI_DEFAULT_REQUEST_TIMEOUT`]
96    /// - **Maximum retries**: [`LOKI_DEFAULT_RETRIES`]
97    pub fn new<S>(url: S) -> Self
98    where
99        S: Into<String>,
100    {
101        Self::with_labels(
102            url,
103            LOKI_DEFAULT_APP.to_string(),
104            LOKI_DEFAULT_JOB.to_string(),
105            LOKI_DEFAULT_ENV.to_string(),
106        )
107    }
108
109    /// Creates a new [`LokiConfig`][`Config`] with customized labels default settings.
110    ///
111    /// # Parameters
112    /// - `url`: Base url for loki.
113    /// - `job`: The logical group (e.g., "logger-service").
114    /// - `app`: The specific application name.
115    /// - `env`: The deployment environment (e.g., "production", "dev").
116    /// - `level`: The severity of the log (automatically extracted from the message).
117    ///
118    /// # Default Values:
119    /// - **Workers:** [`LOKI_DEFAULT_WORKERS`]
120    /// - **Connection timeout**: [`LOKI_DEFAULT_CONNECTION_TIMEOUT`]
121    /// - **Request timeout**: [`LOKI_DEFAULT_REQUEST_TIMEOUT`]
122    /// - **Maximum retries**: [`LOKI_DEFAULT_RETRIES`]
123    pub fn with_labels<S1, S2, S3, S4>(url: S1, app: S3, job: S2, env: S4) -> Self
124    where
125        S1: Into<String>,
126        S2: Into<String>,
127        S3: Into<String>,
128        S4: Into<String>,
129    {
130        let mut url = url.into();
131        if !url.ends_with('/') {
132            url.push('/');
133        }
134
135        Self {
136            url,
137            app: app.into(),
138            job: job.into(),
139            env: env.into(),
140            basic_auth: None,
141            bearer_auth: None,
142            connection_timeout: LOKI_DEFAULT_CONNECTION_TIMEOUT,
143            request_timeout: LOKI_DEFAULT_REQUEST_TIMEOUT,
144            max_retries: LOKI_DEFAULT_RETRIES,
145            worker_count: LOKI_DEFAULT_WORKERS,
146        }
147    }
148
149    /// Returns the destination Loki base URL.
150    pub fn get_url(&self) -> &str {
151        &self.url
152    }
153
154    /// Returns the value of the `app` label.
155    pub fn get_app(&self) -> &str {
156        &self.app
157    }
158
159    /// Returns the value of the `job` label.
160    pub fn get_job(&self) -> &str {
161        &self.job
162    }
163
164    /// Returns the value of the `env` label.
165    pub fn get_env(&self) -> &str {
166        &self.env
167    }
168
169    /// Returns the Basic Authentication credentials if configured.
170    pub fn get_basic_auth(&self) -> Option<&BasicAuth> {
171        self.basic_auth.as_ref()
172    }
173
174    /// Returns the Bearer Token if configured.
175    pub fn get_bearer_auth(&self) -> Option<&str> {
176        self.bearer_auth.as_ref().map(|auth| auth.as_str())
177    }
178
179    /// Returns the connection timeout duration.
180    pub fn get_connection_timeout(&self) -> Duration {
181        self.connection_timeout
182    }
183
184    /// Returns the request timeout duration.
185    pub fn get_request_timeout(&self) -> Duration {
186        self.request_timeout
187    }
188
189    /// Returns the number of background worker threads requested for this service.
190    pub fn get_worker_count(&self) -> usize {
191        self.worker_count
192    }
193
194    /// Returns the maximum number of teries allowed for this service.
195    pub fn get_max_retries(&self) -> usize {
196        self.max_retries
197    }
198
199    /// Sets the destination Loki base URL (e.g., `http://localhost:3100`).
200    pub fn url<S: Into<String>>(mut self, url: S) -> Self {
201        let mut url = url.into();
202        if !url.ends_with('/') {
203            url.push('/');
204        }
205        self.url = url;
206        self
207    }
208
209    /// Sets the `app` label to identify this specific application instance.
210    pub fn app<S: Into<String>>(mut self, app: S) -> Self {
211        self.app = app.into();
212        self
213    }
214
215    /// Sets the `job` label used by Loki for indexing.
216    pub fn job<S: Into<String>>(mut self, job: S) -> Self {
217        self.job = job.into();
218        self
219    }
220
221    /// Sets the `env` label used by Loki for indexing.
222    pub fn env<S: Into<String>>(mut self, env: S) -> Self {
223        self.env = env.into();
224        self
225    }
226
227    /// Configures the number of parallel workers that should process logs for this service.
228    pub fn worker_count(mut self, worker_count: usize) -> Self {
229        self.worker_count = worker_count;
230        self
231    }
232
233    /// Configures the number of maximum retries that the process should be attempted.
234    pub fn max_retries(mut self, max_retries: usize) -> Self {
235        self.max_retries = max_retries;
236        self
237    }
238
239    /// Enables Basic Authentication for the Loki connection.
240    ///
241    /// # Arguments
242    /// * `basic_auth` [Basic auth][`BasicAuth`] object representing the login credentials.
243    pub fn basic_auth<BA>(mut self, basic_auth: Option<BA>) -> Self
244    where
245        BA: Into<BasicAuth>,
246    {
247        self.basic_auth = basic_auth.map(|auth| auth.into());
248        self
249    }
250
251    /// Enables Bearer Token authentication (e.g., JWT).
252    pub fn bearer_auth<S>(mut self, token: Option<S>) -> Self
253    where
254        S: Into<String>,
255    {
256        self.bearer_auth = token.map(|token| token.into());
257        self
258    }
259
260    /// Sets the connection timeout to try to log in loki.
261    pub fn connection_timeout<D: Into<Duration>>(mut self, connection_timeout: D) -> Self {
262        self.connection_timeout = connection_timeout.into();
263        self
264    }
265
266    /// Sets the request timeout to try to log in loki.
267    pub fn request_timeout<S: Into<Duration>>(mut self, request_timeout: S) -> Self {
268        self.request_timeout = request_timeout.into();
269        self
270    }
271}
272
273impl std::fmt::Debug for Config {
274    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275        let mut d = f.debug_struct("LokiConfig");
276
277        // Show normal fields
278        d.field("url", &self.url)
279            .field("app", &self.app)
280            .field("job", &self.job)
281            .field("env", &self.env);
282
283        // Conditional display for secrets
284        #[cfg(debug_assertions)]
285        {
286            d.field("basic_auth", &self.basic_auth)
287                .field("bearer_auth", &self.bearer_auth);
288        }
289
290        #[cfg(not(debug_assertions))]
291        {
292            // In release, we just show that a value exists without revealing it
293            let auth_status = if self.bearer_auth.is_some() || self.basic_auth.is_some() {
294                "REDACTED (Set)"
295            } else {
296                "None"
297            };
298            d.field("auth", &auth_status);
299        }
300
301        d.field("connection_timeout", &self.connection_timeout)
302            .field("request_timeout", &self.request_timeout)
303            .field("max_retries", &self.max_retries)
304            .field("worker_count", &self.worker_count)
305            .finish()
306    }
307}
308
309