Skip to content

Instantly share code, notes, and snippets.

@ackkerman
Last active January 25, 2026 00:21
Show Gist options
  • Select an option

  • Save ackkerman/4e7f995bf3114061135da4da0bb50154 to your computer and use it in GitHub Desktop.

Select an option

Save ackkerman/4e7f995bf3114061135da4da0bb50154 to your computer and use it in GitHub Desktop.
Loggerスタイル
#![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())
}
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