|
| 1 | +using Dates |
| 2 | +import Base: isless |
| 3 | + |
| 4 | +raw""" |
| 5 | + DatetimeRotatingFileLogger |
| 6 | +
|
| 7 | +Constructs a FileLogger that rotates its file based on the current date. |
| 8 | +The filename pattern given is interpreted through the `Dates.format()` string formatter, |
| 9 | +allowing for yearly all the way down to millisecond-level log rotation. Note that if you |
| 10 | +wish to have a filename portion that is not interpreted as a format string, you may need |
| 11 | +to escape portions of the filename, as shown below: |
| 12 | +
|
| 13 | +Usage example: |
| 14 | +
|
| 15 | + logger = DatetimeRotatingFileLogger(log_dir, raw"\a\c\c\e\s\s-YYYY-mm-dd.\l\o\g") |
| 16 | +""" |
| 17 | +mutable struct DatetimeRotatingFileLogger <: AbstractLogger |
| 18 | + logger::SimpleLogger |
| 19 | + dir::String |
| 20 | + filename_pattern::DateFormat |
| 21 | + next_reopen_check::DateTime |
| 22 | + always_flush::Bool |
| 23 | +end |
| 24 | + |
| 25 | +function DatetimeRotatingFileLogger(dir, filename_pattern; always_flush=true) |
| 26 | + format = DateFormat(filename_pattern) |
| 27 | + return DatetimeRotatingFileLogger( |
| 28 | + SimpleLogger(open(calc_logpath(dir, filename_pattern), "a"), BelowMinLevel), |
| 29 | + dir, |
| 30 | + format, |
| 31 | + next_datetime_transition(format), |
| 32 | + always_flush, |
| 33 | + ) |
| 34 | +end |
| 35 | + |
| 36 | +function reopen!(drfl::DatetimeRotatingFileLogger) |
| 37 | + drfl.logger = SimpleLogger(open(calc_logpath(drfl.dir, drfl.filename_pattern), "a"), BelowMinLevel) |
| 38 | + drfl.next_reopen_check = next_datetime_transition(drfl.filename_pattern) |
| 39 | + return nothing |
| 40 | +end |
| 41 | + |
| 42 | +# I kind of wish these were defined in Dates |
| 43 | +isless(::Type{Millisecond}, ::Type{Millisecond}) = false |
| 44 | +isless(::Type{Millisecond}, ::Type{T}) where {T <: Dates.Period} = true |
| 45 | + |
| 46 | +isless(::Type{Second}, ::Type{Millisecond}) = false |
| 47 | +isless(::Type{Second}, ::Type{Second}) = false |
| 48 | +isless(::Type{Second}, ::Type{T}) where {T <: Dates.Period} = true |
| 49 | + |
| 50 | +isless(::Type{Minute}, ::Type{Millisecond}) = false |
| 51 | +isless(::Type{Minute}, ::Type{Second}) = false |
| 52 | +isless(::Type{Minute}, ::Type{Minute}) = false |
| 53 | +isless(::Type{Minute}, ::Type{T}) where {T <: Dates.Period} = true |
| 54 | + |
| 55 | +isless(::Type{Hour}, ::Type{Day}) = true |
| 56 | +isless(::Type{Hour}, ::Type{Month}) = true |
| 57 | +isless(::Type{Hour}, ::Type{Year}) = true |
| 58 | +isless(::Type{Hour}, ::Type{T}) where {T <: Dates.Period} = false |
| 59 | + |
| 60 | +isless(::Type{Day}, ::Type{Month}) = true |
| 61 | +isless(::Type{Day}, ::Type{Year}) = true |
| 62 | +isless(::Type{Day}, ::Type{T}) where {T <: Dates.Period} = false |
| 63 | + |
| 64 | +isless(::Type{Month}, ::Type{Year}) = true |
| 65 | +isless(::Type{Month}, ::Type{T}) where {T <: Dates.Period} = false |
| 66 | + |
| 67 | +isless(::Type{Year}, ::Type{T}) where {T <: Dates.Period} = false |
| 68 | + |
| 69 | +""" |
| 70 | + next_datetime_transition(fmt::DateFormat) |
| 71 | +
|
| 72 | +Given a DateFormat that is being applied to our filename, what is the next |
| 73 | +time at which our filepath will need to change? |
| 74 | +""" |
| 75 | +function next_datetime_transition(fmt::DateFormat) |
| 76 | + extract_token(x::Dates.DatePart{T}) where {T} = T |
| 77 | + token_timescales = Dict( |
| 78 | + # Milliseconds is the smallest timescale |
| 79 | + 's' => Millisecond, |
| 80 | + # Seconds |
| 81 | + 'S' => Second, |
| 82 | + # Minutes |
| 83 | + 'M' => Minute, |
| 84 | + # Hours |
| 85 | + 'I' => Hour, |
| 86 | + 'H' => Hour, |
| 87 | + # Days |
| 88 | + 'd' => Day, |
| 89 | + 'e' => Day, |
| 90 | + 'E' => Day, |
| 91 | + # Month |
| 92 | + 'm' => Month, |
| 93 | + 'u' => Month, |
| 94 | + 'U' => Month, |
| 95 | + # Year |
| 96 | + 'y' => Year, |
| 97 | + 'Y' => Year, |
| 98 | + ) |
| 99 | + |
| 100 | + tokens = filter(t -> isa(t, Dates.DatePart), collect(fmt.tokens)) |
| 101 | + minimum_timescale = minimum(map(t -> token_timescales[extract_token(t)], tokens)) |
| 102 | + return Dates.ceil(now(), minimum_timescale) - Second(1) |
| 103 | +end |
| 104 | + |
| 105 | +calc_logpath(dir, filename_pattern) = joinpath(dir, Dates.format(now(), filename_pattern)) |
| 106 | + |
| 107 | +function handle_message(drfl::DatetimeRotatingFileLogger, args...; kwargs...) |
| 108 | + if now() >= drfl.next_reopen_check |
| 109 | + flush(drfl.logger.stream) |
| 110 | + reopen!(drfl) |
| 111 | + end |
| 112 | + handle_message(drfl.logger, args...; kwargs...) |
| 113 | + drfl.always_flush && flush(drfl.logger.stream) |
| 114 | +end |
| 115 | + |
| 116 | +shouldlog(drfl::DatetimeRotatingFileLogger, arg...) = true |
| 117 | +min_enabled_level(drfl::DatetimeRotatingFileLogger) = BelowMinLevel |
| 118 | +catch_exceptions(drfl::DatetimeRotatingFileLogger) = catch_exceptions(drfl.logger) |
0 commit comments