Skip to main content

kamu_logging/
options.rs

1//! Configuration for [`init_with`](crate::init_with).
2
3/// Output format for the fmt layer.
4///
5/// `Auto` is replaced at init time by [`Format::Pretty`] when the chosen sink
6/// is a TTY and [`Format::Compact`] otherwise. Set the `KAMU_LOG_FORMAT`
7/// environment variable (`auto`, `compact`, `pretty`, `json`) to override
8/// without code changes. On wasm32, `Auto` resolves to [`Format::Json`] for
9/// Cloudflare Workers Logs-friendly console output, and [`Format::Pretty`]
10/// falls back to non-ANSI compact output.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum Format {
14    /// Resolved at init time based on sink + env var.
15    #[default]
16    Auto,
17    /// Single-line, machine-readable text format.
18    Compact,
19    /// Multi-line, human-readable text format with ANSI colors.
20    Pretty,
21    /// Line-delimited JSON. Required for log aggregators (Vector, Promtail,
22    /// Datadog Agent, Fluent Bit).
23    Json,
24}
25
26impl Format {
27    /// Parse from an env-var value. Unknown values resolve to `Auto`.
28    #[must_use]
29    pub fn from_env_value(value: &str) -> Self {
30        match value.trim().to_ascii_lowercase().as_str() {
31            "compact" => Self::Compact,
32            "pretty" => Self::Pretty,
33            "json" => Self::Json,
34            _ => Self::Auto,
35        }
36    }
37}
38
39/// Where to write log events.
40///
41/// `Auto` (default) emits to the console when stdout is a TTY and to journald
42/// otherwise. Set `KAMU_LOG_SINK` (`auto`, `stdout`, `stderr`, `journald`) to
43/// override without code changes. `Journald` is rejected on targets without
44/// the `systemd` feature. On wasm32, `Auto`, `Stdout`, and `Stderr` all map to
45/// the JavaScript console.
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum Sink {
49    /// TTY → console, non-TTY → journald (on systemd) or stderr (otherwise).
50    #[default]
51    Auto,
52    /// Write to stdout.
53    Stdout,
54    /// Write to stderr.
55    Stderr,
56    /// Write to the systemd journal. Requires the `systemd` feature.
57    Journald,
58}
59
60impl Sink {
61    /// Parse from an env-var value. Unknown values resolve to `Auto`.
62    #[must_use]
63    pub fn from_env_value(value: &str) -> Self {
64        match value.trim().to_ascii_lowercase().as_str() {
65            "stdout" => Self::Stdout,
66            "stderr" => Self::Stderr,
67            "journald" => Self::Journald,
68            _ => Self::Auto,
69        }
70    }
71}
72
73/// Configuration for [`init_with`](crate::init_with).
74///
75/// Constructed via [`InitOptions::default`] and the `with_*` builder methods.
76/// Each method consumes `self` and returns `Self` to support chaining:
77///
78/// ```no_run
79/// use kamu_logging::{init_with, Format, InitOptions, Sink};
80///
81/// init_with(
82///     InitOptions::default()
83///         .with_service_name("my-service")
84///         .with_format(Format::Json)
85///         .with_sink(Sink::Stdout),
86/// )?;
87/// # Ok::<(), kamu_logging::Error>(())
88/// ```
89#[derive(Debug, Clone, Default)]
90pub struct InitOptions {
91    pub(crate) service_name: Option<String>,
92    pub(crate) default_filter: Option<String>,
93    pub(crate) env_var: Option<String>,
94    pub(crate) format: Format,
95    pub(crate) sink: Sink,
96    pub(crate) idempotent: bool,
97    #[cfg(feature = "with-otlp")]
98    pub(crate) otlp: Option<crate::otlp::OtlpConfig>,
99}
100
101impl InitOptions {
102    /// Attach a `service.name` field to every event. Used by log aggregators
103    /// to route logs across services.
104    #[must_use]
105    pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
106        self.service_name = Some(name.into());
107        self
108    }
109
110    /// Default filter directive when the env var is unset. Defaults to
111    /// `"debug"` in debug builds and `"info"` in release builds.
112    #[must_use]
113    pub fn with_default_filter(mut self, filter: impl Into<String>) -> Self {
114        self.default_filter = Some(filter.into());
115        self
116    }
117
118    /// Environment variable read for the filter directive. Defaults to
119    /// `"RUST_LOG"`. Useful for per-binary triggers like `"KKP_LOG"` to avoid
120    /// collisions with other tools' `RUST_LOG` settings.
121    #[must_use]
122    pub fn with_env_var(mut self, var: impl Into<String>) -> Self {
123        self.env_var = Some(var.into());
124        self
125    }
126
127    /// Output format. See [`Format`].
128    #[must_use]
129    pub fn with_format(mut self, format: Format) -> Self {
130        self.format = format;
131        self
132    }
133
134    /// Output sink. See [`Sink`].
135    #[must_use]
136    pub fn with_sink(mut self, sink: Sink) -> Self {
137        self.sink = sink;
138        self
139    }
140
141    /// When `true`, a second [`init_with`](crate::init_with) call returns
142    /// `Ok(())` instead of [`Error::TracingGlobal`](crate::Error::TracingGlobal).
143    ///
144    /// Default is `false` so library double-init surfaces as an error. Set
145    /// `true` from test harnesses and embedded CLI runs where re-init is
146    /// expected.
147    #[must_use]
148    pub fn idempotent(mut self, enabled: bool) -> Self {
149        self.idempotent = enabled;
150        self
151    }
152
153    /// Attach an OpenTelemetry OTLP exporter layer. Requires the `with-otlp`
154    /// feature.
155    #[cfg(feature = "with-otlp")]
156    #[must_use]
157    pub fn with_otlp(mut self, config: crate::otlp::OtlpConfig) -> Self {
158        self.otlp = Some(config);
159        self
160    }
161
162    #[cfg(feature = "systemd")]
163    pub(crate) fn resolved_env_var(&self) -> &str {
164        self.env_var.as_deref().unwrap_or("RUST_LOG")
165    }
166
167    #[cfg(feature = "systemd")]
168    pub(crate) fn resolved_default_filter(&self) -> &str {
169        if let Some(filter) = self.default_filter.as_deref() {
170            return filter;
171        }
172        if cfg!(debug_assertions) { "debug" } else { "info" }
173    }
174
175    #[cfg(feature = "wasm32")]
176    pub(crate) fn resolved_default_filter(&self) -> &str {
177        if let Some(filter) = self.default_filter.as_deref() {
178            return filter;
179        }
180        if cfg!(debug_assertions) { "debug" } else { "info" }
181    }
182
183    #[cfg(feature = "wasm32")]
184    pub(crate) fn resolved_wasm_format(&self) -> Format {
185        match self.format {
186            Format::Auto => Format::Json,
187            format => format,
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn builder_sets_every_field() {
198        let opts = InitOptions::default()
199            .with_service_name("svc")
200            .with_default_filter("warn")
201            .with_env_var("KKP_LOG")
202            .with_format(Format::Json)
203            .with_sink(Sink::Stdout)
204            .idempotent(true);
205        assert_eq!(opts.service_name.as_deref(), Some("svc"));
206        assert_eq!(opts.default_filter.as_deref(), Some("warn"));
207        assert_eq!(opts.env_var.as_deref(), Some("KKP_LOG"));
208        assert_eq!(opts.format, Format::Json);
209        assert_eq!(opts.sink, Sink::Stdout);
210        assert!(opts.idempotent);
211    }
212
213    #[cfg(feature = "systemd")]
214    #[test]
215    fn resolved_helpers_apply_defaults_then_overrides() {
216        let default = InitOptions::default();
217        assert_eq!(default.resolved_env_var(), "RUST_LOG");
218        let expected = if cfg!(debug_assertions) { "debug" } else { "info" };
219        assert_eq!(default.resolved_default_filter(), expected);
220
221        let custom = InitOptions::default().with_env_var("KKP_LOG").with_default_filter("trace");
222        assert_eq!(custom.resolved_env_var(), "KKP_LOG");
223        assert_eq!(custom.resolved_default_filter(), "trace");
224    }
225}