BazerUtils.jl

Assorted Julia utilities including custom logging
Log | Files | Refs | README | LICENSE

commit ae86651cd3c0bfb15f5475109bb0b6155cd6a12d
parent 745bc554fe070d08001219e7c24b052672fe7acd
Author: Erik Loualiche <[email protected]>
Date:   Sun, 15 Feb 2026 22:54:24 -0600

fix custom logger bugs, clean up imports, loosen compat bounds

CustomLogger fixes:
- Fix module_specific_message_filter using wrong filter function
  (was identical to absolute filter, making filtered_modules_specific a no-op)
- Fix isdir() called on Vector{String} instead of individual dirs
- Fix get_log_filenames hardcoding repeat length to 4
- Fix reduce(&, []) crash when no module filters provided
- Merge duplicate create_absolute_filter/create_specific_filter into
  single create_module_filter
- Rename msg_to_singline -> msg_to_singleline (typo)
- Remove commented-out old demux_logger block
- Rewrite docstring with accurate params and example

Other:
- Remove unused imports (AbstractLogger, FileLogger, TransformerLogger,
  ConsoleLogger)
- Loosen compat bounds to semver ranges, drop stdlib compat entries
- Remove duplicate deploydocs call and hardcoded version in docs/make.jl
- Update README, index.md, logger_guide.md, internals.md

Co-Authored-By: Claude Opus 4.6 <[email protected]>

Diffstat:
MProject.toml | 12+++++-------
MREADME.md | 16+++++++++++-----
Mdocs/make.jl | 14+-------------
Mdocs/src/index.md | 20+++++++++++++++++++-
Mdocs/src/lib/internals.md | 1+
Mdocs/src/man/logger_guide.md | 12+++---------
Msrc/BazerUtils.jl | 5++---
Msrc/CustomLogger.jl | 146++++++++++++++++++++++++++++++-------------------------------------------------
8 files changed, 98 insertions(+), 128 deletions(-)

diff --git a/Project.toml b/Project.toml @@ -12,13 +12,11 @@ LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] -CodecZlib = "0.7.8" -Dates = "1.11.0" -JSON3 = "1.14.3" -Logging = "1.11.0" -LoggingExtras = "1.1.0" -Tables = "1.12.1" -julia = "1.6.7" +CodecZlib = "0.7" +JSON3 = "1.14" +LoggingExtras = "1" +Tables = "1.12" +julia = "1.10" [extras] HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" diff --git a/README.md b/README.md @@ -11,10 +11,10 @@ It is a more mature version of [`Prototypes.jl`](https://github.com/louloulibs/Prototypes.jl) where I try a bunch of things out (there is overlap). -So far the package provides a two sets of functions: +The package provides: - - [`custom_logger`](#custom-logging) is a custom logging output that builds on the standard julia logger - - [`read_jsonl`](#json-lines) provides utilities to read and write json-lines files + - [`custom_logger`](#custom-logging): configurable logging with per-level file output, module filtering, and multiple format options (`pretty`, `log4j`, `syslog`) + - ~~`read_jsonl` / `stream_jsonl` / `write_jsonl`~~: **deprecated** — use [`JSON.jl`](https://github.com/JuliaIO/JSON.jl) v1 with `jsonlines=true` instead ## Installation @@ -63,9 +63,15 @@ custom_logger( ``` -### JSON Lines +### JSON Lines (deprecated) -A easy way to read json lines files into julia leaning on `JSON3` reader. +The JSONL functions (`read_jsonl`, `stream_jsonl`, `write_jsonl`) are deprecated. +Use [`JSON.jl`](https://github.com/JuliaIO/JSON.jl) v1 instead: +```julia +using JSON +data = JSON.parse("data.jsonl"; jsonlines=true) # read +JSON.json("out.jsonl", data; jsonlines=true) # write +``` ## Other stuff diff --git a/docs/make.jl b/docs/make.jl @@ -41,21 +41,9 @@ makedocs( ) -deploydocs( - repo="github.com/LouLouLibs/BazerUtils.jl", - target = "build", -) - deploydocs(; repo="github.com/LouLouLibs/BazerUtils.jl", target = "build", branch = "gh-pages", - devbranch = "main", # or "master" - versions = [ - "stable" => "0.8.2", - "dev" => "dev", - ], + devbranch = "main", ) - - -# -------------------------------------------------------------------------------------------------- diff --git a/docs/src/index.md b/docs/src/index.md @@ -1,4 +1,22 @@ # BazerUtils.jl -Utility functions for everyday julia. +Utility functions for everyday Julia. + +## Features + +- **[Custom Logger](@ref Logging)**: Configurable logging with per-level file output, module filtering, and multiple format options (`pretty`, `log4j`, `syslog`). +- **[JSON Lines](@ref "Working with JSON Lines Files")** *(deprecated)*: Read/write JSONL files. Use [`JSON.jl`](https://github.com/JuliaIO/JSON.jl) v1 with `jsonlines=true` instead. + +## Installation + +```julia +using Pkg +pkg"registry add https://github.com/LouLouLibs/loulouJL.git" +Pkg.add("BazerUtils") +``` + +Or directly from GitHub: +```julia +Pkg.add(url="https://github.com/LouLouLibs/BazerUtils.jl") +``` diff --git a/docs/src/lib/internals.md b/docs/src/lib/internals.md @@ -5,4 +5,5 @@ ```@autodocs Modules = [BazerUtils] Public = false +Order = [:function, :type] ``` diff --git a/docs/src/man/logger_guide.md b/docs/src/man/logger_guide.md @@ -106,16 +106,10 @@ You can use the same logger option with the `overwrite=false` option: ## Other -For `log4j` I do modify the message string to fit on one line. -You will find that the "\n" is now replaced by " | "; I guess I could have an option for which character delimitates lines, but this seems too fussy. - -I am trying to have a path shortener that would allow to reduce the path of the function to a fixed size. -The cost is that path will no longer be "clickable" but we would keep things tidy as messages will all start at the same column. -(see the `shorten_path_str` function). - - - +For `log4j` the message string is modified to fit on one line: `\n` is replaced by ` | `. +There is also a path shortener (`shorten_path_str`) that reduces file paths to a fixed size. +The cost is that paths will no longer be clickable, but log messages will start at the same column. diff --git a/src/BazerUtils.jl b/src/BazerUtils.jl @@ -3,9 +3,8 @@ module BazerUtils # -------------------------------------------------------------------------------------------------- import Dates: format, now, Dates, ISODateTimeFormat -import Logging: global_logger, Logging, Logging.Debug, Logging.Info, Logging.Warn, AbstractLogger -import LoggingExtras: ConsoleLogger, EarlyFilteredLogger, FileLogger, FormatLogger, - MinLevelLogger, TeeLogger, TransformerLogger +import Logging: global_logger, Logging, Logging.Debug, Logging.Info, Logging.Warn +import LoggingExtras: EarlyFilteredLogger, FormatLogger, MinLevelLogger, TeeLogger import JSON3: JSON3 import Tables: Tables import CodecZlib: CodecZlib diff --git a/src/CustomLogger.jl b/src/CustomLogger.jl @@ -25,7 +25,7 @@ function get_log_filenames(filename::AbstractString; # files = ["$(filename)_error.log", "$(filename)_warn.log", # "$(filename)_info.log", "$(filename)_debug.log"] else - files = repeat([filename], 4) + files = repeat([filename], length(file_loggers)) end return files end @@ -72,29 +72,35 @@ end """ custom_logger(filename; kw...) +Set up a custom global logger with per-level file output, module filtering, and configurable formatting. + +When `create_log_files=true`, creates one log file per level (e.g. `filename_error.log`, `filename_warn.log`, etc.). +Otherwise all levels write to the same file. + # Arguments - `filename::AbstractString`: base name for the log files -- `output_dir::AbstractString=./log/`: name of directory where log files are written -- `filtered_modules_specific::Vector{Symbol}=nothing`: which modules do you want to filter out of logging (only for info and stdout) - Some packages just write too much log ... filter them out but still be able to check them out in other logs -- `filtered_modules_all::Vector{Symbol}=nothing`: which modules do you want to filter out of logging (across all logs) - Examples could be TranscodingStreams (noticed that it writes so much to logs that it sometimes slows down I/O) -- `file_loggers::Union{Symbol, Vector{Symbol}}=[:error, :warn, :info, :debug]`: which file logger to register -- `log_date_format::AbstractString="yyyy-mm-dd"`: time stamp format at beginning of each logged lines for dates -- `log_time_format::AbstractString="HH:MM:SS"`: time stamp format at beginning of each logged lines for times -- `displaysize::Tuple{Int,Int}=(50,100)`: how much to show on log (same for all logs for now!) -- `log_format::Symbol=:log4j`: how to format the log files; I have added an option for pretty (all or nothing for now) -- `log_format_stdout::Symbol=:pretty`: how to format the stdout; default is pretty -- `overwrite::Bool=false`: do we overwrite previously created log files - -The custom_logger function creates four files in `output_dir` for four different levels of logging: - from least to most verbose: `filename.info.log.jl`, `filename.warn.log.jl`, `filename.debug.log.jl`, `filename.full.log.jl` -The debug logging offers the option to filter messages from specific packages (some packages are particularly verbose) using the `filter` optional argument -The full logging gets all of the debug without any of the filters. -Info and warn log the standard info and warning level logging messages. - -Note that the default **overwrites** old log files (specify overwrite=false to avoid this). - +- `filtered_modules_specific::Union{Nothing, Vector{Symbol}}=nothing`: modules to filter out of stdout and info-level file logs only (e.g. `[:TranscodingStreams]`) +- `filtered_modules_all::Union{Nothing, Vector{Symbol}}=nothing`: modules to filter out of all logs (e.g. `[:HTTP]`) +- `file_loggers::Union{Symbol, Vector{Symbol}}=[:error, :warn, :info, :debug]`: which file loggers to register +- `log_date_format::AbstractString="yyyy-mm-dd"`: date format in log timestamps +- `log_time_format::AbstractString="HH:MM:SS"`: time format in log timestamps +- `displaysize::Tuple{Int,Int}=(50,100)`: display size for non-string log messages +- `log_format::Symbol=:log4j`: format for file logs (`:log4j`, `:pretty`, or `:syslog`) +- `log_format_stdout::Symbol=:pretty`: format for stdout +- `shorten_path::Symbol=:relative_path`: path shortening strategy for log4j format +- `create_log_files::Bool=false`: create separate files per log level +- `overwrite::Bool=false`: overwrite existing log files +- `create_dir::Bool=false`: create the log directory if it doesn't exist +- `verbose::Bool=false`: warn about filtering non-imported modules + +# Example +```julia +custom_logger("/tmp/myapp"; + filtered_modules_all=[:HTTP, :TranscodingStreams], + create_log_files=true, + overwrite=true, + log_format=:log4j) +``` """ function custom_logger( sink::LogSink; @@ -112,52 +118,34 @@ function custom_logger( # warning if some non imported get filtered ... imported_modules = filter((x) -> typeof(getfield(Main, x)) <: Module && x ≠ :Main, names(Main, imported=true)) - all_filters = filter(x->!isnothing(x), unique([filtered_modules_specific; filtered_modules_all])) - catch_nonimported = map(x -> x ∈ imported_modules, all_filters) - if !(reduce(&, catch_nonimported)) && verbose - @warn "Some non (directly) imported modules are being filtered ... $(join(string.(all_filters[.!catch_nonimported]), ", "))" - end - - # Filter functions - function create_absolute_filter(modules) - return function(log) - if isnothing(modules) - return true - else - module_name = string(log._module) - # Check if the module name starts with any of the filtered module names - # some modules did not get filtered because of submodules... - # Note: we might catch too many modules here so keep it in mind if something does not show up in log - for m in modules - if startswith(module_name, string(m)) - return false # Filter out if matches - end - end - return true # Keep if no matches - end + all_filters = Symbol[x for x in unique(vcat( + something(filtered_modules_specific, Symbol[]), + something(filtered_modules_all, Symbol[]))) if !isnothing(x)] + if !isempty(all_filters) && verbose + catch_nonimported = map(x -> x ∈ imported_modules, all_filters) + if !all(catch_nonimported) + @warn "Some non (directly) imported modules are being filtered ... $(join(string.(all_filters[.!catch_nonimported]), ", "))" end end - module_absolute_message_filter = create_absolute_filter(filtered_modules_all) - function create_specific_filter(modules) + # Create a log filter that drops messages from the given modules. + # Uses startswith to also catch submodules (e.g. :HTTP catches HTTP.ConnectionPool). + function create_module_filter(modules) return function(log) if isnothing(modules) return true - else - module_name = string(log._module) - # Check if the module name starts with any of the filtered module names - # some modules did not get filtered because of submodules... - # Note: we might catch too many modules here so keep it in mind if something does not show up in log - for m in modules - if startswith(module_name, string(m)) - return false # Filter out if matches - end + end + module_name = string(log._module) + for m in modules + if startswith(module_name, string(m)) + return false end - return true # Keep if no matches end + return true end end - module_specific_message_filter = create_absolute_filter(all_filters) + module_absolute_message_filter = create_module_filter(filtered_modules_all) + module_specific_message_filter = create_module_filter(filtered_modules_specific) format_log_stdout = (io,log_record)->custom_format(io, log_record; @@ -173,29 +161,6 @@ function custom_logger( log_format=log_format, shorten_path=shorten_path) - # Create demux_logger using sink's IO streams - # demux_logger = TeeLogger( - # MinLevelLogger( - # EarlyFilteredLogger(module_absolute_message_filter, # error - # FormatLogger(format_log_file, sink.ios[1])), - # Logging.Error), - # MinLevelLogger( - # EarlyFilteredLogger(module_absolute_message_filter, # warn - # FormatLogger(format_log_file, sink.ios[2])), - # Logging.Warn), - # MinLevelLogger( - # EarlyFilteredLogger(module_specific_message_filter, # info - # FormatLogger(format_log_file, sink.ios[3])), - # Logging.Info), - # MinLevelLogger( - # EarlyFilteredLogger(module_absolute_message_filter, # debug - # FormatLogger(format_log_file, sink.ios[4])), - # Logging.Debug), - # MinLevelLogger( - # EarlyFilteredLogger(module_specific_message_filter, # stdout - # FormatLogger(format_log_stdout, stdout)), - # Logging.Info) - # ) demux_logger = create_demux_logger(sink, file_loggers, module_absolute_message_filter, module_specific_message_filter, format_log_file, format_log_stdout) @@ -226,12 +191,13 @@ function custom_logger( # create directory if needed and bool true # returns an error if directory does not exist and bool false - log_dir = unique(dirname.(files)) - if create_dir && !isdir(log_dir) - @warn "Creating directory for logs ... $(join(log_dir, ", "))" - mkpath.(log_dir) - elseif !isdir(log_dir) - @error "Directory for logs does not exist ... $(join(log_dir, ", "))" + log_dirs = unique(dirname.(files)) + missing_dirs = filter(d -> !isempty(d) && !isdir(d), log_dirs) + if create_dir && !isempty(missing_dirs) + @warn "Creating directory for logs ... $(join(missing_dirs, ", "))" + mkpath.(missing_dirs) + elseif !isempty(missing_dirs) + @error "Directory for logs does not exist ... $(join(missing_dirs, ", "))" end # Handle cleanup if needed overwrite && foreach(f -> rm(f, force=true), files) @@ -347,10 +313,10 @@ function custom_format(io, log_record::NamedTuple; elseif log_format == :log4j log_entry = log_record |> - str -> format_log4j(str, shorten_path=shorten_path) |> msg_to_singline + str -> format_log4j(str, shorten_path=shorten_path) |> msg_to_singleline println(io, log_entry) elseif log_format == :syslog - log_entry = log_record |> format_syslog |> msg_to_singline + log_entry = log_record |> format_syslog |> msg_to_singleline println(io, log_entry) end @@ -384,7 +350,7 @@ function reformat_msg(log_record; end -function msg_to_singline(message::AbstractString)::AbstractString +function msg_to_singleline(message::AbstractString)::AbstractString message |> str -> replace(str, r"\"\"\"[\r\n\s]*(.+?)[\r\n\s]*\"\"\""s => s"\1") |> str -> replace(str, r"\n\s*" => " | ") |>