Skip to content

Commit 06936e8

Browse files
committed
Implement a callback function for DatetimeRotatingFileLogger.
1 parent 21cd994 commit 06936e8

File tree

3 files changed

+61
-12
lines changed

3 files changed

+61
-12
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ julia> filter(f -> endswith(f, ".log"), readdir(pwd()))
316316
```
317317
318318
The user implicitly controls when the files will be rolled over based on the `DateFormat` given.
319+
To post-process the newly rotated file pass `callback::Function` as a keyword argument.
320+
See the docstring with (`?DatetimeRotatingFileLogger` in the REPL) for more details.
321+
319322
To control the logging output it is possible to pass a formatter function as the first argument
320323
in the constructor. See `FormatLogger` for the requirements on the formatter function.
321324

src/datetime_rotation.jl

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ using Dates
22
import Base: isless
33

44
raw"""
5-
DatetimeRotatingFileLogger(dir, file_pattern; always_flush=true)
6-
DatetimeRotatingFileLogger(f::Function, dir, file_pattern; always_flush=true)
5+
DatetimeRotatingFileLogger(dir, file_pattern; always_flush=true, callback=nothing)
6+
DatetimeRotatingFileLogger(f::Function, dir, file_pattern; always_flush=true, callback=nothing)
77
88
Construct a `DatetimeRotatingFileLogger` that rotates its file based on the current date.
99
The constructor takes a log output directory, `dir`, and a filename pattern.
@@ -18,6 +18,11 @@ where `log_args` has the following fields:
1818
`(level, message, _module, group, id, file, line, kwargs)`.
1919
See `?LoggingExtra.handle_message_args` for more information about what each field represents.
2020
21+
It is also possible to pass `callback::Function` as a keyword argument. This function
22+
will be called every time a file rotation is happening. The function should accept one
23+
argument which is the absolute path to the just-rotated file. The logger will block until
24+
the callback function returns. Use `@async` if the callback is expensive.
25+
2126
# Examples
2227
2328
```julia
@@ -28,19 +33,25 @@ logger = DatetimeRotatingFileLogger(log_dir, raw"\a\c\c\e\s\s-yyyy-mm-dd.\l\o\g"
2833
logger = DatetimeRotatingFileLogger(log_dir, raw"yyyy-mm-dd-HH.\l\o\g") do io, args
2934
println(io, args.level, " | ", args.message)
3035
end
36+
37+
# Example callback function to compress the recently-closed file
38+
callback(file) = run(`gzip $(file)`)
3139
"""
3240
mutable struct DatetimeRotatingFileLogger <: AbstractLogger
3341
logger::Union{SimpleLogger,FormatLogger}
3442
dir::String
3543
filename_pattern::DateFormat
3644
next_reopen_check::DateTime
3745
always_flush::Bool
46+
reopen_lock::ReentrantLock
47+
current_file::Union{String,Nothing}
48+
callback::Union{Function,Nothing}
3849
end
3950

40-
function DatetimeRotatingFileLogger(dir, filename_pattern; always_flush=true)
41-
DatetimeRotatingFileLogger(nothing, dir, filename_pattern; always_flush=always_flush)
51+
function DatetimeRotatingFileLogger(dir, filename_pattern; always_flush=true, callback=nothing)
52+
DatetimeRotatingFileLogger(nothing, dir, filename_pattern; always_flush=always_flush, callback=callback)
4253
end
43-
function DatetimeRotatingFileLogger(f::Union{Function,Nothing}, dir, filename_pattern; always_flush=true)
54+
function DatetimeRotatingFileLogger(f::Union{Function,Nothing}, dir, filename_pattern; always_flush=true, callback=nothing)
4455
# Construct the backing logger with a temp IOBuffer that will be replaced
4556
# by the correct filestream in the call to reopen! below
4657
logger = if f === nothing
@@ -50,15 +61,25 @@ function DatetimeRotatingFileLogger(f::Union{Function,Nothing}, dir, filename_pa
5061
end
5162
# abspath in case user constructs the logger with a relative path and later cd's.
5263
drfl = DatetimeRotatingFileLogger(logger, abspath(dir),
53-
DateFormat(filename_pattern), now(), always_flush)
64+
DateFormat(filename_pattern), now(), always_flush, ReentrantLock(), nothing, callback)
5465
reopen!(drfl)
5566
return drfl
5667
end
5768

5869
similar_logger(::SimpleLogger, io) = SimpleLogger(io, BelowMinLevel)
5970
similar_logger(l::FormatLogger, io) = FormatLogger(l.f, io, l.always_flush)
6071
function reopen!(drfl::DatetimeRotatingFileLogger)
61-
io = open(calc_logpath(drfl.dir, drfl.filename_pattern), "a")
72+
if drfl.current_file !== nothing
73+
# flush and close the old IOStream
74+
flush(drfl.logger.stream)
75+
close(drfl.logger.stream)
76+
if drfl.callback !== nothing
77+
drfl.callback(drfl.current_file)
78+
end
79+
end
80+
new_file = calc_logpath(drfl.dir, drfl.filename_pattern)
81+
drfl.current_file = new_file
82+
io = open(new_file, "a")
6283
drfl.logger = similar_logger(drfl.logger, io)
6384
drfl.next_reopen_check = next_datetime_transition(drfl.filename_pattern)
6485
return nothing
@@ -110,9 +131,10 @@ end
110131
calc_logpath(dir, filename_pattern) = joinpath(dir, Dates.format(now(), filename_pattern))
111132

112133
function handle_message(drfl::DatetimeRotatingFileLogger, args...; kwargs...)
113-
if now() >= drfl.next_reopen_check
114-
flush(drfl.logger.stream)
115-
reopen!(drfl)
134+
lock(drfl.reopen_lock) do
135+
if now() >= drfl.next_reopen_check
136+
reopen!(drfl)
137+
end
116138
end
117139
handle_message(drfl.logger, args...; kwargs...)
118140
drfl.always_flush && flush(drfl.logger.stream)

test/runtests.jl

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,17 @@ end
128128
drfl_min = DatetimeRotatingFileLogger(dir, raw"\m\i\n-YYYY-mm-dd-HH-MM.\l\o\g")
129129
func = (io, args) -> println(io, reverse(args.message))
130130
drfl_fmt = DatetimeRotatingFileLogger(func, dir, raw"\f\m\t-YYYY-mm-dd-HH-MM-SS.\l\o\g")
131+
function compressor(file)
132+
open(file) do src; open(file * ".compressed", "w") do sink
133+
for line in eachline(src)
134+
write(sink, replace(line, r"Info" => "I"))
135+
end
136+
end end
137+
end
138+
drfl_cb = DatetimeRotatingFileLogger(dir, raw"\c\o\m\p-YYYY-mm-dd-HH-MM-SS.\l\o\g";
139+
callback=compressor)
131140

132-
sink = TeeLogger(drfl_sec, drfl_min, drfl_fmt)
141+
sink = TeeLogger(drfl_sec, drfl_min, drfl_fmt, drfl_cb)
133142
with_logger(sink) do
134143
while millisecond(now()) < 100 || millisecond(now()) > 200
135144
sleep(0.001)
@@ -142,7 +151,7 @@ end
142151

143152
# Drop anything that's not a .log file or empty
144153
files = sort(map(f -> joinpath(dir, f), readdir(dir)))
145-
files = filter(f -> endswith(f, ".log") && filesize(f) > 0, files)
154+
files = filter(f -> (endswith(f, ".log") || endswith(f, ".compressed")) && filesize(f) > 0, files)
146155
sec_files = filter(f -> startswith(basename(f), "sec-"), files)
147156
@test length(sec_files) == 2
148157

@@ -152,6 +161,13 @@ end
152161
fmt_files = filter(f -> startswith(basename(f), "fmt-"), files)
153162
@test length(fmt_files) == 2
154163

164+
comp_files = filter(f -> startswith(basename(f), "comp-") && endswith(f, ".log"), files)
165+
@test length(comp_files) == 2
166+
167+
compressed_files = filter(f -> endswith(basename(f), ".compressed"), files)
168+
# Last file has not been rotated although it exists
169+
@test length(compressed_files) == 1
170+
155171
sec1_data = String(read(sec_files[1]))
156172
@test occursin("first", sec1_data)
157173
@test occursin("second", sec1_data)
@@ -168,6 +184,14 @@ end
168184
@test occursin("dnoces", fmt_data)
169185
fmt_data = String(read(fmt_files[2]))
170186
@test occursin("driht", fmt_data)
187+
188+
comp_data = String(read(comp_files[1]))
189+
@test occursin("Info:", comp_data)
190+
@test !occursin("I:", comp_data)
191+
192+
compressed_data = String(read(compressed_files[1]))
193+
@test !occursin("Info:", compressed_data)
194+
@test occursin("I:", compressed_data)
171195
end
172196
end
173197

0 commit comments

Comments
 (0)