BazerUtils.jl

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

commit a4ce90a0b8acae89ccdbd551f29007a5ec260142
parent ece4c8df3850a1c6a5578a3bf30917cc020c29aa
Author: Erik Loualiche <eloualic@umn.edu>
Date:   Tue, 24 Mar 2026 21:29:30 -0500

docs: update README and logger guide for v0.11.0

Document all 6 log formats with examples, cascading_loglevels,
thread safety, and deprecation of :log4j symbol.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Diffstat:
MREADME.md | 70++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mdocs/src/index.md | 2+-
Mdocs/src/man/logger_guide.md | 165++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
3 files changed, 158 insertions(+), 79 deletions(-)

diff --git a/README.md b/README.md @@ -13,13 +13,13 @@ It is a more mature version of [`Prototypes.jl`](https://github.com/louloulibs/P The package provides: - - [`custom_logger`](#custom-logging): configurable logging with per-level file output, module filtering, and multiple format options (`pretty`, `log4j`, `syslog`) + - [`custom_logger`](#custom-logging): configurable logging with per-level file output, module filtering, and multiple format options (`:pretty`, `:oneline`, `:json`, `:logfmt`, `:syslog`, `:log4j_standard`) - ~~`read_jsonl` / `stream_jsonl` / `write_jsonl`~~: **deprecated** — use [`JSON.jl`](https://github.com/JuliaIO/JSON.jl) v1 with `jsonlines=true` instead ## Installation -`BazerUtils.jl` is a registered package. +`BazerUtils.jl` is a registered package. You can install from the my julia registry [`loulouJL`](https://github.com/LouLouLibs/loulouJL) via the julia package manager: ```julia > using Pkg, LocalRegistry @@ -38,30 +38,64 @@ If you don't want to add a new registry, you can install it directly from github ### Custom Logging -This one is a little niche. -I wanted to have a custom logger that would allow me to filter messages from specific modules and redirect them to different files, which I find useful to monitor long jobs in a format that is easy to read and that I can control. -The formatter is hard-coded to what I like but I guess I could change it easily and make it an option. +A configurable logger that lets you filter messages from specific modules and redirect them to different files, with a format that is easy to read and control. -Here is an example where you can create a custom logger and redirect logging to different files. -See the doc for more [examples](https://louloulibs.github.io/BazerUtils.jl/dev/man/logger_guide) ```julia custom_logger( - "./log/build_stable_sample_multiplier"; # prefix of log-file being generated - file_loggers=[:warn, :debug], # which file logger to deploy + "./log/build_stable_sample_multiplier"; + file_loggers=[:warn, :debug], # which file loggers to deploy - filtered_modules_all=[:HTTP], # filtering messages across all loggers from specific modules - filtered_modules_specific=[:TranscodingStreams], # filtering messages for stdout and info from specific modules + filtered_modules_all=[:HTTP], # filter across all loggers + filtered_modules_specific=[:TranscodingStreams], # filter for stdout and info only - displaysize=(50,100), # how much to show - log_format=:log4j, # how to format the log for files - log_format_stdout = :pretty, # how to format the log for the repl + displaysize=(50,100), # how much to show for non-string messages + log_format=:oneline, # format for files (see formats below) + log_format_stdout=:pretty, # format for REPL - create_log_files=true, # if false all logs are written to a single file - overwrite=true, # overwrite old logs - + cascading_loglevels=false, # false = each file gets only its level + # true = each file gets its level and above + + create_log_files=true, # separate file per level + overwrite=true, ); ``` +#### Log Formats + +| Format | Symbol | Description | +|--------|--------|-------------| +| **Pretty** | `:pretty` | Box-drawing + ANSI colors — default for stdout | +| **Oneline** | `:oneline` | Single-line with timestamp, level, module, file:line — default for files | +| **JSON** | `:json` | One JSON object per line — for log aggregation (ELK, Datadog, Loki) | +| **logfmt** | `:logfmt` | `key=value` pairs — grep-friendly, popular with Splunk/Heroku | +| **Syslog** | `:syslog` | RFC 5424 syslog format | +| **Log4j Standard** | `:log4j_standard` | Apache Log4j PatternLayout — for Java tooling interop | + +Example output for each: + +``` +# :pretty (stdout default) +┌ [08:28:08 2025-02-12] Info | @ Main[script.jl:42] +└ Processing batch 5 of 10 + +# :oneline (file default) +[/home/user/project] 2025-02-12 08:28:08 INFO Main[./script.jl:42] Processing batch 5 of 10 + +# :json +{"timestamp":"2025-02-12T08:28:08","level":"INFO","module":"Main","file":"script.jl","line":42,"message":"Processing batch 5 of 10"} + +# :logfmt +ts=2025-02-12T08:28:08 level=info module=Main file=script.jl line=42 msg="Processing batch 5 of 10" + +# :syslog +<14>1 2025-02-12T08:28:08 hostname julia 12345 - - Processing batch 5 of 10 + +# :log4j_standard +2025-02-12 08:28:08,000 INFO [1] Main - Processing batch 5 of 10 +``` + +> **Note:** `:log4j` still works as a deprecated alias for `:oneline` and will be removed in a future version. + ### JSON Lines (deprecated) @@ -77,7 +111,7 @@ JSON.json("out.jsonl", data; jsonlines=true) # write ## Other stuff -See my other package +See my other package - [BazerData.jl](https://github.com/louloulibs/BazerData.jl) which groups together data wrangling functions. - [FinanceRoutines.jl](https://github.com/louloulibs/FinanceRoutines.jl) which is more focused and centered on working with financial data. - [TigerFetch.jl](https://github.com/louloulibs/TigerFetch.jl) which simplifies downloading shape files from the Census. diff --git a/docs/src/index.md b/docs/src/index.md @@ -4,7 +4,7 @@ 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`). +- **[Custom Logger](@ref Logging)**: Configurable logging with per-level file output, module filtering, thread safety, and six format options (`pretty`, `oneline`, `json`, `logfmt`, `syslog`, `log4j_standard`). - **[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 diff --git a/docs/src/man/logger_guide.md b/docs/src/man/logger_guide.md @@ -1,16 +1,18 @@ # Logging The function `custom_logger` is a wrapper over the `Logging.jl` and `LoggingExtras.jl` libraries. -I made them such that I could fine tune the type of log I use repeatedly across projects. +I made them such that I could fine tune the type of log I use repeatedly across projects. The things I find most useful: 1. four different log files for each different level of logging from *error* to *debug* -2. pretty (to me) formatting for stdout but also an option to have `log4j` style formatting in the files -3. filtering out messages of verbose packages (`TranscodingStreams`, etc...) which sometimes slows down julia because of excessive logging. +2. six output formats: `:pretty` for the REPL, `:oneline`, `:json`, `:logfmt`, `:syslog`, and `:log4j_standard` for files +3. filtering out messages of verbose packages (`TranscodingStreams`, etc...) which sometimes slows down julia because of excessive logging +4. exact-level filtering so each file gets only its own level (or cascading for the old behavior) +5. thread-safe writes via per-stream locking There are still a few things that might be useful down the line: -(1) a catch-all log file where filters do not apply; (2) filtering out specific functions of packages; +(1) a catch-all log file where filters do not apply; (2) filtering out specific functions of packages; Overall this is working fine for me. @@ -19,107 +21,150 @@ Overall this is working fine for me. Say at the beginning of a script you would have something like: ```julia using BazerUtils -custom_logger("/tmp/log_test"; - filtered_modules_all=[:StatsModels, :TranscodingStreams, :Parquet2], - create_log_files=true, - overwrite=true, - log_format = :log4j); - -┌ Info: Creating four different files for logging ... -│ ⮑ /tmp/log_test_error.log -│ /tmp/log_test_warn.log -│ /tmp/log_test_info.log -└ /tmp/log_test_debug.log +custom_logger("/tmp/log_test"; + filtered_modules_all=[:StatsModels, :TranscodingStreams, :Parquet2], + create_log_files=true, + overwrite=true, + log_format=:oneline); + +┌ Info: Creating 4 log files: +│ ⮑ /tmp/log_test_error.log +│ /tmp/log_test_warn.log +│ /tmp/log_test_info.log +└ /tmp/log_test_debug.log ``` The REPL will see all messages above debug level: ```julia > @error "This is an error level message" -┌ [08:28:08 2025-02-12] ERROR | @ Main[REPL[17]:1] +┌ [08:28:08 2025-02-12] Error | @ Main[REPL[17]:1] └ This is an error level message > @warn "This is an warn level message" -┌ [08:28:08 2025-02-12] WARN | @ Main[REPL[18]:1] +┌ [08:28:08 2025-02-12] Warn | @ Main[REPL[18]:1] └ This is an warn level message > @info "This is an info level message" -┌ [08:28:08 2025-02-12] INFO | @ Main[REPL[19]:1] +┌ [08:28:08 2025-02-12] Info | @ Main[REPL[19]:1] └ This is an info level message > @debug "This is an debug level message" ``` -Then each of the respective log-levels will be redirected to the individual files and if the log4j option was specified they will look like something like this -```log4j -2025-02-12 08:28:08 ERROR Main[REPL[17]:1] - This is an error level message -2025-02-12 08:28:08 WARN Main[REPL[18]:1] - This is an warn level message -2025-02-12 08:28:08 INFO Main[REPL[19]:1] - This is an info level message -2025-02-12 08:28:08 DEBUG Main[REPL[20]:1] - This is an debug level message +Then each of the respective log-levels will be redirected to the individual files. With the `:oneline` format they will look like: +``` +[/home/user] 2025-02-12 08:28:08 ERROR Main[./REPL[17]:1] This is an error level message +[/home/user] 2025-02-12 08:28:08 WARN Main[./REPL[18]:1] This is an warn level message +[/home/user] 2025-02-12 08:28:08 INFO Main[./REPL[19]:1] This is an info level message +[/home/user] 2025-02-12 08:28:08 DEBUG Main[./REPL[20]:1] This is an debug level message ``` ## Options -### Formatting +### Log Formats -The `log_format` is `log4j` by default (only for the files). -The only other option for now is `pretty` which uses the format I wrote for the REPL; note that it is a little cumbersome for files especially since you have to make sure your editor has the ansi interpreter on. +The `log_format` kwarg controls how file logs are formatted. Default is `:oneline`. +The `log_format_stdout` kwarg controls REPL output. Default is `:pretty`. -### Files +All formats are available for both file and stdout. -The default is to create one file for each level. -There is an option to only create one file for each level and keep things a little tidier in your directories: -```julia -> custom_logger("/tmp/log_test"; - create_log_files=false, overwrite=true, log_format = :log4j); +| Format | Symbol | Best for | +|--------|--------|----------| +| Pretty | `:pretty` | Human reading in the REPL. Box-drawing characters + ANSI colors. | +| Oneline | `:oneline` | File logs. Single line with timestamp, level, module, file:line, message. | +| JSON | `:json` | Structured log aggregation (ELK, Datadog, Loki). One JSON object per line, zero external dependencies. | +| logfmt | `:logfmt` | Grep-friendly structured logs. `key=value` pairs, popular with Splunk/Heroku. | +| Syslog | `:syslog` | RFC 5424 syslog collectors. | +| Log4j Standard | `:log4j_standard` | Java tooling interop. Actual Apache Log4j PatternLayout with thread ID and milliseconds. | -> @error "This is an error level message" -> @warn "This is an warn level message" -> @info "This is an info level message" -> @debug "This is an debug level message" +Example: +```julia +# JSON logs for a data pipeline +custom_logger("/tmp/pipeline"; + log_format=:json, + create_log_files=true, + overwrite=true) + +# logfmt for grep-friendly output +custom_logger("/tmp/pipeline"; + log_format=:logfmt, + overwrite=true) ``` -And then the file `/tmp/log_test` has the following: -```log4j -2025-02-12 08:37:29 ERROR Main[REPL[22]:1] - This is an error level message -2025-02-12 08:37:29 WARN Main[REPL[23]:1] - This is an warn level message -2025-02-12 08:37:29 INFO Main[REPL[24]:1] - This is an info level message -2025-02-12 08:37:29 DEBUG Main[REPL[25]:1] - This is an debug level message -``` +> **Deprecation note:** `:log4j` still works as an alias for `:oneline` but emits a deprecation warning. Use `:oneline` for the single-line format or `:log4j_standard` for the actual Apache Log4j format. -Now imagine you want to keep the same log file but for a different script. -You can use the same logger option with the `overwrite=false` option: -```julia -> custom_logger("/tmp/log_test"; - create_log_files=false, overwrite=false, log_format = :log4j); -> @error "This is an error level message from a different script and new logger" -``` +### Level Filtering: `cascading_loglevels` -### Filtering +By default (`cascading_loglevels=false`), each file gets only messages at its exact level: +- `app_error.log` — only errors +- `app_warn.log` — only warnings +- `app_info.log` — only info +- `app_debug.log` — only debug -- `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) +With `cascading_loglevels=true`, each file gets its level **and everything above**: +- `app_error.log` — only errors +- `app_warn.log` — warnings + errors +- `app_info.log` — info + warnings + errors +- `app_debug.log` — everything +```julia +# Old cascading behavior +custom_logger("/tmp/log_test"; + create_log_files=true, + cascading_loglevels=true, + overwrite=true) +``` -## Other -For `log4j` the message string is modified to fit on one line: `\n` is replaced by ` | `. +### Files + +The default is to write all levels to a single file. +Set `create_log_files=true` to create one file per level: -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. +```julia +# Single file (default) +custom_logger("/tmp/log_test"; overwrite=true) + +# Separate files per level +custom_logger("/tmp/log_test"; + create_log_files=true, overwrite=true) +``` +You can also select only specific levels: +```julia +custom_logger("/tmp/log_test"; + create_log_files=true, + file_loggers=[:warn, :debug], # only warn and debug files + overwrite=true) +``` +Use `overwrite=false` (the default) to append to existing log files across script runs. +### Filtering +- `filtered_modules_specific::Vector{Symbol}`: filter modules from stdout and info-level file logs only. + Some packages write too much — filter them from info but still see them in debug. +- `filtered_modules_all::Vector{Symbol}`: filter modules from ALL logs. + Use for extremely verbose packages like `TranscodingStreams` that can slow down I/O. +```julia +custom_logger("/tmp/log_test"; + filtered_modules_all=[:TranscodingStreams], + filtered_modules_specific=[:HTTP], + overwrite=true) +``` +### Thread Safety +All file writes are wrapped in per-stream `ReentrantLock`s. Multiple threads can log concurrently without interleaving output. In single-file mode, all levels share one lock. In multi-file mode, each file has its own lock so writes to different files don't block each other. +## Other +For single-line formats (`:oneline`, `:logfmt`, `:syslog`, `:log4j_standard`), multi-line messages are collapsed to a single line: `\n` is replaced by ` | `. The `:json` format escapes newlines as `\n` in the JSON string. +There is also a path shortener (`shorten_path`) that reduces file paths. Options: `:relative_path` (default), `:truncate_middle`, `:truncate_to_last`, `:truncate_from_right`, `:truncate_to_unique`, `:no`.