http_wasm_guest/
host_logger.rs1use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
2use std::io::Write;
3
4use crate::{host, memory};
5
6static LOGGER: HostLogger = HostLogger;
7const TRUNC_MARKER: &[u8] = b"... [truncated]";
8
9pub struct HostLogger;
14
15impl Log for HostLogger {
16 #[inline]
17 fn enabled(&self, metadata: &Metadata) -> bool {
18 metadata.level() <= log::max_level()
19 }
20
21 fn log(&self, record: &Record) {
22 if self.enabled(record.metadata()) {
23 memory::with_buffer(|buf| {
24 let written = format_log_message(buf, record.args());
25 host::log::write(host_level(record.metadata()), buf.as_subslice(written));
26 });
27 }
28 }
29
30 fn flush(&self) {}
31}
32
33fn format_log_message(buf: &mut memory::Buffer, args: &std::fmt::Arguments) -> usize {
36 let capacity = buf.capacity();
37 let mut slice = buf.as_mut_slice();
38 match write!(slice, "{}", args) {
39 Ok(()) => capacity - slice.len(),
40 Err(_) => {
41 let start = capacity - TRUNC_MARKER.len();
42 let slice = buf.as_mut_slice();
43 slice[start..].copy_from_slice(TRUNC_MARKER);
44 buf.capacity()
45 }
46 }
47}
48
49impl HostLogger {
50 #[inline]
54 pub fn init() -> Result<(), SetLoggerError> {
55 HostLogger::init_with_level(Level::Info)
56 }
57
58 #[inline]
62 pub fn init_with_level(level: Level) -> Result<(), SetLoggerError> {
63 log::set_max_level(max_level(level.to_level_filter()));
64 log::set_logger(&LOGGER)
65 }
66}
67
68fn max_level(level_filter: LevelFilter) -> LevelFilter {
72 max_level_with(level_filter, |level| host::log::enabled(map_to_host(level)))
73}
74
75fn max_level_with(mut level_filter: LevelFilter, is_enabled: impl Fn(Level) -> bool) -> LevelFilter {
77 while let Some(level) = level_filter.to_level() {
78 if is_enabled(level) {
79 return level_filter;
80 }
81 level_filter = level_filter.decrement_severity();
82 }
83 LevelFilter::Off
84}
85
86fn map_to_host(level: Level) -> i32 {
91 match level {
92 Level::Error => 2,
93 Level::Warn => 1,
94 Level::Info => 0,
95 Level::Debug => -1,
96 Level::Trace => -2,
97 }
98}
99
100fn host_level(md: &Metadata) -> i32 {
101 map_to_host(md.level())
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn test_init_with_level() {
110 let _result = HostLogger::init();
113 }
116
117 #[test]
118 fn map_level_to_host() {
119 assert_eq!(map_to_host(Level::Error), 2);
120 assert_eq!(map_to_host(Level::Warn), 1);
121 assert_eq!(map_to_host(Level::Info), 0);
122 assert_eq!(map_to_host(Level::Debug), -1);
123 assert_eq!(map_to_host(Level::Trace), -2);
124 }
125
126 #[test]
127 fn test_log_truncation_marker() {
128 let long_msg = "A".repeat(3000);
130 memory::with_buffer(|buf| {
131 let written = super::format_log_message(buf, &format_args!("{}", long_msg));
132 let slice = buf.as_subslice(written);
133 assert_eq!(slice.len(), buf.capacity(), "Truncated log should fill the buffer");
134 assert!(slice.ends_with(TRUNC_MARKER), "Log message should end with truncation marker");
135 });
136 }
137
138 #[test]
139 fn test_format_log_message() {
140 let msg = "Test";
141 memory::with_buffer(|buf| {
142 let written = super::format_log_message(buf, &format_args!("{}", msg));
143 assert_eq!(written, msg.len(), "message should not be truncated");
144 assert_eq!(buf.as_subslice(written), msg.as_bytes());
145 });
146 }
147
148 #[test]
149 fn test_format_log_message_limit() {
150 let msg = "A".repeat(2048);
151 memory::with_buffer(|buf| {
152 let written = super::format_log_message(buf, &format_args!("{}", msg));
153 assert_eq!(written, msg.len(), "message should not be truncated");
154 assert_eq!(buf.as_subslice(written), msg.as_bytes());
155 });
156 }
157 #[test]
158 fn host_logger_enabled_within_max_level() {
159 log::set_max_level(LevelFilter::Info);
161 let metadata = log::Metadata::builder().level(Level::Info).target("test").build();
162 assert!(LOGGER.enabled(&metadata));
163 }
164
165 #[test]
166 fn host_logger_enabled_below_max_level() {
167 log::set_max_level(LevelFilter::Info);
168 let metadata = log::Metadata::builder().level(Level::Error).target("test").build();
169 assert!(LOGGER.enabled(&metadata));
171 }
172
173 #[test]
174 fn host_logger_disabled_above_max_level() {
175 log::set_max_level(LevelFilter::Warn);
176 let metadata = log::Metadata::builder().level(Level::Debug).target("test").build();
177 assert!(!LOGGER.enabled(&metadata));
179 }
180
181 #[test]
182 fn host_logger_flush() {
183 LOGGER.flush();
185 }
186
187 #[test]
188 fn test_max_level_enabled() {
189 let level = max_level(LevelFilter::Info);
191 assert_eq!(level, LevelFilter::Info);
193 }
194
195 #[test]
196 fn test_max_level_off_stays_off() {
197 assert_eq!(max_level(LevelFilter::Off), LevelFilter::Off);
199 }
200
201 #[test]
202 fn test_max_level_returns_off_when_host_disables_everything() {
203 let level = max_level_with(LevelFilter::Trace, |_| false);
204 assert_eq!(level, LevelFilter::Off);
205 }
206
207 #[test]
208 fn test_max_level_stops_at_first_enabled_level() {
209 let level = max_level_with(LevelFilter::Trace, |level| matches!(level, Level::Warn | Level::Error));
210 assert_eq!(level, LevelFilter::Warn);
211 }
212
213 #[test]
214 fn host_logger_log_direct_call() {
215 log::set_max_level(LevelFilter::Info);
217
218 let record = log::Record::builder().level(Level::Info).target("test").args(format_args!("direct log test")).build();
220
221 LOGGER.log(&record);
223 }
224
225 #[test]
226 fn host_logger_log_skips_disabled_level() {
227 log::set_max_level(LevelFilter::Error);
229
230 let record =
232 log::Record::builder().level(Level::Debug).target("test").args(format_args!("this should be skipped")).build();
233
234 LOGGER.log(&record);
236 }
237
238 #[test]
239 fn test_max_level_decrement_until_enabled() {
240 log::set_max_level(LevelFilter::Warn);
242 let result = max_level(LevelFilter::Warn);
244 assert_eq!(result, LevelFilter::Warn, "max_level should decrement to Warn when only Warn is enabled on host");
245 }
246}