Last active
January 25, 2026 00:21
-
-
Save ackkerman/4e7f995bf3114061135da4da0bb50154 to your computer and use it in GitHub Desktop.
Loggerスタイル
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #![allow(clippy::tabs_in_doc_comments)] | |
| #![warn(rust_2018_idioms, unreachable_pub)] | |
| #![forbid(elided_lifetimes_in_paths)] | |
| #![cfg_attr(all(doc, nightly), feature(doc_auto_cfg))] | |
| use anstyle::Style; | |
| use env_logger::fmt::Formatter; | |
| use log::{Level, Record}; | |
| #[cfg(feature = "custom-arg-formatter")] | |
| use once_cell::sync::OnceCell; | |
| use std::{ | |
| io, | |
| io::Write, | |
| sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering} | |
| }; | |
| static MAX_MODULE_LEN: AtomicUsize = AtomicUsize::new(0); | |
| static SHOW_MODULE: AtomicBool = AtomicBool::new(true); | |
| static SHOW_EMOJIS: AtomicBool = AtomicBool::new(true); | |
| #[cfg(feature = "time")] | |
| static SHOW_TIME: AtomicU8 = AtomicU8::new(TimestampPrecision::Seconds as u8); | |
| #[cfg(feature = "custom-arg-formatter")] | |
| static ARG_FORMATTER: OnceCell<Box<dyn ArgFormatter + Send + Sync>> = OnceCell::new(); | |
| pub use env_logger; | |
| #[repr(u8)] | |
| /// RFC3339 timestamps | |
| pub enum TimestampPrecision { | |
| Disable, | |
| Seconds, | |
| Millis, | |
| Micros, | |
| Nanos | |
| } | |
| /// Create a preconfigured builder, | |
| /// with same configuration like [`just_log()`]. | |
| /// | |
| /// For an unconfigurated bulider use [`env_logger::Builder::new()`] | |
| pub fn builder() -> env_logger::Builder { | |
| let mut builder = env_logger::Builder::new(); | |
| builder.filter_level(log::LevelFilter::Info) //set defaul log level | |
| .parse_default_env() | |
| .format(format); | |
| builder | |
| } | |
| ///create and regstier a logger from the default environment variables | |
| pub fn just_log() { | |
| builder().init(); | |
| } | |
| ///enable or disabel showing the module path | |
| pub fn show_module(show: bool) { | |
| SHOW_MODULE.store(show, Ordering::Relaxed); | |
| } | |
| ///enable or disabel showing emojis before the log level | |
| pub fn show_emoji(show: bool) { | |
| SHOW_EMOJIS.store(show, Ordering::Relaxed); | |
| } | |
| /// return the current module len and set the module length to the maximum of the current value and the given `len`. | |
| /// | |
| /// Usefull if you already know the length of module and would like to have an consistant indentation from the beginnig. | |
| pub fn get_set_max_module_len(len: usize) -> usize { | |
| let module_len = MAX_MODULE_LEN.load(Ordering::Relaxed); | |
| if module_len < len { | |
| MAX_MODULE_LEN.store(len, Ordering::Relaxed); | |
| } | |
| module_len | |
| } | |
| #[cfg(feature = "time")] | |
| /// set the timestamp precision or disable timestamps complete | |
| pub fn set_timestamp_precision(timestamp_precission: TimestampPrecision) { | |
| SHOW_TIME.store(timestamp_precission as u8, Ordering::Relaxed); | |
| } | |
| pub trait ArgFormatter { | |
| fn arg_format(&self, buf: &mut Formatter, record: &Record<'_>) -> io::Result<()>; | |
| } | |
| impl<F: Fn(&mut Formatter, &Record<'_>) -> io::Result<()>> ArgFormatter for F { | |
| fn arg_format(&self, buf: &mut Formatter, record: &Record<'_>) -> io::Result<()> { | |
| self(buf, record) | |
| } | |
| } | |
| /// Use a custom formater to format the args (the actual message) of the record . | |
| /// This function can only be called once and return an Error if called a second time. | |
| /// | |
| /// This example remove the private user ipv4 loggeg by `hickory` from the log, if the loglevel is below Debug. | |
| /// ``` | |
| /// use env_logger::fmt::Formatter; | |
| /// use log::{Level, Record}; | |
| /// use once_cell::sync::Lazy; | |
| /// use regex::Regex; | |
| /// use std::{io, io::Write}; | |
| /// | |
| /// static REGEX: Lazy<Regex> = Lazy::new(|| { | |
| /// // https://ihateregex.io/expr/ip/ | |
| /// Regex::new(r"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}") | |
| /// .unwrap() | |
| /// }); | |
| /// | |
| /// fn arg_format(buf: &mut Formatter, record: &Record<'_>) -> io::Result<()> { | |
| /// if let Some(mod_path) = record.module_path() { | |
| /// if log::max_level() < Level::Debug && mod_path.starts_with("hickory") { | |
| /// let message = format!("{}", record.args()); | |
| /// let message = REGEX.replace_all(&message, "RESTRAINED"); | |
| /// return writeln!(buf, "{}", message); | |
| /// } | |
| /// }; | |
| /// writeln!(buf, "{}", record.args()) | |
| /// } | |
| /// | |
| /// my_env_logger_style::set_arg_formatter(Box::new(arg_format)).unwrap(); | |
| /// ``` | |
| #[cfg(feature = "custom-arg-formatter")] | |
| pub fn set_arg_formatter( | |
| forrmatter: Box<dyn ArgFormatter + Send + Sync> | |
| ) -> Result<(), ()> { | |
| ARG_FORMATTER.set(forrmatter).map_err(|_| ()) | |
| } | |
| ///log formater witch can be used at the [`format()`](env_logger::Builder::format()) function of the [`env_logger::Builder`]. | |
| pub fn format(buf: &mut Formatter, record: &Record<'_>) -> io::Result<()> { | |
| let bold = Style::new().bold(); | |
| let dimmed = Style::new().dimmed(); | |
| #[cfg(feature = "time")] | |
| { | |
| let show_time = SHOW_TIME.load(Ordering::Relaxed); | |
| // safety: SHOW_TIME is inilized with TimestampPrecision::Seconds | |
| // and can only be written by using set_timestamp_precision() | |
| match unsafe { std::mem::transmute(show_time) } { | |
| TimestampPrecision::Disable => Ok(()), | |
| TimestampPrecision::Seconds => { | |
| write!(buf, "{dimmed}{}{dimmed:#} ", buf.timestamp_seconds()) | |
| }, | |
| TimestampPrecision::Millis => { | |
| write!(buf, "{dimmed}{}{dimmed:#} ", buf.timestamp_millis()) | |
| }, | |
| TimestampPrecision::Micros => { | |
| write!(buf, "{dimmed}{}{dimmed:#} ", buf.timestamp_micros()) | |
| }, | |
| TimestampPrecision::Nanos => { | |
| write!(buf, "{dimmed}{}{dimmed:#} ", buf.timestamp_nanos()) | |
| } | |
| }?; | |
| } | |
| let level_style = buf.default_level_style(record.level()); | |
| let level_symbol = if SHOW_EMOJIS.load(Ordering::Relaxed) { | |
| match record.level() { | |
| //💥 and 🔬 are 2 chars big at the terminal. How does it look with other fonts/terminals? | |
| Level::Trace => "🔬", | |
| Level::Debug => " ⚙️", | |
| Level::Info => " ℹ", | |
| Level::Warn => " ⚠", | |
| Level::Error => "💥" | |
| } | |
| } else { | |
| "" | |
| }; | |
| write!( | |
| buf, | |
| "{level_symbol} {level_style}{:5}{level_style:#} ", | |
| record.level() | |
| )?; | |
| if SHOW_MODULE.load(Ordering::Relaxed) { | |
| let module = record.module_path().unwrap_or_default(); | |
| let module_len = get_set_max_module_len(module.len()); | |
| write!( | |
| buf, | |
| "{dimmed}{module:module_len$}{dimmed:#} {bold}>{bold:#} " | |
| )?; | |
| } | |
| #[cfg(feature = "custom-arg-formatter")] | |
| if let Some(formatter) = ARG_FORMATTER.get() { | |
| return formatter.arg_format(buf, record); | |
| } | |
| writeln!(buf, "{}", record.args()) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "io" | |
| "log/slog" | |
| "os" | |
| "path/filepath" | |
| "runtime" | |
| "strconv" | |
| "strings" | |
| "time" | |
| ) | |
| const ( | |
| colorReset = "\x1b[0m" | |
| colorRed = "\x1b[31m" | |
| colorYellow = "\x1b[33m" | |
| colorGreen = "\x1b[32m" | |
| colorCyan = "\x1b[36m" | |
| colorWhite = "\x1b[37m" | |
| appTag = "application" | |
| ) | |
| func buildLogger() *slog.Logger { | |
| handler := newColorHandler(os.Stdout, &slog.HandlerOptions{ | |
| Level: slog.LevelInfo, | |
| AddSource: true, | |
| }) | |
| logger := slog.New(handler) | |
| slog.SetDefault(logger) | |
| return logger | |
| } | |
| type colorHandler struct { | |
| w io.Writer | |
| opts slog.HandlerOptions | |
| attrs []slog.Attr | |
| groups []string | |
| } | |
| func newColorHandler(w io.Writer, opts *slog.HandlerOptions) *colorHandler { | |
| var copied slog.HandlerOptions | |
| if opts != nil { | |
| copied = *opts | |
| } | |
| return &colorHandler{w: w, opts: copied} | |
| } | |
| func (h *colorHandler) Enabled(_ context.Context, level slog.Level) bool { | |
| if h.opts.Level == nil { | |
| return true | |
| } | |
| return level >= h.opts.Level.Level() | |
| } | |
| func (h *colorHandler) Handle(_ context.Context, r slog.Record) error { | |
| var b strings.Builder | |
| now := r.Time | |
| if now.IsZero() { | |
| now = time.Now() | |
| } | |
| timeText := now.Format("2006/01/02T15:04:05") | |
| levelLabel := strings.ToUpper(r.Level.String()) | |
| color := colorForLevel(r.Level) | |
| levelText := colorize(levelLabel, color) | |
| msgText := colorize(r.Message, color) | |
| sourceText := formatSource(r.PC) | |
| b.WriteString("[") | |
| b.WriteString(timeText) | |
| b.WriteString("] [") | |
| b.WriteString(appTag) | |
| b.WriteString("] [") | |
| b.WriteString(levelText) | |
| b.WriteString("] [") | |
| b.WriteString(sourceText) | |
| b.WriteString("] ") | |
| b.WriteString(msgText) | |
| attrs := make([]slog.Attr, 0, len(h.attrs)+8) | |
| if len(h.attrs) > 0 { | |
| attrs = append(attrs, h.attrs...) | |
| } | |
| r.Attrs(func(a slog.Attr) bool { | |
| attrs = append(attrs, a) | |
| return true | |
| }) | |
| if len(attrs) > 0 { | |
| pairs := formatAttrs(attrs, h.groups) | |
| if pairs != "" { | |
| b.WriteString(" ") | |
| b.WriteString(pairs) | |
| } | |
| } | |
| b.WriteString("\n") | |
| _, err := io.WriteString(h.w, b.String()) | |
| return err | |
| } | |
| func (h *colorHandler) WithAttrs(attrs []slog.Attr) slog.Handler { | |
| next := &colorHandler{ | |
| w: h.w, | |
| opts: h.opts, | |
| attrs: append([]slog.Attr{}, h.attrs...), | |
| groups: append([]string{}, h.groups...), | |
| } | |
| next.attrs = append(next.attrs, attrs...) | |
| return next | |
| } | |
| func (h *colorHandler) WithGroup(name string) slog.Handler { | |
| next := &colorHandler{ | |
| w: h.w, | |
| opts: h.opts, | |
| attrs: append([]slog.Attr{}, h.attrs...), | |
| groups: append([]string{}, h.groups...), | |
| } | |
| next.groups = append(next.groups, name) | |
| return next | |
| } | |
| func formatSource(pc uintptr) string { | |
| if pc == 0 { | |
| return "unknown" | |
| } | |
| fn := runtime.FuncForPC(pc) | |
| if fn == nil { | |
| return "unknown" | |
| } | |
| file, line := fn.FileLine(pc) | |
| file = filepath.Base(file) | |
| name := filepath.Base(fn.Name()) | |
| return fmt.Sprintf("%s:%d,%s", file, line, name) | |
| } | |
| func colorForLevel(level slog.Level) string { | |
| switch { | |
| case level <= slog.LevelDebug: | |
| return colorCyan | |
| case level >= slog.LevelError: | |
| return colorRed | |
| case level >= slog.LevelWarn: | |
| return colorYellow | |
| default: | |
| return colorGreen | |
| } | |
| } | |
| func colorize(text, color string) string { | |
| if text == "" { | |
| return text | |
| } | |
| return color + text + colorReset | |
| } | |
| func formatAttrs(attrs []slog.Attr, groups []string) string { | |
| var pairs []string | |
| prefix := strings.Join(groups, ".") | |
| appendAttrs(&pairs, prefix, attrs) | |
| return strings.Join(pairs, " ") | |
| } | |
| func appendAttrs(pairs *[]string, prefix string, attrs []slog.Attr) { | |
| for _, attr := range attrs { | |
| if attr.Equal(slog.Attr{}) { | |
| continue | |
| } | |
| key := attr.Key | |
| val := attr.Value | |
| if val.Kind() == slog.KindGroup { | |
| nextPrefix := joinPrefix(prefix, key) | |
| appendAttrs(pairs, nextPrefix, val.Group()) | |
| continue | |
| } | |
| key = joinPrefix(prefix, key) | |
| *pairs = append(*pairs, fmt.Sprintf("%s=%s", key, formatValue(val))) | |
| } | |
| } | |
| func joinPrefix(prefix, key string) string { | |
| if prefix == "" { | |
| return key | |
| } | |
| if key == "" { | |
| return prefix | |
| } | |
| return prefix + "." + key | |
| } | |
| func formatValue(val slog.Value) string { | |
| switch val.Kind() { | |
| case slog.KindString: | |
| return quoteIfNeeded(val.String()) | |
| case slog.KindBool: | |
| return strconv.FormatBool(val.Bool()) | |
| case slog.KindInt64: | |
| return strconv.FormatInt(val.Int64(), 10) | |
| case slog.KindUint64: | |
| return strconv.FormatUint(val.Uint64(), 10) | |
| case slog.KindFloat64: | |
| return strconv.FormatFloat(val.Float64(), 'f', -1, 64) | |
| case slog.KindDuration: | |
| return val.Duration().String() | |
| case slog.KindTime: | |
| return val.Time().Format(time.RFC3339) | |
| case slog.KindAny: | |
| if err, ok := val.Any().(error); ok && err != nil { | |
| return quoteIfNeeded(err.Error()) | |
| } | |
| return quoteIfNeeded(fmt.Sprint(val.Any())) | |
| default: | |
| return quoteIfNeeded(val.String()) | |
| } | |
| } | |
| func quoteIfNeeded(s string) string { | |
| if s == "" { | |
| return `""` | |
| } | |
| if strings.ContainsAny(s, " \t\n\"") { | |
| return strconv.Quote(s) | |
| } | |
| return s | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment