Skip to content

Commit 692e06b

Browse files
committed
added logging() for redirection of info/warn/error messages
1 parent d44977c commit 692e06b

12 files changed

+248
-26
lines changed

NEWS.md

+3
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ Library improvements
132132
implemented; the semantics are as if the `Nullable` were a container with
133133
zero or one elements ([#16961]).
134134

135+
* `logging` can be used to redirect `info`, `warn`, and `error` messages
136+
either universally or on a per-module/function basis ([#16213]).
137+
135138
Compiler/Runtime improvements
136139
-----------------------------
137140

base/REPL.jl

+1-12
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,6 @@ function ip_matches_func(ip, func::Symbol)
110110
return false
111111
end
112112

113-
function display_error(io::IO, er, bt)
114-
print_with_color(Base.error_color(), io, "ERROR: "; bold = true)
115-
# remove REPL-related frames from interactive printing
116-
eval_ind = findlast(addr->Base.REPL.ip_matches_func(addr, :eval), bt)
117-
if eval_ind != 0
118-
bt = bt[1:eval_ind-1]
119-
end
120-
showerror(IOContext(io, :limit => true), er, bt)
121-
end
122-
123113
immutable REPLDisplay{R<:AbstractREPL} <: Display
124114
repl::R
125115
end
@@ -144,8 +134,7 @@ function print_response(errio::IO, val::ANY, bt, show_value::Bool, have_color::B
144134
try
145135
Base.sigatomic_end()
146136
if bt !== nothing
147-
display_error(errio, val, bt)
148-
println(errio)
137+
Base.display_error(errio, val, bt)
149138
iserr, lasterr = false, ()
150139
else
151140
if val !== nothing && show_value

base/client.jl

+17-5
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,26 @@ function repl_cmd(cmd, out)
125125
nothing
126126
end
127127

128-
display_error(er) = display_error(er, [])
129-
function display_error(er, bt)
130-
print_with_color(Base.error_color(), STDERR, "ERROR: "; bold = true)
131-
with_output_color(Base.error_color(), STDERR) do io
132-
showerror(io, er, bt)
128+
function display_error(io::IO, er, bt)
129+
if !isempty(bt)
130+
st = stacktrace(bt)
131+
if !isempty(st)
132+
io = redirect(io, log_error_to, st[1])
133+
end
134+
end
135+
print_with_color(Base.error_color(), io, "ERROR: "; bold = true)
136+
# remove REPL-related frames from interactive printing
137+
eval_ind = findlast(addr->Base.REPL.ip_matches_func(addr, :eval), bt)
138+
if eval_ind != 0
139+
bt = bt[1:eval_ind-1]
140+
end
141+
with_output_color(Base.error_color(), io) do io
142+
showerror(IOContext(io, :limit => true), er, bt)
133143
println(io)
134144
end
135145
end
146+
display_error(er, bt) = display_error(STDERR, er, bt)
147+
display_error(er) = display_error(er, [])
136148

137149
function eval_user_input(ast::ANY, show_value)
138150
errcount, lasterr, bt = 0, (), nothing

base/error.jl

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
## native julia error handling ##
2020

2121
error(s::AbstractString) = throw(ErrorException(s))
22+
23+
"""
24+
error(msg...)
25+
26+
Raise an `ErrorException` with the given message.
27+
28+
See also [`logging`](@ref).
29+
"""
2230
error(s...) = throw(ErrorException(Main.Base.string(s...)))
2331

2432
rethrow() = ccall(:jl_rethrow, Bottom, ())

base/exports.jl

+1
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,7 @@ export
810810
isxdigit,
811811
join,
812812
lcfirst,
813+
logging,
813814
lowercase,
814815
lpad,
815816
lstrip,

base/stacktraces.jl

+7
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ function stacktrace(trace::Vector{Ptr{Void}}, c_funcs::Bool=false)
153153

154154
# Remove frame for this function (and any functions called by this function).
155155
remove_frames!(stack, :stacktrace)
156+
157+
# is there a better way? the func symbol has a number suffix which changes.
158+
# it's possible that no test is needed and we could just shift! all the time.
159+
# this line was added to PR #16213 because otherwise stacktrace() != stacktrace(false).
160+
# not sure why. possibly b/c of re-ordering of base/sysimg.jl
161+
!isempty(stack) && startswith(string(stack[1].func),"jlcall_stacktrace") && shift!(stack)
162+
stack
156163
end
157164

158165
stacktrace(c_funcs::Bool=false) = stacktrace(backtrace(), c_funcs)

base/sysimg.jl

+4-4
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ include("REPLCompletions.jl")
311311
include("REPL.jl")
312312
include("client.jl")
313313

314+
# Stack frames and traces
315+
include("stacktraces.jl")
316+
importall .StackTraces
317+
314318
# misc useful functions & macros
315319
include("util.jl")
316320

@@ -342,10 +346,6 @@ include("libgit2/libgit2.jl")
342346
# package manager
343347
include("pkg/pkg.jl")
344348

345-
# Stack frames and traces
346-
include("stacktraces.jl")
347-
importall .StackTraces
348-
349349
# profiler
350350
include("profile.jl")
351351
importall .Profile

base/util.jl

+95-1
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,88 @@ println_with_color(color::Union{Int, Symbol}, msg::AbstractString...; bold::Bool
335335

336336
## warnings and messages ##
337337

338+
const log_info_to = Dict{Tuple{Union{Module,Void},Union{Symbol,Void}},IO}()
339+
const log_warn_to = Dict{Tuple{Union{Module,Void},Union{Symbol,Void}},IO}()
340+
const log_error_to = Dict{Tuple{Union{Module,Void},Union{Symbol,Void}},IO}()
341+
342+
function _redirect(io::IO, log_to::Dict, sf::StackTraces.StackFrame)
343+
isnull(sf.linfo) && return io
344+
mod = get(sf.linfo).def.module
345+
fun = sf.func
346+
if haskey(log_to, (mod,fun))
347+
return log_to[(mod,fun)]
348+
elseif haskey(log_to, (mod,nothing))
349+
return log_to[(mod,nothing)]
350+
elseif haskey(log_to, (nothing,nothing))
351+
return log_to[(nothing,nothing)]
352+
else
353+
return io
354+
end
355+
end
356+
357+
function _redirect(io::IO, log_to::Dict, fun::Symbol)
358+
clos = string("#",fun,"#")
359+
kw = string("kw##",fun)
360+
local sf
361+
break_next_frame = false
362+
for trace in backtrace()
363+
stack::Vector{StackFrame} = StackTraces.lookup(trace)
364+
filter!(frame -> !frame.from_c, stack)
365+
for frame in stack
366+
isnull(frame.linfo) && continue
367+
sf = frame
368+
break_next_frame && (@goto skip)
369+
get(frame.linfo).def.module == Base || continue
370+
sff = string(frame.func)
371+
if frame.func == fun || startswith(sff, clos) || startswith(sff, kw)
372+
break_next_frame = true
373+
end
374+
end
375+
end
376+
@label skip
377+
_redirect(io, log_to, sf)
378+
end
379+
380+
@inline function redirect(io::IO, log_to::Dict, arg::Union{Symbol,StackTraces.StackFrame})
381+
if isempty(log_to)
382+
return io
383+
else
384+
if length(log_to)==1 && in((nothing,nothing),keys(log_to))
385+
return log_to[(nothing,nothing)]
386+
else
387+
return _redirect(io, log_to, arg)
388+
end
389+
end
390+
end
391+
392+
"""
393+
logging(io [, m [, f]][; kind=:all])
394+
logging([; kind=:all])
395+
396+
Stream output of informational, warning, and/or error messages to `io`,
397+
overriding what was otherwise specified. Optionally, divert stream only for
398+
module `m`, or specifically function `f` within `m`. `kind` can also be
399+
`:info`, `:warn`, or `:error`. See `Base.log_{info,warn,error}_to` for the
400+
current set of redirections. Call `logging` with no arguments (or just the
401+
`kind`) to reset everything.
402+
"""
403+
function logging(io::IO, m::Union{Module,Void}=nothing, f::Union{Symbol,Void}=nothing;
404+
kind::Symbol=:all)
405+
(kind==:all || kind==:info) && (log_info_to[(m,f)] = io)
406+
(kind==:all || kind==:warn) && (log_warn_to[(m,f)] = io)
407+
(kind==:all || kind==:error) && (log_error_to[(m,f)] = io)
408+
nothing
409+
end
410+
411+
function logging(; kind::Symbol=:all)
412+
(kind==:all || kind==:info) && empty!(log_info_to)
413+
(kind==:all || kind==:warn) && empty!(log_warn_to)
414+
(kind==:all || kind==:error) && empty!(log_error_to)
415+
nothing
416+
end
338417

339418
"""
340-
info(msg...; prefix="INFO: ")
419+
info([io, ] msg..., [prefix="INFO: "])
341420
342421
Display an informational message.
343422
Argument `msg` is a string describing the information to be displayed.
@@ -351,10 +430,14 @@ INFO: hello world
351430
julia> info("hello world"; prefix="MY INFO: ")
352431
MY INFO: hello world
353432
```
433+
434+
See also [`logging`](@ref).
354435
"""
355436
function info(io::IO, msg...; prefix="INFO: ")
437+
io = redirect(io, log_info_to, :info)
356438
print_with_color(info_color(), io, prefix; bold = true)
357439
println_with_color(info_color(), io, chomp(string(msg...)))
440+
return
358441
end
359442
info(msg...; prefix="INFO: ") = info(STDERR, msg..., prefix=prefix)
360443

@@ -365,6 +448,16 @@ const have_warned = Set()
365448
warn_once(io::IO, msg...) = warn(io, msg..., once=true)
366449
warn_once(msg...) = warn(STDERR, msg..., once=true)
367450

451+
"""
452+
warn([io, ] msg..., [prefix="WARNING: ", once=false, key=nothing, bt=nothing, filename=nothing, lineno::Int=0])
453+
454+
Display a warning. Argument `msg` is a string describing the warning to be
455+
displayed. Set `once` to true and specify a `key` to only display `msg` the
456+
first time `warn` is called. If `bt` is not `nothing` a backtrace is displayed.
457+
If `filename` is not `nothing` both it and `lineno` are displayed.
458+
459+
See also [`logging`](@ref).
460+
"""
368461
function warn(io::IO, msg...;
369462
prefix="WARNING: ", once=false, key=nothing, bt=nothing,
370463
filename=nothing, lineno::Int=0)
@@ -376,6 +469,7 @@ function warn(io::IO, msg...;
376469
(key in have_warned) && return
377470
push!(have_warned, key)
378471
end
472+
io = redirect(io, log_warn_to, :warn)
379473
print_with_color(warn_color(), io, prefix; bold = true)
380474
print_with_color(warn_color(), io, str)
381475
if bt !== nothing

doc/src/stdlib/io-network.md

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Base.println
6666
Base.print_with_color
6767
Base.info
6868
Base.warn
69+
Base.logging
6970
Base.Printf.@printf
7071
Base.Printf.@sprintf
7172
Base.sprint

test/misc.jl

+107
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,113 @@ let bt = backtrace()
2828
end
2929
end
3030

31+
# PR #16213
32+
module Logging
33+
function bar(io)
34+
info(io,"barinfo")
35+
warn(io,"barwarn")
36+
Base.display_error(io,"barerror",backtrace())
37+
end
38+
function pooh(io)
39+
info(io,"poohinfo")
40+
warn(io,"poohwarn")
41+
Base.display_error(io,"pooherror",backtrace())
42+
end
43+
end
44+
function foo(io)
45+
info(io,"fooinfo")
46+
warn(io,"foowarn")
47+
Base.display_error(io,"fooerror",backtrace())
48+
end
49+
50+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "WARNING: barwarn", "ERROR: \"barerror\""]))
51+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn", "ERROR: \"pooherror\""]))
52+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
53+
54+
55+
logging(DevNull, TestMain_misc.Logging, :bar; kind=:info)
56+
@test all(contains.(sprint(Logging.bar), ["WARNING: barwarn", "ERROR: \"barerror\""]))
57+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn", "ERROR: \"pooherror\""]))
58+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
59+
60+
logging(DevNull, TestMain_misc.Logging; kind=:info)
61+
@test all(contains.(sprint(Logging.bar), ["WARNING: barwarn", "ERROR: \"barerror\""]))
62+
@test all(contains.(sprint(Logging.pooh), ["WARNING: poohwarn", "ERROR: \"pooherror\""]))
63+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
64+
65+
logging(DevNull; kind=:info)
66+
@test all(contains.(sprint(Logging.bar), ["WARNING: barwarn", "ERROR: \"barerror\""]))
67+
@test all(contains.(sprint(Logging.pooh), ["WARNING: poohwarn", "ERROR: \"pooherror\""]))
68+
@test all(contains.(sprint(foo), ["WARNING: foowarn", "ERROR: \"fooerror\""]))
69+
70+
logging(kind=:info)
71+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "WARNING: barwarn", "ERROR: \"barerror\""]))
72+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn", "ERROR: \"pooherror\""]))
73+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
74+
75+
76+
logging(DevNull, TestMain_misc.Logging, :bar; kind=:warn)
77+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "ERROR: \"barerror\""]))
78+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn", "ERROR: \"pooherror\""]))
79+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
80+
81+
logging(DevNull, TestMain_misc.Logging; kind=:warn)
82+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "ERROR: \"barerror\""]))
83+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "ERROR: \"pooherror\""]))
84+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
85+
86+
logging(DevNull; kind=:warn)
87+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "ERROR: \"barerror\""]))
88+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "ERROR: \"pooherror\""]))
89+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "ERROR: \"fooerror\""]))
90+
91+
logging(kind=:warn)
92+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "WARNING: barwarn", "ERROR: \"barerror\""]))
93+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn", "ERROR: \"pooherror\""]))
94+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
95+
96+
97+
logging(DevNull, TestMain_misc.Logging, :bar; kind=:error)
98+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "WARNING: barwarn"]))
99+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn", "ERROR: \"pooherror\""]))
100+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
101+
102+
logging(DevNull, TestMain_misc.Logging; kind=:error)
103+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "WARNING: barwarn"]))
104+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn"]))
105+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
106+
107+
logging(DevNull; kind=:error)
108+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "WARNING: barwarn"]))
109+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn"]))
110+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn"]))
111+
112+
logging(kind=:error)
113+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "WARNING: barwarn"]))
114+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn"]))
115+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn"]))
116+
117+
118+
logging(DevNull, TestMain_misc.Logging, :bar)
119+
@test sprint(Logging.bar) == ""
120+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn", "ERROR: \"pooherror\""]))
121+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
122+
123+
logging(DevNull, TestMain_misc.Logging)
124+
@test sprint(Logging.bar) == ""
125+
@test sprint(Logging.pooh) == ""
126+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
127+
128+
logging(DevNull)
129+
@test sprint(Logging.bar) == ""
130+
@test sprint(Logging.pooh) == ""
131+
@test sprint(foo) == ""
132+
133+
logging()
134+
@test all(contains.(sprint(Logging.bar), ["INFO: barinfo", "WARNING: barwarn", "ERROR: \"barerror\""]))
135+
@test all(contains.(sprint(Logging.pooh), ["INFO: poohinfo", "WARNING: poohwarn", "ERROR: \"pooherror\""]))
136+
@test all(contains.(sprint(foo), ["INFO: fooinfo", "WARNING: foowarn", "ERROR: \"fooerror\""]))
137+
31138
# test assert() method
32139
@test_throws AssertionError assert(false)
33140
let res = assert(true)

0 commit comments

Comments
 (0)