1use std::fmt;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum Format {
17 #[default]
19 Auto,
20 Compact,
22 Pretty,
24 Json,
27}
28
29impl Format {
30 pub fn from_env_value(value: &str) -> Result<Self, ParseFormatError> {
37 value.parse()
38 }
39}
40
41impl FromStr for Format {
42 type Err = ParseFormatError;
43
44 fn from_str(value: &str) -> Result<Self, Self::Err> {
45 match value.trim().to_ascii_lowercase().as_str() {
46 "auto" => Ok(Self::Auto),
47 "compact" => Ok(Self::Compact),
48 "pretty" => Ok(Self::Pretty),
49 "json" => Ok(Self::Json),
50 _ => Err(ParseFormatError),
51 }
52 }
53}
54
55#[non_exhaustive]
57#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
58#[error("expected one of: auto, compact, pretty, json")]
59pub struct ParseFormatError;
60
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
68#[non_exhaustive]
69pub enum Sink {
70 #[default]
72 Auto,
73 Stdout,
75 Stderr,
77 Journald,
79}
80
81impl Sink {
82 pub fn from_env_value(value: &str) -> Result<Self, ParseSinkError> {
89 value.parse()
90 }
91}
92
93impl FromStr for Sink {
94 type Err = ParseSinkError;
95
96 fn from_str(value: &str) -> Result<Self, Self::Err> {
97 match value.trim().to_ascii_lowercase().as_str() {
98 "auto" => Ok(Self::Auto),
99 "stdout" => Ok(Self::Stdout),
100 "stderr" => Ok(Self::Stderr),
101 "journald" => Ok(Self::Journald),
102 _ => Err(ParseSinkError),
103 }
104 }
105}
106
107#[non_exhaustive]
109#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
110#[error("expected one of: auto, stdout, stderr, journald")]
111pub struct ParseSinkError;
112
113#[derive(Clone, Default)]
130pub struct InitOptions {
131 pub(crate) service_name: Option<String>,
132 pub(crate) default_filter: Option<String>,
133 pub(crate) env_var: Option<String>,
134 pub(crate) format: Format,
135 pub(crate) sink: Sink,
136 pub(crate) idempotent: bool,
137 #[cfg(feature = "with-otlp")]
138 pub(crate) otlp: Option<crate::otlp::OtlpConfig>,
139}
140
141impl fmt::Debug for InitOptions {
142 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
143 let mut debug = formatter.debug_struct("InitOptions");
144 debug
145 .field("service_name", &self.service_name)
146 .field("default_filter", &self.default_filter)
147 .field("env_var", &self.env_var)
148 .field("format", &self.format)
149 .field("sink", &self.sink)
150 .field("idempotent", &self.idempotent);
151 #[cfg(feature = "with-otlp")]
152 debug.field("otlp", &self.otlp);
153 debug.finish()
154 }
155}
156
157impl InitOptions {
158 #[must_use]
161 pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
162 self.service_name = Some(name.into());
163 self
164 }
165
166 #[must_use]
169 pub fn with_default_filter(mut self, filter: impl Into<String>) -> Self {
170 self.default_filter = Some(filter.into());
171 self
172 }
173
174 #[must_use]
178 pub fn with_env_var(mut self, var: impl Into<String>) -> Self {
179 self.env_var = Some(var.into());
180 self
181 }
182
183 #[must_use]
185 pub fn with_format(mut self, format: Format) -> Self {
186 self.format = format;
187 self
188 }
189
190 #[must_use]
192 pub fn with_sink(mut self, sink: Sink) -> Self {
193 self.sink = sink;
194 self
195 }
196
197 #[must_use]
204 pub fn idempotent(mut self, enabled: bool) -> Self {
205 self.idempotent = enabled;
206 self
207 }
208
209 #[cfg(feature = "with-otlp")]
212 #[must_use]
213 pub fn with_otlp(mut self, config: crate::otlp::OtlpConfig) -> Self {
214 self.otlp = Some(config);
215 self
216 }
217
218 #[cfg(feature = "systemd")]
219 pub(crate) fn resolved_env_var(&self) -> &str {
220 self.env_var.as_deref().unwrap_or("RUST_LOG")
221 }
222
223 pub(crate) fn resolved_default_filter(&self) -> &str {
224 if let Some(filter) = self.default_filter.as_deref() {
225 return filter;
226 }
227 if cfg!(debug_assertions) { "debug" } else { "info" }
228 }
229
230 #[cfg(feature = "wasm32")]
231 pub(crate) fn resolved_wasm_format(&self) -> Format {
232 match self.format {
233 Format::Auto => Format::Json,
234 format => format,
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn builder_sets_every_field() {
245 let opts = InitOptions::default()
246 .with_service_name("svc")
247 .with_default_filter("warn")
248 .with_env_var("KKP_LOG")
249 .with_format(Format::Json)
250 .with_sink(Sink::Stdout)
251 .idempotent(true);
252 assert_eq!(opts.service_name.as_deref(), Some("svc"));
253 assert_eq!(opts.default_filter.as_deref(), Some("warn"));
254 assert_eq!(opts.env_var.as_deref(), Some("KKP_LOG"));
255 assert_eq!(opts.format, Format::Json);
256 assert_eq!(opts.sink, Sink::Stdout);
257 assert!(opts.idempotent);
258 }
259
260 #[cfg(feature = "systemd")]
261 #[test]
262 fn resolved_helpers_apply_defaults_then_overrides() {
263 let default = InitOptions::default();
264 assert_eq!(default.resolved_env_var(), "RUST_LOG");
265 let expected = if cfg!(debug_assertions) { "debug" } else { "info" };
266 assert_eq!(default.resolved_default_filter(), expected);
267
268 let custom = InitOptions::default().with_env_var("KKP_LOG").with_default_filter("trace");
269 assert_eq!(custom.resolved_env_var(), "KKP_LOG");
270 assert_eq!(custom.resolved_default_filter(), "trace");
271 }
272}