1use std::path::PathBuf;
9
10pub(crate) const DEFAULT_LEVEL: &str = "info";
12pub(crate) const DEFAULT_SERVICE_NAME: &str = "app";
14pub(crate) const DEFAULT_FILE_PREFIX: &str = "app";
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum ErrorLogDetail {
21 #[default]
23 TypeOnly,
24 MessageOnly,
26 FullChain,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum LogFormat {
34 #[default]
37 Auto,
38 Pretty,
40 Compact,
42 Json,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
48#[serde(rename_all = "lowercase")]
49pub enum Rotation {
50 Never,
52 Hourly,
54 #[default]
56 Daily,
57}
58
59#[derive(Debug, Clone)]
61pub struct FileLogConfig {
62 pub(crate) directory: PathBuf,
63 pub(crate) prefix: String,
64 pub(crate) rotation: Rotation,
65 pub(crate) non_blocking: bool,
66}
67
68impl FileLogConfig {
69 pub fn new(directory: impl Into<PathBuf>) -> Self {
71 Self {
72 directory: directory.into(),
73 prefix: DEFAULT_FILE_PREFIX.to_owned(),
74 rotation: Rotation::default(),
75 non_blocking: true,
76 }
77 }
78
79 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
81 self.prefix = prefix.into();
82 self
83 }
84
85 pub fn rotation(mut self, rotation: Rotation) -> Self {
87 self.rotation = rotation;
88 self
89 }
90
91 pub fn non_blocking(mut self, non_blocking: bool) -> Self {
93 self.non_blocking = non_blocking;
94 self
95 }
96}
97
98#[allow(dead_code)]
101#[derive(Debug, Clone)]
102pub struct TelemetryConfig {
103 pub(crate) enabled: bool,
104 pub(crate) otlp_endpoint: String,
105 pub(crate) service_name: String,
106}
107
108impl TelemetryConfig {
109 pub fn new(otlp_endpoint: impl Into<String>) -> Self {
111 Self {
112 enabled: true,
113 otlp_endpoint: otlp_endpoint.into(),
114 service_name: DEFAULT_SERVICE_NAME.to_owned(),
115 }
116 }
117
118 pub fn service_name(mut self, name: impl Into<String>) -> Self {
120 self.service_name = name.into();
121 self
122 }
123
124 pub fn enabled(mut self, enabled: bool) -> Self {
126 self.enabled = enabled;
127 self
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct LoggerConfig {
134 pub(crate) level: String,
135 pub(crate) format: LogFormat,
136 pub(crate) color: bool,
137 pub(crate) service_name: String,
138 pub(crate) error_detail: ErrorLogDetail,
139 pub(crate) request_logs: bool,
140 #[allow(dead_code)]
143 pub(crate) include_source: bool,
144 #[allow(dead_code)]
145 pub(crate) include_thread_ids: bool,
146 pub(crate) non_blocking: bool,
147 pub(crate) file: Option<FileLogConfig>,
148 #[allow(dead_code)]
149 pub(crate) telemetry: Option<TelemetryConfig>,
150}
151
152impl Default for LoggerConfig {
153 fn default() -> Self {
154 Self {
155 level: DEFAULT_LEVEL.to_owned(),
156 format: LogFormat::Auto,
157 color: true,
158 service_name: DEFAULT_SERVICE_NAME.to_owned(),
159 error_detail: ErrorLogDetail::default(),
160 request_logs: true,
161 include_source: false,
162 include_thread_ids: false,
163 non_blocking: false,
164 file: None,
165 telemetry: None,
166 }
167 }
168}
169
170impl LoggerConfig {
171 pub fn new() -> Self {
173 Self::default()
174 }
175
176 pub fn level(mut self, level: impl Into<String>) -> Self {
179 self.level = level.into();
180 self
181 }
182
183 pub fn format(mut self, format: LogFormat) -> Self {
185 self.format = format;
186 self
187 }
188
189 pub fn color(mut self, color: bool) -> Self {
191 self.color = color;
192 self
193 }
194
195 pub fn service_name(mut self, name: impl Into<String>) -> Self {
197 self.service_name = name.into();
198 self
199 }
200
201 pub fn error_detail(mut self, detail: ErrorLogDetail) -> Self {
203 self.error_detail = detail;
204 self
205 }
206
207 pub fn request_logs(mut self, enabled: bool) -> Self {
209 self.request_logs = enabled;
210 self
211 }
212
213 pub fn include_source(mut self, include: bool) -> Self {
215 self.include_source = include;
216 self
217 }
218
219 pub fn include_thread_ids(mut self, include: bool) -> Self {
221 self.include_thread_ids = include;
222 self
223 }
224
225 pub fn non_blocking(mut self, non_blocking: bool) -> Self {
227 self.non_blocking = non_blocking;
228 self
229 }
230
231 pub fn file(mut self, file: FileLogConfig) -> Self {
233 self.file = Some(file);
234 self
235 }
236
237 pub fn telemetry(mut self, telemetry: TelemetryConfig) -> Self {
239 self.telemetry = Some(telemetry);
240 self
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn defaults_are_sensible() {
250 let config = LoggerConfig::new();
251 assert_eq!(config.level, "info");
252 assert_eq!(config.format, LogFormat::Auto);
253 assert!(config.color);
254 assert_eq!(config.error_detail, ErrorLogDetail::TypeOnly);
255 assert!(config.request_logs);
256 assert!(config.file.is_none());
257 assert!(config.telemetry.is_none());
258 }
259
260 #[test]
261 fn builders_set_fields() {
262 let config = LoggerConfig::new()
263 .level("debug")
264 .format(LogFormat::Json)
265 .service_name("tork-api")
266 .error_detail(ErrorLogDetail::FullChain)
267 .request_logs(false)
268 .include_source(true)
269 .include_thread_ids(true)
270 .non_blocking(true)
271 .file(
272 FileLogConfig::new("./logs")
273 .prefix("api")
274 .rotation(Rotation::Hourly),
275 );
276
277 assert_eq!(config.level, "debug");
278 assert_eq!(config.format, LogFormat::Json);
279 assert_eq!(config.service_name, "tork-api");
280 assert_eq!(config.error_detail, ErrorLogDetail::FullChain);
281 assert!(!config.request_logs);
282 assert!(config.include_source);
283 assert!(config.include_thread_ids);
284 assert!(config.non_blocking);
285 let file = config.file.expect("file sink");
286 assert_eq!(file.prefix, "api");
287 assert_eq!(file.rotation, Rotation::Hourly);
288 }
289
290 #[test]
291 fn file_and_telemetry_builders_cover_all_fields() {
292 let file = FileLogConfig::new("./logs")
293 .prefix("svc")
294 .rotation(Rotation::Never)
295 .non_blocking(false);
296 assert_eq!(file.directory, PathBuf::from("./logs"));
297 assert_eq!(file.prefix, "svc");
298 assert_eq!(file.rotation, Rotation::Never);
299 assert!(!file.non_blocking);
300
301 let telemetry = TelemetryConfig::new("http://localhost:4317")
302 .service_name("tork-api")
303 .enabled(false);
304 assert!(!telemetry.enabled);
305 assert_eq!(telemetry.otlp_endpoint, "http://localhost:4317");
306 assert_eq!(telemetry.service_name, "tork-api");
307 }
308
309 #[test]
310 fn log_format_and_rotation_deserialize_from_lowercase() {
311 let format: LogFormat = serde_json::from_str("\"json\"").unwrap();
312 assert_eq!(format, LogFormat::Json);
313 let format: LogFormat = serde_json::from_str("\"auto\"").unwrap();
314 assert_eq!(format, LogFormat::Auto);
315 let format: LogFormat = serde_json::from_str("\"pretty\"").unwrap();
316 assert_eq!(format, LogFormat::Pretty);
317 let format: LogFormat = serde_json::from_str("\"compact\"").unwrap();
318 assert_eq!(format, LogFormat::Compact);
319
320 let rotation: Rotation = serde_json::from_str("\"never\"").unwrap();
321 assert_eq!(rotation, Rotation::Never);
322 let rotation: Rotation = serde_json::from_str("\"hourly\"").unwrap();
323 assert_eq!(rotation, Rotation::Hourly);
324 let rotation: Rotation = serde_json::from_str("\"daily\"").unwrap();
325 assert_eq!(rotation, Rotation::Daily);
326 }
327}