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