Skip to main content

hyperion_framework/logging/
logging_service.rs

1// -------------------------------------------------------------------------------------------------
2// Hyperion Framework
3// https://github.com/robert-hannah/hyperion-framework
4//
5// A lightweight component-based TCP framework for building service-oriented Rust applications with
6// CLI control, async messaging, and lifecycle management.
7//
8// Copyright 2025 Robert Hannah
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21// -------------------------------------------------------------------------------------------------
22
23// Package
24use chrono::Utc;
25use colored::*;
26use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};
27
28// Define a simple logger that will handle logging messages with colours
29pub struct LoggingService {
30    pub min_log_level: LevelFilter,
31}
32
33impl log::Log for LoggingService {
34    fn enabled(&self, metadata: &Metadata) -> bool {
35        metadata.level().to_level_filter() <= self.min_log_level
36    }
37
38    fn log(&self, record: &Record) {
39        if self.enabled(record.metadata()) {
40            let level = match record.level() {
41                Level::Error => format!("{:<5}", "ERROR").red(),
42                Level::Warn => format!("{:<5}", "WARN").yellow(),
43                Level::Info => format!("{:<5}", "INFO").green(),
44                Level::Debug => format!("{:<5}", "DEBUG").bright_cyan(),
45                Level::Trace => format!("{:<5}", "TRACE").blue(),
46            };
47
48            let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S");
49
50            let module = record
51                .module_path()
52                .and_then(|m| m.split("::").last())
53                .unwrap_or("unknown");
54
55            println!("[{timestamp}] {level} {module:<20} - {}", record.args());
56        }
57    }
58
59    fn flush(&self) {}
60}
61
62// Function to initialise the logger - not part of the struct
63pub fn initialise_logger(log_level: LevelFilter) -> Result<(), SetLoggerError> {
64    let logger = LoggingService {
65        min_log_level: log_level,
66    };
67
68    // Set the logger as the global logger
69    log::set_max_level(logger.min_log_level);
70    log::set_boxed_logger(Box::new(logger))?;
71    Ok(())
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use log::{Level, Log};
78
79    fn make_service(level: LevelFilter) -> LoggingService {
80        LoggingService {
81            min_log_level: level,
82        }
83    }
84
85    #[test]
86    fn enabled_passes_at_or_above_min_level() {
87        let svc = make_service(LevelFilter::Info);
88        assert!(
89            svc.enabled(
90                &log::Metadata::builder()
91                    .level(Level::Error)
92                    .target("t")
93                    .build()
94            )
95        );
96        assert!(
97            svc.enabled(
98                &log::Metadata::builder()
99                    .level(Level::Warn)
100                    .target("t")
101                    .build()
102            )
103        );
104        assert!(
105            svc.enabled(
106                &log::Metadata::builder()
107                    .level(Level::Info)
108                    .target("t")
109                    .build()
110            )
111        );
112    }
113
114    #[test]
115    fn enabled_filters_below_min_level() {
116        let svc = make_service(LevelFilter::Info);
117        assert!(
118            !svc.enabled(
119                &log::Metadata::builder()
120                    .level(Level::Debug)
121                    .target("t")
122                    .build()
123            )
124        );
125        assert!(
126            !svc.enabled(
127                &log::Metadata::builder()
128                    .level(Level::Trace)
129                    .target("t")
130                    .build()
131            )
132        );
133    }
134
135    #[test]
136    fn log_does_not_panic_for_all_levels() {
137        let svc = make_service(LevelFilter::Trace);
138        // format_args! temporaries must be inlined — they cannot be stored in a let binding.
139        svc.log(
140            &log::Record::builder()
141                .level(Level::Error)
142                .target("t")
143                .module_path(Some("test::module"))
144                .args(format_args!("error message"))
145                .build(),
146        );
147        svc.log(
148            &log::Record::builder()
149                .level(Level::Warn)
150                .target("t")
151                .module_path(Some("test::module"))
152                .args(format_args!("warn message"))
153                .build(),
154        );
155        svc.log(
156            &log::Record::builder()
157                .level(Level::Info)
158                .target("t")
159                .module_path(Some("test::module"))
160                .args(format_args!("info message"))
161                .build(),
162        );
163        svc.log(
164            &log::Record::builder()
165                .level(Level::Debug)
166                .target("t")
167                .module_path(Some("test::module"))
168                .args(format_args!("debug message"))
169                .build(),
170        );
171        svc.log(
172            &log::Record::builder()
173                .level(Level::Trace)
174                .target("t")
175                .module_path(Some("test::module"))
176                .args(format_args!("trace message"))
177                .build(),
178        );
179    }
180
181    #[test]
182    fn log_silently_skips_filtered_levels() {
183        let svc = make_service(LevelFilter::Error);
184        svc.log(
185            &log::Record::builder()
186                .level(Level::Trace)
187                .target("t")
188                .args(format_args!("should be skipped"))
189                .build(),
190        ); // Must not panic
191    }
192
193    #[test]
194    fn log_handles_missing_module_path() {
195        let svc = make_service(LevelFilter::Trace);
196        svc.log(
197            &log::Record::builder()
198                .level(Level::Info)
199                .target("t")
200                .module_path(None)
201                .args(format_args!("no module path"))
202                .build(),
203        ); // Should fall back to "unknown"
204    }
205}