Skip to main content

workflow_log/
log.rs

1use cfg_if::cfg_if;
2use std::fmt;
3
4cfg_if! {
5    if #[cfg(target_arch = "bpf")] {
6        pub use workflow_log::levels::{ Level, LevelFilter };
7    } else {
8        use std::sync::Arc;
9        pub use log::{ Level, LevelFilter };
10        use downcast::{ downcast_sync, AnySync };
11        use crate::hex as hexplay;
12        pub use crate::hex::HexViewBuilder;
13        pub use termcolor::Buffer;
14        //use core::ops::Range;
15
16        /// Builder wrapper around a [`HexViewBuilder`] that accumulates colored
17        /// ranges of a hex data dump and renders them via [`ColorHexView::try_print`].
18        pub struct ColorHexView<'a>{
19            /// The underlying hex view builder that ranges and colors are applied to.
20            pub builder: HexViewBuilder<'a>,
21            /// Running byte offset marking where the next sequentially-added color range begins.
22            pub color_start: usize
23        }
24        impl<'a> ColorHexView<'a>{
25            /// Creates a new view from a [`HexViewBuilder`] and a list of
26            /// `(color, length)` pairs applied sequentially from offset zero.
27            pub fn new(builder:HexViewBuilder<'a>, colors:Vec<(&'a str, usize)>)->Self{
28                Self{
29                    builder,
30                    color_start:0
31                }.add_colors(colors)
32            }
33
34            /// Appends `(color, length)` pairs, coloring consecutive byte ranges
35            /// starting at the current offset and advancing it by each length.
36            pub fn add_colors(mut self, colors:Vec<(&'a str, usize)>)->Self{
37                let mut builder = self.builder;
38                for (color, len) in colors{
39                    let end = self.color_start+len;
40                    let range = self.color_start..end;
41                    self.color_start = end;
42                    builder = builder.add_color(color, range);
43                }
44                self.builder = builder;
45                self
46            }
47
48            /// Applies `(color, range)` pairs, coloring each explicitly specified
49            /// byte range of the hex dump.
50            pub fn add_colors_with_range(mut self, colors:Vec<(&'a str, std::ops::Range<usize>)>)->Self{
51                let mut builder = self.builder;
52                for (color, range) in colors{
53                    builder = builder.add_color(color, range);
54                }
55                self.builder = builder;
56                self
57            }
58
59            /// Renders the colored hex view and emits it via [`log_trace`],
60            /// returning an error string if formatting or UTF-8 conversion fails.
61            pub fn try_print(self)->std::result::Result<(), String>{
62                let mut buf = Buffer::ansi();
63                match self.builder.finish().fmt(&mut buf){
64                    Ok(()) => {
65                        match String::from_utf8(buf.as_slice().to_vec()){
66                            Ok(str)=>{
67                                log_trace!("{}", str);
68                            }
69                            Err(_)=>{
70                                return Err("Unable to convert HexView to string".to_string());
71                            }
72                        }
73                    },
74                    Err(_) => {
75                        return Err("Unable to format HexView".to_string());
76                    }
77                }
78                Ok(())
79            }
80        }
81
82        /// A log sink trait that can be installed into the log subsystem using the [`pipe`]
83        /// function and will receive all log messages.
84        pub trait Sink : AnySync {
85            /// Receives a single log message; returns `true` to allow the message
86            /// to continue to the default console output, or `false` to consume it.
87            fn write(&self, target: Option<&str>, level : Level, args : &fmt::Arguments<'_>) -> bool;
88        }
89
90        #[allow(unused)]
91        struct SinkHandler {
92            // #[allow(dead_code)]
93            sink : Arc<dyn Sink>, // + Send + Sync + 'static>,
94        }
95
96        downcast_sync!(dyn Sink);
97    }
98}
99
100cfg_if! {
101    if #[cfg(target_arch = "bpf")] {
102        #[inline(always)]
103        pub fn log_level_enabled(_level: Level) -> bool {
104            true
105        }
106    } else if #[cfg(target_arch = "wasm32")] {
107        use wasm_bindgen::prelude::*;
108
109        static mut LEVEL_FILTER : LevelFilter = LevelFilter::Info;
110        #[inline(always)]
111        pub fn log_level_enabled(level: Level) -> bool {
112            unsafe { LEVEL_FILTER >= level }
113        }
114        pub fn set_log_level(level: LevelFilter) {
115            unsafe { LEVEL_FILTER = level };
116        }
117
118        #[wasm_bindgen]
119        extern "C" {
120            #[wasm_bindgen(typescript_type = r###""off" | "error" | "warn" | "info" | "debug" | "trace""###)]
121            #[derive(Debug)]
122            pub type LogLevelT;
123        }
124
125        #[doc="Set the logger log level using a string representation."]
126        #[doc="Available variants are: 'off', 'error', 'warn', 'info', 'debug', 'trace'"]
127        #[doc="@category General"]
128        #[wasm_bindgen(js_name = "setLogLevel")]
129        // pub fn set_log_level_str(level: &str) {
130        pub fn set_log_level_wasm(level: LogLevelT) {
131            if let Some(level) = level.as_string() {
132                let level = match level.as_str() {
133                    "off" => LevelFilter::Off,
134                    "error" => LevelFilter::Error,
135                    "warn" => LevelFilter::Warn,
136                    "info" => LevelFilter::Info,
137                    "debug" => LevelFilter::Debug,
138                    "trace" => LevelFilter::Trace,
139                    _ => panic!("Invalid log level: {level}"),
140                };
141                set_log_level(level);
142            } else {
143                panic!("log level must be a string, received: {level:?}");
144            }
145        }
146
147        cfg_if! {
148            if #[cfg(feature = "sink")] {
149                use std::sync::Mutex;
150                static SINK : Mutex<Option<SinkHandler>> = Mutex::new(None);
151                // pub fn pipe(sink : Arc<dyn Sink + Send + Sync + 'static>) {
152                pub fn pipe(sink : Option<Arc<dyn Sink>>) {
153                    match sink {
154                        Some(sink) => { *SINK.lock().unwrap() = Some(SinkHandler { sink }); },
155                        None => { *SINK.lock().unwrap() = None; }
156                    }
157                }
158                #[inline(always)]
159                fn to_sink(target: Option<&str>, level : Level, args : &fmt::Arguments<'_>) -> bool {
160                    match SINK.lock().unwrap().as_ref() {
161                        Some(handler) => {
162                            handler.sink.write(target, level, args)
163                        },
164                        None => { false }
165                    }
166                }
167            }
168        }
169
170    } else {
171        use std::sync::Mutex;
172
173        lazy_static::lazy_static! {
174            static ref LEVEL_FILTER : Mutex<LevelFilter> = Mutex::new(LevelFilter::Info);
175        }
176        #[inline(always)]
177        /// Returns true if the current log level is below the
178        /// currently set [`LevelFilter`]
179        pub fn log_level_enabled(level: Level) -> bool {
180            *LEVEL_FILTER.lock().unwrap() >= level
181        }
182        /// Enable filtering of log messages using the [`LevelFilter`]
183        pub fn set_log_level(level: LevelFilter) {
184            *LEVEL_FILTER.lock().unwrap() = level;
185        }
186        cfg_if! {
187            if #[cfg(feature = "sink")] {
188                lazy_static::lazy_static! {
189                    static ref SINK : Mutex<Option<SinkHandler>> = Mutex::new(None);
190                }
191                // pub fn pipe(sink : Option<Arc<dyn Sink + Send + Sync + 'static>>) {
192                /// Receives an Option with an `Arc`ed [`Sink`] trait reference
193                /// and installs it as a log sink / receiver.
194                /// The sink can be later disabled by invoking `pipe(None)`
195                pub fn pipe(sink : Option<Arc<dyn Sink>>) {
196                    match sink {
197                        Some(sink) => { *SINK.lock().unwrap() = Some(SinkHandler { sink }); },
198                        None => { *SINK.lock().unwrap() = None; }
199                    }
200
201                }
202                #[inline(always)]
203                fn to_sink(target : Option<&str>, level : Level, args : &fmt::Arguments<'_>) -> bool {
204                    match SINK.lock().unwrap().as_ref() {
205                        Some(handler) => {
206                            handler.sink.write(target, level, args)
207                        },
208                        None => { false }
209                    }
210                }
211            }
212        }
213
214        #[cfg(feature = "external-logger")]
215        mod workflow_logger {
216            use log::{ Level, LevelFilter, Record, Metadata, SetLoggerError };
217
218            pub struct WorkflowLogger;
219
220            impl log::Log for WorkflowLogger {
221                fn enabled(&self, metadata: &Metadata) -> bool {
222                    super::log_level_enabled(metadata.level())
223                }
224
225                fn log(&self, record: &Record) {
226                    if self.enabled(record.metadata()) {
227                        match record.metadata().level() {
228                            Level::Error => { super::error_impl(record.args()); },
229                            Level::Warn => { super::warn_impl(record.args()); },
230                            Level::Info => { super::info_impl(record.args()); },
231                            Level::Debug => { super::debug_impl(record.args()); },
232                            Level::Trace => { super::trace_impl(record.args()); },
233                        }
234                    }
235                }
236
237                fn flush(&self) {}
238            }
239
240            static LOGGER: WorkflowLogger = WorkflowLogger;
241
242            pub fn init() -> Result<(), SetLoggerError> {
243                log::set_logger(&LOGGER)
244                    .map(|()| log::set_max_level(LevelFilter::Trace))
245            }
246        }
247
248        #[cfg(feature = "external-logger")]
249        pub fn init() -> Result<(), log::SetLoggerError> {
250            workflow_logger::init()
251        }
252
253    }
254}
255
256#[cfg(target_arch = "wasm32")]
257pub mod wasm_log {
258    use wasm_bindgen::prelude::*;
259
260    #[wasm_bindgen]
261    extern "C" {
262        #[wasm_bindgen(js_namespace = console)]
263        pub fn log(s: &str);
264        #[wasm_bindgen(js_namespace = console)]
265        pub fn warn(s: &str);
266        #[wasm_bindgen(js_namespace = console)]
267        pub fn error(s: &str);
268    }
269}
270
271/// Backing implementation functions invoked by the `log_*!` macros to route
272/// messages to the sink, browser console, Solana log, or standard output.
273pub mod impls {
274    use super::*;
275
276    #[inline(always)]
277    #[allow(unused_variables)]
278    /// Emits a message at [`Level::Error`], honoring the current level filter and sink.
279    pub fn error_impl(target: Option<&str>, args: &fmt::Arguments<'_>) {
280        if log_level_enabled(Level::Error) {
281            #[cfg(all(not(target_arch = "bpf"), feature = "sink"))]
282            {
283                if to_sink(target, Level::Error, args) {
284                    return;
285                }
286            }
287            cfg_if! {
288                if #[cfg(target_arch = "wasm32")] {
289                    workflow_log::wasm_log::error(&args.to_string());
290                } else if #[cfg(target_arch = "bpf")] {
291                    solana_program::log::sol_log(&args.to_string());
292                } else {
293                    println!("{args}");
294                }
295            }
296        }
297    }
298
299    #[inline(always)]
300    #[allow(unused_variables)]
301    /// Emits a message at [`Level::Warn`], honoring the current level filter and sink.
302    pub fn warn_impl(target: Option<&str>, args: &fmt::Arguments<'_>) {
303        if log_level_enabled(Level::Warn) {
304            #[cfg(all(not(target_arch = "bpf"), feature = "sink"))]
305            {
306                if to_sink(target, Level::Warn, args) {
307                    return;
308                }
309            }
310            cfg_if! {
311                if #[cfg(target_arch = "wasm32")] {
312                    workflow_log::wasm_log::warn(&args.to_string());
313                } else if #[cfg(target_arch = "bpf")] {
314                    solana_program::log::sol_log(&args.to_string());
315                } else {
316                    println!("{args}");
317                }
318            }
319        }
320    }
321
322    #[inline(always)]
323    #[allow(unused_variables)]
324    /// Emits a message at [`Level::Info`], honoring the current level filter and sink.
325    pub fn info_impl(target: Option<&str>, args: &fmt::Arguments<'_>) {
326        if log_level_enabled(Level::Info) {
327            #[cfg(all(not(target_arch = "bpf"), feature = "sink"))]
328            {
329                if to_sink(target, Level::Info, args) {
330                    return;
331                }
332            }
333            cfg_if! {
334                if #[cfg(target_arch = "wasm32")] {
335                    workflow_log::wasm_log::log(&args.to_string());
336                } else if #[cfg(target_arch = "bpf")] {
337                    solana_program::log::sol_log(&args.to_string());
338                } else {
339                    println!("{args}");
340                }
341            }
342        }
343    }
344
345    #[inline(always)]
346    #[allow(unused_variables)]
347    /// Emits a message at [`Level::Debug`], honoring the current level filter and sink.
348    pub fn debug_impl(target: Option<&str>, args: &fmt::Arguments<'_>) {
349        if log_level_enabled(Level::Debug) {
350            #[cfg(all(not(target_arch = "bpf"), feature = "sink"))]
351            {
352                if to_sink(target, Level::Debug, args) {
353                    return;
354                }
355            }
356            cfg_if! {
357                if #[cfg(target_arch = "wasm32")] {
358                    workflow_log::wasm_log::log(&args.to_string());
359                } else if #[cfg(target_arch = "bpf")] {
360                    solana_program::log::sol_log(&args.to_string());
361                } else {
362                    println!("{args}");
363                }
364            }
365        }
366    }
367
368    #[inline(always)]
369    #[allow(unused_variables)]
370    /// Emits a message at [`Level::Trace`], honoring the current level filter and sink.
371    pub fn trace_impl(target: Option<&str>, args: &fmt::Arguments<'_>) {
372        if log_level_enabled(Level::Trace) {
373            #[cfg(all(not(target_arch = "bpf"), feature = "sink"))]
374            {
375                if to_sink(target, Level::Trace, args) {
376                    return;
377                }
378            }
379            cfg_if! {
380                if #[cfg(target_arch = "wasm32")] {
381                    workflow_log::wasm_log::log(&args.to_string());
382                } else if #[cfg(target_arch = "bpf")] {
383                    solana_program::log::sol_log(&args.to_string());
384                } else {
385                    println!("{args}");
386                }
387            }
388        }
389    }
390}
391
392/// Format and log message with [`Level::Error`]
393#[macro_export]
394macro_rules! log_error {
395    (target: $target:expr_2021, $($arg:tt)+) => (
396        workflow_log::impls::error_impl(Some($target),&format_args!($($t)*))
397    );
398
399    ($($t:tt)*) => (
400        workflow_log::impls::error_impl(None,&format_args!($($t)*))
401    )
402}
403
404/// Format and log message with [`Level::Warn`]
405#[macro_export]
406macro_rules! log_warn {
407    (target: $target:expr_2021, $($arg:tt)+) => (
408        workflow_log::impls::warn_impl(Some($target),&format_args!($($t)*))
409    );
410
411    ($($t:tt)*) => (
412        workflow_log::impls::warn_impl(None,&format_args!($($t)*))
413    )
414}
415
416/// Format and log message with [`Level::Info`]
417#[macro_export]
418macro_rules! log_info {
419    (target: $target:expr_2021, $($arg:tt)+) => (
420        workflow_log::impls::info_impl(Some($target),&format_args!($($t)*))
421    );
422
423    ($($t:tt)*) => (
424        workflow_log::impls::info_impl(None,&format_args!($($t)*))
425    )
426}
427
428/// Format and log message with [`Level::Debug`]
429#[macro_export]
430macro_rules! log_debug {
431    (target: $target:expr_2021, $($arg:tt)+) => (
432        workflow_log::impls::debug_impl(Some($target),&format_args!($($t)*))
433    );
434
435    ($($t:tt)*) => (
436        workflow_log::impls::debug_impl(None,&format_args!($($t)*))
437    )
438}
439
440/// Format and log message with [`Level::Trace`]
441#[macro_export]
442macro_rules! log_trace {
443    (target: $target:expr_2021, $($arg:tt)+) => (
444        workflow_log::impls::trace_impl(Some($target),&format_args!($($t)*))
445    );
446
447    ($($t:tt)*) => (
448        workflow_log::impls::trace_impl(None,&format_args!($($t)*))
449    )
450}
451
452pub use log_debug;
453pub use log_error;
454pub use log_info;
455pub use log_trace;
456pub use log_warn;
457
458/// Prints (using [`log_trace`]) a data slice
459/// formatted as a hex data dump.
460#[cfg(not(target_arch = "bpf"))]
461pub fn trace_hex(data: &[u8]) {
462    let hex = format_hex(data);
463    log_trace!("{}", hex);
464}
465
466/// Returns a string formatted as a hex data dump
467/// of the supplied slice argument.
468#[cfg(not(target_arch = "bpf"))]
469pub fn format_hex(data: &[u8]) -> String {
470    let view = hexplay::HexViewBuilder::new(data)
471        .address_offset(0)
472        .row_width(16)
473        .finish();
474
475    format!("{view}")
476}
477
478/// Formats a hex data dump to contain color ranges
479#[cfg(not(target_arch = "bpf"))]
480pub fn format_hex_with_colors<'a>(
481    data: &'a [u8],
482    colors: Vec<(&'a str, usize)>,
483) -> ColorHexView<'a> {
484    let view_builder = hexplay::HexViewBuilder::new(data)
485        .address_offset(0)
486        .row_width(16);
487
488    ColorHexView::new(view_builder, colors)
489}
490#[cfg(not(target_arch = "bpf"))]
491/// Trait helpers for emitting a type's binary data as a colorized hex dump.
492pub mod color_log {
493    use super::*;
494    type Index = usize;
495    type Length = usize;
496    type Color<'a> = &'a str;
497    type Result<T> = std::result::Result<T, String>;
498    /// Implemented by types that can render themselves as a colorized hex dump
499    /// via [`log_trace`].
500    pub trait ColoLogTrace {
501        /// Returns the raw bytes to be rendered as a hex dump.
502        fn log_data(&self) -> Vec<u8>;
503        /// Returns optional `(index, length, color)` tuples describing colored
504        /// byte ranges; defaults to `None` for an uncolored dump.
505        fn log_index_length_color(&self) -> Option<Vec<(Index, Length, Color<'_>)>> {
506            None
507        }
508
509        /// Renders the data (with any specified colors) as a hex dump and emits
510        /// it via [`log_trace`], falling back to an uncolored dump on failure.
511        fn log_trace(&self) -> Result<bool> {
512            let data_vec = self.log_data();
513            let mut view = format_hex_with_colors(&data_vec, vec![]);
514            if let Some(index_length_color) = self.log_index_length_color() {
515                let mut colors = Vec::new();
516                for (index, length, color) in index_length_color {
517                    colors.push((color, index..index + length));
518                }
519                view = view.add_colors_with_range(colors);
520            }
521
522            if view.try_print().is_err() {
523                trace_hex(&data_vec);
524                return Ok(false);
525            }
526            Ok(true)
527        }
528    }
529}
530
531#[cfg(not(target_arch = "bpf"))]
532pub use color_log::*;