Skip to content

Commit 7a2b014

Browse files
authored
Merge pull request #7 from tanmaykm/tan/misc
add RollingFileWriterTee
2 parents 915f0fc + 8a5c8f6 commit 7a2b014

File tree

5 files changed

+106
-9
lines changed

5 files changed

+106
-9
lines changed

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The LogRoller.jl package is licensed under the MIT "Expat" License:
22

3-
> Copyright (c) 2019-2020: Tanmay Mohapatra, Julia Computing
3+
> Copyright (c) 2020-2021: Tanmay Mohapatra, Julia Computing
44
>
55
> Permission is hereby granted, free of charge, to any person obtaining
66
> a copy of this software and associated documentation files (the

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ uuid = "c41e01d8-14e5-11ea-185b-e7eabed7be4b"
33
keywords = ["log", "rotate", "roller", "logrotate"]
44
license = "MIT"
55
authors = ["Tanmay Mohapatra <[email protected]>"]
6-
version = "0.2.2"
6+
version = "0.3.0"
77

88
[compat]
99
julia = "1"

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ Rotates files as below:
2828
- ...
2929
- `<filename>_n.gz` : last rotated file is discarded when rotated
3030

31+
32+
## `RollingFileWriterTee`
33+
34+
Tees raw log entries made a RollingFileWriter on to a Julia `AbstractLogger`.
35+
36+
Each line of text is taken as a single log message.
37+
38+
All log entries are made with the same log level, which can be provided during construction. It leaves
39+
further examination/parsing of log messages (to extract parameters, or detect exact log levels) to the
40+
downstream logger.
41+
42+
Constructor parameters in addition to those for `RollingFileWriter`:
43+
- `logger`: instance of AbstractLogger to tee log entries to
44+
- `assumed_level`: level of the log messages to assume (default Info)
45+
46+
3147
## `RollingLogger`
3248

3349
A logger that implements `AbstractLogger` interface and uses a `RollingFileWriter` to provide log rotation.
@@ -66,6 +82,16 @@ julia> io = RollingFileWriter("/tmp/mylog.log", 1000, 3);
6682
julia> run(pipeline(`myshellscript.sh`; stdout=io, stderr=io));
6783
```
6884

85+
Using `RollingFileWriterTee`
86+
87+
```julia
88+
julia> using LogRoller, Logging
89+
90+
julia> io = RollingFileWriterTee("/tmp/mylog.log", 1000, 3, ConsoleLogger(stderr));
91+
92+
julia> run(pipeline(`myshellscript.sh`; stdout=io, stderr=io));
93+
```
94+
6995
Using `RollingLogger`
7096

7197
```julia

src/LogRoller.jl

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ using Logging
77

88
import Logging: shouldlog, min_enabled_level, catch_exceptions, handle_message
99
import Base: write, close, rawhandle
10-
export RollingLogger, RollingFileWriter, postrotate
10+
export RollingLogger, RollingFileWriter, RollingFileWriterTee, postrotate
1111

1212
const BUFFSIZE = 1024*16 # try and read 16K pages when possible
1313

@@ -28,12 +28,14 @@ mutable struct RollingFileWriter <: IO
2828
lck::ReentrantLock
2929
procstream::Union{Nothing,Pipe}
3030
procstreamer::Union{Nothing,Task}
31+
procstreamteelogger::Union{Nothing,AbstractLogger}
32+
assumed_level::LogLevel
3133
postrotate::Union{Nothing,Function}
3234

3335
function RollingFileWriter(filename::String, sizelimit::Int, nfiles::Int)
3436
stream = open(filename, "a")
3537
filesize = stat(stream).size
36-
new(filename, sizelimit, nfiles, filesize, stream, ReentrantLock(), nothing, nothing, nothing)
38+
new(filename, sizelimit, nfiles, filesize, stream, ReentrantLock(), nothing, nothing, nothing, Logging.Info, nothing)
3739
end
3840
end
3941

@@ -47,6 +49,15 @@ function postrotate(fn::Function, io::RollingFileWriter)
4749
nothing
4850
end
4951

52+
"""
53+
Tee all lines to the provided logger
54+
"""
55+
function tee(io::RollingFileWriter, logger::AbstractLogger, level::LogLevel)
56+
io.procstreamteelogger = logger
57+
io.assumed_level = level
58+
io
59+
end
60+
5061
"""
5162
Close any open file handle and streams.
5263
A closed object must not be used again.
@@ -57,6 +68,7 @@ function close(io::RollingFileWriter)
5768
lock(io.lck) do
5869
io.procstream = nothing
5970
io.procstreamer = nothing
71+
io.procstreamteelogger = nothing
6072
end
6173
end
6274
close(io.stream)
@@ -128,6 +140,24 @@ function rotate_file(io::RollingFileWriter)
128140
nothing
129141
end
130142

143+
"""
144+
Tees raw log entries made a RollingFileWriter on to a provided Julia AbstractLogger.
145+
146+
Each line of text is taken as a single log message.
147+
148+
All log entries are made with the same log level, which can be provided during construction. It leaves
149+
further examination/parsing of log messages (to extract parameters, or detect exact log levels) to the
150+
downstream logger.
151+
"""
152+
function RollingFileWriterTee(filename::String, sizelimit::Int, nfiles::Int, logger::AbstractLogger, assumed_level::LogLevel=Logging.Info)
153+
io = RollingFileWriter(filename, sizelimit, nfiles)
154+
RollingFileWriterTee(io, logger, assumed_level)
155+
end
156+
157+
function RollingFileWriterTee(io::RollingFileWriter, logger::AbstractLogger, assumed_level::LogLevel=Logging.Info)
158+
tee(io, logger, assumed_level)
159+
end
160+
131161
"""
132162
RollingLogger(filename, sizelimit, nfiles, min_level=Info; timestamp_identifier::Symbol=:time)
133163
Log into a log file. Rotate log file based on file size. Compress rotated logs.
@@ -205,10 +235,15 @@ end
205235

206236
function stream_process_logs(writer::RollingFileWriter)
207237
try
208-
bytes = readavailable(writer.procstream)
209-
while !isempty(bytes)
210-
write(writer, bytes)
211-
bytes = readavailable(writer.procstream)
238+
while true
239+
logline = readline(writer.procstream; keep=true)
240+
if !isempty(logline)
241+
write(writer, logline)
242+
if writer.procstreamteelogger !== nothing
243+
@logmsg(writer.assumed_level, strip(logline))
244+
end
245+
end
246+
eof(writer.procstream) && break
212247
end
213248
finally
214249
close(writer.procstream)
@@ -225,7 +260,13 @@ function rawhandle(writer::RollingFileWriter)
225260
writer.procstream = Pipe()
226261
Base.link_pipe!(writer.procstream)
227262
writer.procstreamer = @async begin
228-
stream_process_logs(writer)
263+
if writer.procstreamteelogger !== nothing
264+
with_logger(writer.procstreamteelogger) do
265+
stream_process_logs(writer)
266+
end
267+
else
268+
stream_process_logs(writer)
269+
end
229270
end
230271
end
231272
return rawhandle(Base.pipe_writer(writer.procstream))

test/runtests.jl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,35 @@ function test_filewriter()
7373
end
7474
end
7575

76+
function test_pipelined_tee()
77+
mktempdir() do logdir
78+
filename1 = "test1.log"
79+
filename2 = "test2.log"
80+
filepath1 = joinpath(logdir, filename1)
81+
filepath2 = joinpath(logdir, filename2)
82+
@test !isfile(filepath1)
83+
@test !isfile(filepath2)
84+
85+
logger1io = open(filepath1, "w+")
86+
logger1 = SimpleLogger(logger1io)
87+
logger2 = RollingFileWriterTee(filepath2, 1000, 3, logger1, Logging.Info)
88+
89+
julia = joinpath(Sys.BINDIR, "julia")
90+
cmd = pipeline(`$julia -e 'for i in 1:5 println(string("hello",i)) end; flush(stdout)'`; stdout=logger2, stderr=logger2)
91+
run(cmd)
92+
93+
close(logger2)
94+
close(logger1io)
95+
96+
@test isfile(filepath1)
97+
@test isfile(filepath2)
98+
@test (2*length(readlines(filepath2))) == length(readlines(filepath1)) # Julia logger entry will be two lines for every raw message
99+
if !Sys.iswindows() # streams do not seem to be flushed cleanly on Windows
100+
@test length(readlines(filepath2)) == 5
101+
end
102+
end
103+
end
104+
76105
function test_logger()
77106
mktempdir() do logdir
78107
filename = "test.log"
@@ -249,6 +278,7 @@ function test_timestamp_handling()
249278
end
250279

251280
test_filewriter()
281+
test_pipelined_tee()
252282
test_logger()
253283
test_process_streams()
254284
test_postrotate()

0 commit comments

Comments
 (0)