Skip to content

Commit 4a04639

Browse files
committed
implement LogCompose
1 parent 6485cc3 commit 4a04639

File tree

6 files changed

+247
-0
lines changed

6 files changed

+247
-0
lines changed

Project.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name = "LogCompose"
2+
uuid = "8416b438-731e-11ea-2421-05f642269042"
3+
keywords = ["configure", "compose", "logging", "logger"]
4+
authors = ["tan <[email protected]>"]
5+
license = "MIT"
6+
desc = "Compose loggers and logger ensembles declaratively using configuration files"
7+
version = "0.1.0"
8+
9+
[deps]
10+
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
11+
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
12+
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
13+
SyslogLogging = "0c4b0c42-68ec-11ea-3bc9-e7fb6e00ea0f"
14+
LogRoller = "c41e01d8-14e5-11ea-185b-e7eabed7be4b"
15+
LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36"
16+
17+
[compat]
18+
julia = "1"
19+
SyslogLogging = "0.1"
20+
LoggingExtras = "0.4"
21+
LogRoller = "0.2"
22+
23+
[extras]
24+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
25+
26+
[targets]
27+
test = ["Test"]

src/LogCompose.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module LogCompose
2+
3+
using Pkg, Logging
4+
5+
export logcompose
6+
7+
include("compose.jl")
8+
include("connectors.jl")
9+
10+
end # module LogCompose

src/compose.jl

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Logging, LogRoller, SyslogLogging, LoggingExtras
2+
3+
name_parts(loggername) = split(loggername, '.')
4+
5+
function configuration(configfile::String; section::String="")
6+
config = Pkg.TOML.parsefile(configfile)
7+
isempty(section) ? config : get_section(config, [section])
8+
end
9+
10+
function get_section(config::Dict{String,Any}, path::Vector)
11+
isempty(path) && (return config)
12+
13+
top = get(config, first(path)) do
14+
Dict{String,Any}()
15+
end
16+
17+
(length(path) > 1) ? get_section(top, path[2:end]) : top
18+
end
19+
20+
function flatten(config::Dict{String,Any}, path::Vector)
21+
result = Dict{String,Any}()
22+
for depth in 1:length(path)
23+
merge!(result, get_section(config, path[1:depth]))
24+
end
25+
result
26+
end
27+
28+
function get_type(s::String, topmodule::Module=@__MODULE__)
29+
try
30+
T = topmodule
31+
for t in split(s, ".")
32+
T = Base.eval(T, Symbol(t))
33+
end
34+
return T
35+
catch ex
36+
nextmodule = parentmodule(topmodule)
37+
if nextmodule == topmodule
38+
error("Could not resolve logger $s. Ensure that the required logging packages are imported!")
39+
else
40+
return get_type(s, nextmodule)
41+
end
42+
end
43+
end
44+
45+
logger(configfile::String, loggername::String; section::String="") = logger(configuration(configfile; section=section), name_parts(loggername))
46+
logger(config::Dict{String,Any}, loggername::String) = logger(config, name_parts(loggername))
47+
function logger(config::Dict{String,Any}, loggername::Vector)
48+
loggercfg = flatten(config, loggername)
49+
loggertypestr = loggercfg["type"]
50+
loggertype = get_type(loggertypestr)
51+
logcompose(loggertype, config, loggercfg)
52+
end
53+
54+
logcompose(::Type{T}, config::Dict{String,Any}, logger_config::Dict{String,Any}) where {T} = @error("logcompose not implemented for type $T")
55+
56+
log_min_level(logger_config::Dict{String,Any}, default::String="Info") = getproperty(Logging, Symbol(get(logger_config, "min_level", default)))

src/connectors.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
module Connectors
2+
3+
using ..LogCompose
4+
import ..LogCompose: logcompose, log_min_level
5+
6+
using Logging, LogRoller, SyslogLogging, LoggingExtras, Sockets
7+
8+
#--------------------------------------------------------------------
9+
# The connectors for individual logger implementations should ideally
10+
# be in their own packages.
11+
#--------------------------------------------------------------------
12+
13+
function logcompose(::Type{Logging.SimpleLogger}, config::Dict{String,Any}, logger_config::Dict{String,Any})
14+
level = log_min_level(logger_config, "Info")
15+
16+
streamname = strip(get(logger_config, "stream", "stdout"))
17+
@assert !isempty(streamname)
18+
19+
stream = streamname == "stdout" ? stdout :
20+
streamname == "stderr" ? stderr :
21+
open(streamname, "w+")
22+
23+
Logging.SimpleLogger(stream, level)
24+
end
25+
26+
const sysloglck = ReentrantLock()
27+
function logcompose(::Type{SyslogLogging.SyslogLogger}, config::Dict{String,Any}, logger_config::Dict{String,Any})
28+
ident = logger_config["identity"]
29+
level = log_min_level(logger_config, "Info")
30+
31+
kwargs = Dict{Symbol,Any}()
32+
33+
kwargs[:facility] = Symbol(get(logger_config, "facility", "user"))
34+
35+
if get(logger_config, "lock", false)
36+
kwargs[:lck] = sysloglck
37+
end
38+
39+
server_type = get(logger_config, "server_type", "local")
40+
if (server_type == "tcp") || (server_type == "udp")
41+
kwargs[:tcp] = (server_type == "tcp")
42+
if haskey(logger_config, "server_host")
43+
kwargs[:host] = Sockets.IPv4(logger_config["server_host"])
44+
end
45+
if haskey(logger_config, "server_port")
46+
kwargs[:port] = parse(Int, logger_config["server_port"])
47+
end
48+
end
49+
SyslogLogging.SyslogLogger(ident, level; kwargs...)
50+
end
51+
52+
function logcompose(::Type{LogRoller.RollingLogger}, config::Dict{String,Any}, logger_config::Dict{String,Any})
53+
filename = String(strip(logger_config["filename"]))
54+
@assert !isempty(filename)
55+
56+
level = log_min_level(logger_config, "Info")
57+
sizelimit = parse(Int, get(logger_config, "sizelimit", "10240000"))
58+
nfiles = parse(Int, get(logger_config, "nfiles", "5"))
59+
timestamp_identifier = Symbol(get(logger_config, "timestamp_identifier", "time"))
60+
61+
LogRoller.RollingLogger(filename, sizelimit, nfiles, level; timestamp_identifier=timestamp_identifier)
62+
end
63+
64+
function logcompose(::Type{LoggingExtras.TeeLogger}, config::Dict{String,Any}, logger_config::Dict{String,Any})
65+
destinations = [LogCompose.logger(config, dest) for dest in logger_config["destinations"]]
66+
LoggingExtras.TeeLogger(destinations...)
67+
end
68+
69+
end # module Connectors

test/runtests.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using LogCompose, Test, Logging, LogRoller, SyslogLogging, LoggingExtras
2+
3+
function test()
4+
config = joinpath(@__DIR__, "testapp.toml")
5+
default_logfile = "/tmp/default.log"
6+
rotated_logfile = "/tmp/testapp.log"
7+
rm(rotated_logfile; force=true)
8+
rm(default_logfile; force=true)
9+
10+
let logger = LogCompose.logger(config, "default"; section="loggers")
11+
with_logger(logger) do
12+
@info("testdefault")
13+
end
14+
@test isfile(default_logfile)
15+
end
16+
17+
let logger = LogCompose.logger(config, "file.testapp"; section="loggers")
18+
with_logger(logger) do
19+
@info("testroller")
20+
end
21+
@test isfile(rotated_logfile)
22+
end
23+
24+
let logger = LogCompose.logger(config, "syslog.testapp"; section="loggers")
25+
with_logger(logger) do
26+
@info("testsyslog")
27+
end
28+
end
29+
30+
let logger = LogCompose.logger(config, "testapp"; section="loggers")
31+
with_logger(logger) do
32+
@info("testtee")
33+
end
34+
end
35+
36+
log_file_contents = readlines(default_logfile)
37+
@test findfirst("testdefault", log_file_contents[1]) !== nothing
38+
39+
log_file_contents = readlines(rotated_logfile)
40+
@test findfirst("testroller", log_file_contents[1]) !== nothing
41+
@test findfirst("testtee", log_file_contents[3]) !== nothing
42+
end
43+
44+
test()

test/testapp.toml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[loggers.default]
2+
type = "Logging.SimpleLogger"
3+
# min_level = "Debug" # Debug, Info (default) or Error
4+
stream = "/tmp/default.log" # stdout (default), stderr or a filepath
5+
6+
[loggers.file]
7+
type = "LogRoller.RollingLogger"
8+
# sizelimit = 10240000
9+
# nfiles = 5
10+
# min_level = "Debug" # Debug, Info (default) or Error
11+
# timestamp_identifier = "time"
12+
13+
[loggers.syslog]
14+
type = "SyslogLogging.SyslogLogger"
15+
# min_level = "Info" # default is Info
16+
# facility = "user" # one of the facility types listed in Syslogs.jl (default is user)
17+
# server_type = "tcp" # tcp or udp or local (default)
18+
# server_host =
19+
# server_port =
20+
# lock = false # whether to lock the syslog facility while logging
21+
22+
# [loggers.stackdriver]
23+
# type = "StackDriver.StackdriverLogger"
24+
# min_level = "Info" # default is Info
25+
# project_name =
26+
# log_name =
27+
# key_file_path =
28+
# access_token =
29+
30+
[loggers.file.testapp]
31+
filename = "/tmp/testapp.log"
32+
33+
[loggers.syslog.testapp]
34+
identity = "testapp"
35+
36+
[loggers.stackdriver.testapp]
37+
log_name = "testapp"
38+
39+
[loggers.testapp]
40+
type = "LoggingExtras.TeeLogger"
41+
destinations = ["file.testapp", "syslog.testapp"]

0 commit comments

Comments
 (0)