webdav_server/logger/mod.rs
1//! Structured, asynchronous telemetry for the Kosh server.
2//!
3//! This module provides the entire logging infrastructure used by the server at runtime.
4//! It is intentionally decoupled from the HTTP request path: the server emits log entries
5//! through an in-memory channel, and a dedicated background task handles all blocking
6//! I/O (disk writes, Unix socket broadcasts) independently.
7//!
8//! ## Architecture
9//!
10//! ```text
11//! Route Handler / Middleware
12//! │
13//! │ crate::info!(Module::Api, "...") ← zero-cost if logger is inactive
14//! │
15//! ▼
16//! GLOBAL_LOGGER (OnceLock<Sender<Entry>>)
17//! │
18//! │ mpsc channel (bounded, non-blocking try_send)
19//! │
20//! ▼
21//! Service::run() ← dedicated tokio task
22//! ├── writes bincode-serialized Entry to a rolling daily .bin file
23//! └── broadcasts the same bytes over a Unix Datagram Socket
24//! └── consumed by the Ratatui admin CLI (kosh-cli)
25//! ```
26//!
27//! ## Enabling the logger
28//!
29//! The logger is opt-in. Call [`Service::start`] on startup and store the returned
30//! sender in [`GLOBAL_LOGGER`]. If `GLOBAL_LOGGER` is never initialized, all macros
31//! (`info!`, `warn!`, `error!`, `fatal!`) become complete no-ops with zero overhead.
32//!
33//! ## Emitting log entries
34//!
35//! Use the crate-level macros exported from [`macro`](crate) rather than constructing
36//! [`Entry`] values manually:
37//!
38//! ```ignore
39//! info!(Module::Api, "user {} logged in", user_id);
40//! warn!(Module::Storage, "disk usage above 90%");
41//! error!(Module::Database, "query failed: {}", e);
42//! ```
43mod error;
44mod r#macro;
45mod service;
46
47use std::path::PathBuf;
48use std::sync::OnceLock;
49
50use bincode_next::{Decode, Encode};
51mod loggable;
52
53pub use loggable::Loggable;
54pub use service::{SOCKET_ADDR, format_date_time};
55
56/// The global channel sender used to submit log entries to the background logging service.
57///
58/// Initialized once on startup by calling [`Service::start`] and storing the returned sender
59/// here via `OnceLock::set`. Once set, this value is immutable for the lifetime of the process.
60///
61/// The sender is intentionally stored as an `OnceLock` rather than a separate `AtomicBool` flag
62/// so that there is a single source of truth for whether logging is active. Code that needs to
63/// check whether logging is enabled should call [`logging_enabled`] instead of reading this
64/// directly.
65pub static GLOBAL_LOGGER: OnceLock<Sender<Entry>> = OnceLock::new();
66
67/// Returns `true` if the logging service has been initialized and is currently active.
68///
69/// This is the canonical way to check whether logging is enabled before performing any
70/// work related to telemetry. It reads from [`GLOBAL_LOGGER`] and avoids the need for
71/// a separate `AtomicBool` flag.
72///
73/// Used in `IntoResponse` to decide whether to construct an [`Entry`] for the error
74/// telemetry middleware, and in `route_main` to decide whether to attach the logging
75/// middleware layer.
76#[inline]
77pub fn logging_enabled() -> bool {
78 GLOBAL_LOGGER.get().is_some()
79}
80
81/// A single structured log event emitted by the server.
82///
83/// `Entry` is the wire format for all telemetry in the system. It is serialized using
84/// `bincode` before being written to disk or broadcast over the Unix Datagram Socket.
85/// The layout is intentionally compact: `Module` and `Level` are stored as small integer
86/// enums, keeping each entry small for high-throughput workloads.
87///
88/// Entries are constructed by the logging macros (`info!`, `error!`, etc.) and by the
89/// error telemetry middleware in `api/middleware/log.rs`. They should not typically be
90/// constructed manually.
91#[derive(Encode, Decode, Clone)]
92pub struct Entry {
93 /// The subsystem that generated this log entry.
94 pub module: Module,
95 /// The severity level of this log entry.
96 pub level: Level,
97 /// The Unix epoch timestamp in milliseconds at which this entry was created.
98 ///
99 /// The logging service uses this value (not the wall clock) to determine which
100 /// daily log file to write the entry into, preventing incorrect file rotation
101 /// when entries are processed slightly after midnight due to channel queue lag.
102 pub timestamp_ms: i64,
103 /// The human-readable log message.
104 ///
105 /// For error telemetry entries produced by `IntoResponse`, this is the result of
106 /// calling `.to_string()` on the error, which uses the `Display` format defined
107 /// by `thiserror`.
108 pub message: String,
109}
110
111/// The severity level of a log entry.
112///
113/// Levels are assigned by each error type through the [`Loggable`] trait, which allows
114/// individual domain errors to decide their own severity without requiring a centralized
115/// `match` statement in the middleware.
116///
117/// The admin CLI (`kosh-cli`) uses these levels to apply color coding and filtering
118/// when rendering the log feed.
119#[derive(Debug, Encode, Decode, Clone, Copy, PartialEq, Eq)]
120pub enum Level {
121 /// Routine informational events: successful logins, uploads, health checks.
122 Info,
123 /// Events that are abnormal but non-fatal: client authentication failures,
124 /// request conflicts, or resource not found responses.
125 Warning,
126 /// Unexpected failures that affect a single request but do not crash the service:
127 /// database query errors, storage write failures.
128 Error,
129 /// Critical failures that indicate the server may be in an unrecoverable state:
130 /// vault directory missing, logger task crash.
131 Fatal,
132 /// A special sentinel level used to stop the logging service gracefully.
133 ///
134 /// When the service receives an entry with this level, it stops its receive loop
135 /// and allows the `LoggerHandler` to join cleanly. This is the "poison pill"
136 /// pattern used instead of relying on channel closure, because the sender lives
137 /// inside a `OnceLock` and is never dropped during normal operation.
138 Shutdown,
139}
140
141/// The server subsystem that produced a log entry.
142///
143/// Used by the admin CLI to filter or group log entries by origin. Each domain error
144/// type reports its module through the [`Loggable`] trait.
145#[derive(Debug, Encode, Decode, Clone, Copy, PartialEq, Eq)]
146pub enum Module {
147 /// HTTP layer: route handlers, middleware, request parsing.
148 Api,
149 /// Database layer: `sqlx` queries, migrations, model operations.
150 Database,
151 /// Core server lifecycle: startup, shutdown, TCP listener.
152 Server,
153 /// Asset domain: upload, download, delete, and ownership operations.
154 Asset,
155 /// Storage layer: CAS filesystem, file transactions, blob management.
156 Storage,
157 /// The logging service itself. Used for internal diagnostics such as
158 /// grace-period timeout warnings during shutdown.
159 Logger,
160}
161
162pub use service::Service;
163use tokio::sync::mpsc::Sender;
164
165#[must_use]
166pub fn path() -> Option<PathBuf> {
167 dirs::state_dir().map(|x| x.join("kosh").join("logs"))
168}