CustomLogger.jl (26653B)
1 # ================================================================================================== 2 # CustomLogger.jl — Custom multi-sink logger with per-level filtering and pluggable formats 3 # ================================================================================================== 4 5 6 # --- Format types (multiple dispatch instead of if/elseif) --- 7 8 abstract type LogFormat end 9 struct PrettyFormat <: LogFormat end 10 struct OnelineFormat <: LogFormat end 11 struct SyslogFormat <: LogFormat end 12 struct JsonFormat <: LogFormat end 13 struct LogfmtFormat <: LogFormat end 14 struct Log4jStandardFormat <: LogFormat end 15 16 const VALID_FORMATS = "Valid options: :pretty, :oneline, :syslog, :json, :logfmt, :log4j_standard" 17 18 """ 19 resolve_format(s::Symbol) -> LogFormat 20 21 Map a format symbol to its LogFormat type. `:log4j` is a deprecated alias for `:oneline`. 22 """ 23 # TODO (March 2027): Remove :log4j alias for :oneline. Rename :log4j_standard to :log4j. 24 # This is a breaking change requiring a major version bump. 25 function resolve_format(s::Symbol)::LogFormat 26 s === :pretty && return PrettyFormat() 27 s === :oneline && return OnelineFormat() 28 s === :log4j && (Base.depwarn( 29 ":log4j is deprecated, use :oneline for single-line format or :log4j_standard for Apache Log4j format. :log4j will be removed in a future major version.", 30 :log4j); return OnelineFormat()) 31 s === :syslog && return SyslogFormat() 32 s === :json && return JsonFormat() 33 s === :logfmt && return LogfmtFormat() 34 s === :log4j_standard && return Log4jStandardFormat() 35 throw(ArgumentError("Unknown log_format: :$s. $VALID_FORMATS")) 36 end 37 38 39 # --- Helper functions --- 40 41 """ 42 get_module_name(mod) -> String 43 44 Extract module name as a string, returning "unknown" for `nothing`. 45 """ 46 get_module_name(mod::Module) = string(nameof(mod)) 47 get_module_name(::Nothing) = "unknown" 48 49 """ 50 reformat_msg(log_record; displaysize=(50,100)) -> String 51 52 Convert log record message to a string. Strings pass through; other types 53 are rendered via `show` with display size limits. 54 """ 55 function reformat_msg(log_record; displaysize::Tuple{Int,Int}=(50,100))::String 56 msg = log_record.message 57 msg isa AbstractString && return String(msg) 58 buf = IOBuffer() 59 show(IOContext(buf, :limit => true, :compact => true, :displaysize => displaysize), 60 "text/plain", msg) 61 return String(take!(buf)) 62 end 63 64 """ 65 msg_to_singleline(message::AbstractString) -> String 66 67 Collapse a multi-line message to a single line, using ` | ` as separator. 68 """ 69 function msg_to_singleline(message::AbstractString)::String 70 message |> 71 str -> replace(str, r"\"\"\"[\r\n\s]*(.+?)[\r\n\s]*\"\"\""s => s"\1") |> 72 str -> replace(str, r"\n\s*" => " | ") |> 73 str -> replace(str, r"\|\s*\|" => "|") |> 74 str -> replace(str, r"\s*\|\s*" => " | ") |> 75 str -> replace(str, r"\|\s*$" => "") |> 76 strip |> String 77 end 78 79 """ 80 json_escape(s::AbstractString) -> String 81 82 Escape a string for inclusion in a JSON value (without surrounding quotes). 83 """ 84 function json_escape(s::AbstractString)::String 85 s = replace(s, '\\' => "\\\\") 86 s = replace(s, '"' => "\\\"") 87 s = replace(s, '\n' => "\\n") 88 s = replace(s, '\r' => "\\r") 89 s = replace(s, '\t' => "\\t") 90 return s 91 end 92 93 """ 94 logfmt_escape(s::AbstractString) -> String 95 96 Format a value for logfmt output. Quotes the value if it contains spaces, equals, or quotes. 97 """ 98 function logfmt_escape(s::AbstractString)::String 99 needs_quoting = contains(s, ' ') || contains(s, '"') || contains(s, '=') 100 if needs_quoting 101 return "\"" * replace(s, '"' => "\\\"") * "\"" 102 end 103 return s 104 end 105 106 107 # --- LogSink infrastructure --- 108 109 abstract type LogSink end 110 111 # Keep the active sink alive so the finalizer does not close it prematurely 112 # while the global logger is still writing to its IO handles. 113 const _active_sink = Ref{Union{Nothing, LogSink}}(nothing) 114 115 """ 116 get_log_filenames(filename; file_loggers, create_files) -> Vector{String} 117 118 Generate log file paths. When `create_files=true`, creates `filename_level.log` per level. 119 When `false`, repeats `filename` for all levels. 120 """ 121 function get_log_filenames(filename::AbstractString; 122 file_loggers::Vector{Symbol}=[:error, :warn, :info, :debug], 123 create_files::Bool=false) 124 if create_files 125 return [string(filename, "_", string(f), ".log") for f in file_loggers] 126 else 127 return repeat([filename], length(file_loggers)) 128 end 129 end 130 131 function get_log_filenames(files::Vector{<:AbstractString}; 132 file_loggers::Vector{Symbol}=[:error, :warn, :info, :debug]) 133 n = length(file_loggers) 134 length(files) != n && throw(ArgumentError( 135 "Expected exactly $n file paths (one per logger: $(join(file_loggers, ", "))), got $(length(files))")) 136 return files 137 end 138 139 """ 140 FileSink <: LogSink 141 142 File-based log sink with per-stream locking for thread safety. 143 144 When all files point to the same path (single-file mode), IO handles and locks are 145 deduplicated — one IO and one lock shared across all slots. 146 """ 147 mutable struct FileSink <: LogSink 148 files::Vector{String} 149 ios::Vector{IO} 150 locks::Vector{ReentrantLock} 151 152 function FileSink(filename::AbstractString; 153 file_loggers::Vector{Symbol}=[:error, :warn, :info, :debug], 154 create_files::Bool=false) 155 files = get_log_filenames(filename; file_loggers=file_loggers, create_files=create_files) 156 if create_files 157 @info "Creating $(length(files)) log files:\n$(join(string.(" \u2B91 ", files), "\n"))" 158 else 159 @info "Single log sink: all levels writing to $filename" 160 end 161 # Deduplicate: open each unique path once, share IO + lock 162 unique_paths = unique(files) 163 path_to_io = Dict(p => open(p, "a") for p in unique_paths) 164 path_to_lock = Dict(p => ReentrantLock() for p in unique_paths) 165 ios = [path_to_io[f] for f in files] 166 locks = [path_to_lock[f] for f in files] 167 obj = new(files, ios, locks) 168 finalizer(close, obj) 169 return obj 170 end 171 172 function FileSink(files::Vector{<:AbstractString}; 173 file_loggers::Vector{Symbol}=[:error, :warn, :info, :debug]) 174 actual_files = get_log_filenames(files; file_loggers=file_loggers) 175 unique_paths = unique(actual_files) 176 path_to_io = Dict(p => open(p, "a") for p in unique_paths) 177 path_to_lock = Dict(p => ReentrantLock() for p in unique_paths) 178 ios = [path_to_io[f] for f in actual_files] 179 locks = [path_to_lock[f] for f in actual_files] 180 obj = new(actual_files, ios, locks) 181 finalizer(close, obj) 182 return obj 183 end 184 end 185 186 function Base.close(sink::FileSink) 187 for io in unique(sink.ios) 188 io !== stdout && io !== stderr && isopen(io) && close(io) 189 end 190 end 191 # -------------------------------------------------------------------------------------------------- 192 193 194 # ================================================================================================== 195 # custom_format — dispatch hub. Called by FormatLogger callbacks. 196 # ================================================================================================== 197 198 """ 199 custom_format(io, fmt::LogFormat, log_record::NamedTuple; kwargs...) 200 201 Format and write a log record to `io` using the given format. Generates a single 202 timestamp and delegates to the appropriate `format_log` method. 203 """ 204 function custom_format(io, fmt::LogFormat, log_record::NamedTuple; 205 displaysize::Tuple{Int,Int}=(50,100), 206 log_date_format::AbstractString="yyyy-mm-dd", 207 log_time_format::AbstractString="HH:MM:SS", 208 shorten_path::Symbol=:relative_path) 209 210 timestamp = now() 211 format_log(io, fmt, log_record, timestamp; 212 displaysize=displaysize, 213 log_date_format=log_date_format, 214 log_time_format=log_time_format, 215 shorten_path=shorten_path) 216 end 217 218 219 # ================================================================================================== 220 # create_demux_logger — builds the TeeLogger pipeline 221 # ================================================================================================== 222 223 function create_demux_logger(sink::FileSink, 224 file_loggers::Vector{Symbol}, 225 module_absolute_message_filter, 226 module_specific_message_filter, 227 fmt_file::LogFormat, 228 fmt_stdout::LogFormat, 229 format_kwargs::NamedTuple; 230 cascading_loglevels::Bool=false) 231 232 logger_configs = Dict( 233 :error => (module_absolute_message_filter, Logging.Error), 234 :warn => (module_absolute_message_filter, Logging.Warn), 235 :info => (module_specific_message_filter, Logging.Info), 236 :debug => (module_absolute_message_filter, Logging.Debug) 237 ) 238 239 logger_list = [] 240 241 for (io_index, logger_key) in enumerate(file_loggers) 242 if !haskey(logger_configs, logger_key) 243 @warn "Unknown logger type: $logger_key — skipping" 244 continue 245 end 246 if io_index > length(sink.ios) 247 error("Not enough IO streams in sink for logger: $logger_key") 248 end 249 250 message_filter, log_level = logger_configs[logger_key] 251 io = sink.ios[io_index] 252 lk = sink.locks[io_index] 253 254 # Thread-safe format callback 255 format_cb = (cb_io, log_record) -> lock(lk) do 256 custom_format(cb_io, fmt_file, log_record; format_kwargs...) 257 end 258 259 inner = EarlyFilteredLogger(message_filter, FormatLogger(format_cb, io)) 260 261 if cascading_loglevels 262 # Old behavior: MinLevelLogger catches this level and above 263 push!(logger_list, MinLevelLogger(inner, log_level)) 264 else 265 # New behavior: exact level only 266 exact_filter = log -> log.level == log_level 267 push!(logger_list, EarlyFilteredLogger(exact_filter, inner)) 268 end 269 end 270 271 # Stdout logger — always Info+, uses specific module filter, no file locking 272 stdout_format_cb = (io, log_record) -> custom_format(io, fmt_stdout, log_record; 273 format_kwargs...) 274 stdout_logger = MinLevelLogger( 275 EarlyFilteredLogger(module_specific_message_filter, 276 FormatLogger(stdout_format_cb, stdout)), 277 Logging.Info) 278 push!(logger_list, stdout_logger) 279 280 return TeeLogger(logger_list...) 281 end 282 283 284 # ================================================================================================== 285 # custom_logger — public API 286 # ================================================================================================== 287 288 """ 289 custom_logger(filename; kw...) 290 291 Set up a custom global logger with per-level file output, module filtering, and configurable formatting. 292 293 When `create_log_files=true`, creates one log file per level (e.g. `filename_error.log`). 294 Otherwise all levels write to the same file. 295 296 # Arguments 297 - `filename::AbstractString`: base name for the log files 298 - `filtered_modules_specific::Union{Nothing, Vector{Symbol}}=nothing`: modules to filter from stdout and info-level file logs 299 - `filtered_modules_all::Union{Nothing, Vector{Symbol}}=nothing`: modules to filter from all logs 300 - `file_loggers::Union{Symbol, Vector{Symbol}}=[:error, :warn, :info, :debug]`: which levels to capture 301 - `log_date_format::AbstractString="yyyy-mm-dd"`: date format in timestamps 302 - `log_time_format::AbstractString="HH:MM:SS"`: time format in timestamps 303 - `displaysize::Tuple{Int,Int}=(50,100)`: display size for non-string messages 304 - `log_format::Symbol=:oneline`: file log format (`:pretty`, `:oneline`, `:syslog`, `:json`, `:logfmt`, `:log4j_standard`) 305 - `log_format_stdout::Symbol=:pretty`: stdout format (same options) 306 - `shorten_path::Symbol=:relative_path`: path shortening strategy (`:oneline` format only) 307 - `cascading_loglevels::Bool=false`: when `true`, each file captures its level and above; when `false`, each file captures only its exact level 308 - `create_log_files::Bool=false`: create separate files per level 309 - `overwrite::Bool=false`: overwrite existing log files 310 - `create_dir::Bool=false`: create log directory if missing 311 - `verbose::Bool=false`: warn about filtering non-imported modules 312 313 # Example 314 ```julia 315 custom_logger("/tmp/myapp"; 316 filtered_modules_all=[:HTTP, :TranscodingStreams], 317 create_log_files=true, 318 overwrite=true, 319 log_format=:oneline) 320 ``` 321 """ 322 function custom_logger( 323 sink::LogSink; 324 filtered_modules_specific::Union{Nothing, Vector{Symbol}}=nothing, 325 filtered_modules_all::Union{Nothing, Vector{Symbol}}=nothing, 326 file_loggers::Union{Symbol, Vector{Symbol}}=[:error, :warn, :info, :debug], 327 log_date_format::AbstractString="yyyy-mm-dd", 328 log_time_format::AbstractString="HH:MM:SS", 329 displaysize::Tuple{Int,Int}=(50,100), 330 log_format::Symbol=:oneline, 331 log_format_stdout::Symbol=:pretty, 332 shorten_path::Symbol=:relative_path, 333 cascading_loglevels::Bool=false, 334 verbose::Bool=false) 335 336 # Resolve format types (validates symbols, handles :log4j deprecation) 337 fmt_file = resolve_format(log_format) 338 fmt_stdout = resolve_format(log_format_stdout) 339 340 # Normalize file_loggers to Vector 341 file_loggers_vec = file_loggers isa Symbol ? [file_loggers] : collect(file_loggers) 342 343 # Warn about filtering non-imported modules 344 if verbose 345 imported_modules = filter( 346 x -> isdefined(Main, x) && typeof(getfield(Main, x)) <: Module && x !== :Main, 347 names(Main, imported=true)) 348 all_filters = Symbol[x for x in unique(vcat( 349 something(filtered_modules_specific, Symbol[]), 350 something(filtered_modules_all, Symbol[]))) if !isnothing(x)] 351 if !isempty(all_filters) 352 missing_mods = filter(x -> x ∉ imported_modules, all_filters) 353 if !isempty(missing_mods) 354 @warn "Filtering non-imported modules: $(join(string.(missing_mods), ", "))" 355 end 356 end 357 end 358 359 # Module filters 360 module_absolute_filter = create_module_filter(filtered_modules_all) 361 module_specific_filter = create_module_filter(filtered_modules_specific) 362 363 format_kwargs = (displaysize=displaysize, 364 log_date_format=log_date_format, 365 log_time_format=log_time_format, 366 shorten_path=shorten_path) 367 368 demux = create_demux_logger(sink, file_loggers_vec, 369 module_absolute_filter, module_specific_filter, 370 fmt_file, fmt_stdout, format_kwargs; 371 cascading_loglevels=cascading_loglevels) 372 373 # Keep sink alive to prevent GC from closing IO handles 374 _active_sink[] = sink 375 376 global_logger(demux) 377 return demux 378 end 379 380 """ 381 create_module_filter(modules) -> Function 382 383 Return a filter function that drops log messages from the specified modules. 384 Uses `startswith` to catch submodules (e.g. `:HTTP` catches `HTTP.ConnectionPool`). 385 """ 386 function create_module_filter(modules) 387 return function(log) 388 isnothing(modules) && return true 389 mod = string(log._module) 390 for m in modules 391 startswith(mod, string(m)) && return false 392 end 393 return true 394 end 395 end 396 397 # Convenience constructor: filename or vector of filenames 398 function custom_logger( 399 filename::Union{AbstractString, Vector{<:AbstractString}}; 400 create_log_files::Bool=false, 401 overwrite::Bool=false, 402 create_dir::Bool=false, 403 file_loggers::Union{Symbol, Vector{Symbol}}=[:error, :warn, :info, :debug], 404 kwargs...) 405 406 file_loggers_array = file_loggers isa Symbol ? [file_loggers] : collect(file_loggers) 407 408 files = if filename isa AbstractString 409 get_log_filenames(filename; file_loggers=file_loggers_array, create_files=create_log_files) 410 else 411 get_log_filenames(filename; file_loggers=file_loggers_array) 412 end 413 414 # Create directories if needed 415 log_dirs = unique(dirname.(files)) 416 missing_dirs = filter(d -> !isempty(d) && !isdir(d), log_dirs) 417 if !isempty(missing_dirs) 418 if create_dir 419 @warn "Creating log directories: $(join(missing_dirs, ", "))" 420 mkpath.(missing_dirs) 421 else 422 @error "Log directories do not exist: $(join(missing_dirs, ", "))" 423 end 424 end 425 426 overwrite && foreach(f -> rm(f, force=true), unique(files)) 427 428 sink = if filename isa AbstractString 429 FileSink(filename; file_loggers=file_loggers_array, create_files=create_log_files) 430 else 431 FileSink(filename; file_loggers=file_loggers_array) 432 end 433 434 custom_logger(sink; file_loggers=file_loggers, kwargs...) 435 end 436 437 # Convenience for batch/script mode 438 function custom_logger(; kwargs...) 439 if !isempty(PROGRAM_FILE) 440 logbase = splitext(abspath(PROGRAM_FILE))[1] 441 custom_logger(logbase; kwargs...) 442 else 443 @error "custom_logger() with no arguments requires a script context (PROGRAM_FILE is empty in the REPL)" 444 end 445 end 446 447 448 # --- Helper: colors for pretty format --- 449 450 function get_color(level) 451 RESET = "\033[0m" 452 BOLD = "\033[1m" 453 LIGHT_BLUE = "\033[94m" 454 RED = "\033[31m" 455 GREEN = "\033[32m" 456 YELLOW = "\033[33m" 457 458 return level == Logging.Debug ? LIGHT_BLUE : 459 level == Logging.Info ? GREEN : 460 level == Logging.Warn ? "$YELLOW$BOLD" : 461 level == Logging.Error ? "$RED$BOLD" : 462 RESET 463 end 464 465 466 # -------------------------------------------------------------------------------------------------- 467 """ 468 shorten_path_str(path::AbstractString; max_length::Int=40, strategy::Symbol=:truncate_middle) 469 470 Shorten a file path string to a specified maximum length using various strategies. 471 472 # Arguments 473 - `path::AbstractString`: The input path to be shortened 474 - `max_length::Int=40`: Maximum desired length of the output path 475 - `strategy::Symbol=:truncate_middle`: Strategy to use for shortening. Options: 476 * `:no`: Return path unchanged 477 * `:truncate_middle`: Truncate middle of path components while preserving start/end 478 * `:truncate_to_last`: Keep only the last n components of the path 479 * `:truncate_from_right`: Progressively remove characters from right side of components 480 * `:truncate_to_unique`: Reduce components to unique prefixes 481 482 # Returns 483 - `String`: The shortened path 484 485 # Examples 486 ```julia 487 # Using different strategies 488 julia> shorten_path_str("/very/long/path/to/file.txt", max_length=20) 489 "/very/…/path/to/file.txt" 490 491 julia> shorten_path_str("/usr/local/bin/program", strategy=:truncate_to_last, max_length=20) 492 "/bin/program" 493 494 julia> shorten_path_str("/home/user/documents/very_long_filename.txt", strategy=:truncate_middle) 495 "/home/user/doc…ents/very_…name.txt" 496 ``` 497 """ 498 function shorten_path_str(path::AbstractString; 499 max_length::Int=40, 500 strategy::Symbol=:truncate_middle 501 )::AbstractString 502 503 if strategy == :no 504 return path 505 elseif strategy == :relative_path 506 return "./" * relpath(path, pwd()) 507 end 508 509 # Return early if path is already short enough 510 if length(path) ≤ max_length 511 return path 512 end 513 514 # Split path into components 515 parts = split(path, '/') 516 is_absolute = startswith(path, '/') 517 518 # Handle empty path or root directory 519 if isempty(parts) || (length(parts) == 1 && isempty(parts[1])) 520 return is_absolute ? "/" : "" 521 end 522 523 # Remove empty strings from split 524 parts = filter(!isempty, parts) 525 526 if strategy == :truncate_to_last 527 # Keep only the last few components 528 n = 2 # number of components to keep 529 if length(parts) > n 530 shortened = parts[end-n+1:end] 531 result = join(shortened, "/") 532 return is_absolute ? "/$result" : result 533 end 534 535 elseif strategy == :truncate_middle 536 # For each component, truncate the middle if it's too long 537 function shorten_component(comp::AbstractString; max_comp_len::Int=10) 538 if length(comp) ≤ max_comp_len 539 return comp 540 end 541 keep = max_comp_len ÷ 2 - 1 542 return string(comp[1:keep], "…", comp[end-keep+1:end]) 543 end 544 545 shortened = map(p -> shorten_component(p), parts) 546 result = join(shortened, "/") 547 if length(result) > max_length 548 # If still too long, drop some middle directories 549 middle_start = length(parts) ÷ 3 550 middle_end = 2 * length(parts) ÷ 3 551 shortened = [parts[1:middle_start]..., "…", parts[middle_end:end]...] 552 result = join(shortened, "/") 553 end 554 return is_absolute ? "/$result" : result 555 556 elseif strategy == :truncate_from_right 557 # Start removing characters from right side of each component 558 shortened = copy(parts) 559 while join(shortened, "/") |> length > max_length && any(length.(shortened) .> 3) 560 # Find longest component 561 idx = argmax(length.(shortened)) 562 if length(shortened[idx]) > 3 563 shortened[idx] = shortened[idx][1:end-1] 564 end 565 end 566 result = join(shortened, "/") 567 return is_absolute ? "/$result" : result 568 569 elseif strategy == :truncate_to_unique 570 # Simplified unique prefix strategy 571 function unique_prefix(str::AbstractString, others::Vector{String}; min_len::Int=1) 572 for len in min_len:length(str) 573 prefix = str[1:len] 574 if !any(s -> s != str && startswith(s, prefix), others) 575 return prefix 576 end 577 end 578 return str 579 end 580 581 # Get unique prefixes for each component 582 shortened = String[] 583 for (i, part) in enumerate(parts) 584 if i == 1 || i == length(parts) 585 push!(shortened, part) 586 else 587 prefix = unique_prefix(part, String.(parts)) 588 push!(shortened, prefix) 589 end 590 end 591 592 result = join(shortened, "/") 593 return is_absolute ? "/$result" : result 594 end 595 596 # Default fallback: return truncated original path 597 return string(path[1:max_length-3], "…") 598 end 599 # -------------------------------------------------------------------------------------------------- 600 601 602 # --- Constants for format_log methods --- 603 604 const SYSLOG_SEVERITY = Dict( 605 Logging.Info => 6, 606 Logging.Warn => 4, 607 Logging.Error => 3, 608 Logging.Debug => 7 609 ) 610 611 const JULIA_BIN = Base.julia_cmd().exec[1] 612 613 614 # ================================================================================================== 615 # format_log methods — one per LogFormat type 616 # All write directly to `io`. All accept a pre-computed `timestamp::DateTime`. 617 # ================================================================================================== 618 619 function format_log(io, ::PrettyFormat, log_record::NamedTuple, timestamp::Dates.DateTime; 620 displaysize::Tuple{Int,Int}=(50,100), 621 log_date_format::AbstractString="yyyy-mm-dd", 622 log_time_format::AbstractString="HH:MM:SS", 623 kwargs...) 624 625 BOLD = "\033[1m" 626 EMPH = "\033[2m" 627 RESET = "\033[0m" 628 629 date = format(timestamp, log_date_format) 630 time_str = format(timestamp, log_time_format) 631 ts = "$BOLD$(time_str)$RESET $EMPH$date$RESET" 632 633 level_str = string(log_record.level) 634 color = get_color(log_record.level) 635 mod_name = get_module_name(log_record._module) 636 source = " @ $mod_name[$(log_record.file):$(log_record.line)]" 637 first_line = "┌ [$ts] $color$level_str$RESET | $source" 638 639 formatted = reformat_msg(log_record; displaysize=displaysize) 640 lines = split(formatted, "\n") 641 642 println(io, first_line) 643 for (i, line) in enumerate(lines) 644 prefix = i < length(lines) ? "│ " : "└ " 645 println(io, prefix, line) 646 end 647 end 648 649 function format_log(io, ::OnelineFormat, log_record::NamedTuple, timestamp::Dates.DateTime; 650 displaysize::Tuple{Int,Int}=(50,100), 651 shorten_path::Symbol=:relative_path, 652 kwargs...) 653 654 ts = format(timestamp, "yyyy-mm-dd HH:MM:SS") 655 level = rpad(uppercase(string(log_record.level)), 5) 656 mod_name = get_module_name(log_record._module) 657 file = shorten_path_str(log_record.file; strategy=shorten_path) 658 prefix = shorten_path === :relative_path ? "[$(pwd())] " : "" 659 msg = reformat_msg(log_record; displaysize=displaysize) |> msg_to_singleline 660 661 println(io, "$prefix$ts $level $mod_name[$file:$(log_record.line)] $msg") 662 end 663 664 function format_log(io, ::SyslogFormat, log_record::NamedTuple, timestamp::Dates.DateTime; 665 displaysize::Tuple{Int,Int}=(50,100), 666 kwargs...) 667 668 ts = Dates.format(timestamp, "yyyy-mm-ddTHH:MM:SS") 669 severity = get(SYSLOG_SEVERITY, log_record.level, 6) 670 pri = (1 * 8) + severity 671 hostname = gethostname() 672 pid = getpid() 673 msg = reformat_msg(log_record; displaysize=displaysize) |> msg_to_singleline 674 675 println(io, "<$pri>1 $ts $hostname $JULIA_BIN $pid - - $msg") 676 end 677 678 function format_log(io, ::JsonFormat, log_record::NamedTuple, timestamp::Dates.DateTime; 679 displaysize::Tuple{Int,Int}=(50,100), 680 kwargs...) 681 682 ts = Dates.format(timestamp, "yyyy-mm-ddTHH:MM:SS") 683 level = json_escape(uppercase(string(log_record.level))) 684 mod_name = json_escape(get_module_name(log_record._module)) 685 file = json_escape(string(log_record.file)) 686 line = log_record.line 687 msg = json_escape(reformat_msg(log_record; displaysize=displaysize)) 688 689 println(io, "{\"timestamp\":\"$ts\",\"level\":\"$level\",\"module\":\"$mod_name\",\"file\":\"$file\",\"line\":$line,\"message\":\"$msg\"}") 690 end 691 692 function format_log(io, ::LogfmtFormat, log_record::NamedTuple, timestamp::Dates.DateTime; 693 displaysize::Tuple{Int,Int}=(50,100), 694 kwargs...) 695 696 ts = Dates.format(timestamp, "yyyy-mm-ddTHH:MM:SS") 697 level = lowercase(string(log_record.level)) 698 mod_name = get_module_name(log_record._module) 699 file = logfmt_escape(string(log_record.file)) 700 msg = logfmt_escape(reformat_msg(log_record; displaysize=displaysize) |> msg_to_singleline) 701 702 println(io, "ts=$ts level=$level module=$mod_name file=$file line=$(log_record.line) msg=$msg") 703 end 704 705 function format_log(io, ::Log4jStandardFormat, log_record::NamedTuple, timestamp::Dates.DateTime; 706 displaysize::Tuple{Int,Int}=(50,100), 707 kwargs...) 708 709 ts = format(timestamp, "yyyy-mm-dd HH:MM:SS") 710 millis = lpad(Dates.millisecond(timestamp), 3, '0') 711 level = rpad(uppercase(string(log_record.level)), 5) 712 thread_id = Threads.threadid() 713 mod_name = get_module_name(log_record._module) 714 msg = reformat_msg(log_record; displaysize=displaysize) |> msg_to_singleline 715 716 println(io, "$ts,$millis $level [$thread_id] $mod_name - $msg") 717 end