Skip to content

Commit 11b5311

Browse files
Preserve structured logging metadata in @Resumable functions (#165)
* Preserve logging metadata in resumable functions * Allow x64 Julia on Apple Silicon CI runners --------- Co-authored-by: Stefan Krastanov <stefan@krastanov.org>
1 parent 9676438 commit 11b5311

7 files changed

Lines changed: 150 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
with:
5151
version: ${{ matrix.version }}
5252
arch: ${{ matrix.arch }}
53+
force-arch: true
5354
- uses: julia-actions/cache@v3
5455
- uses: julia-actions/julia-buildpkg@v1
5556
- uses: julia-actions/julia-runtest@v1

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# News
22

3+
## v1.0.7 - 2026-07-17
4+
5+
- Preserve standard logging macro metadata names, including `_group`, `event`,
6+
and shorthand fields, when logging from an `@resumable` function.
7+
38
## v1.0.6 - 2026-04-23
49

510
- (internal) switch to a reimplementation of `gensym` that is not replayable by JET.jl (see https://github.com/aviatesk/JET.jl/issues/814)

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ license = "MIT"
55
desc = "C# sharp style generators a.k.a. semi-coroutines for Julia."
66
authors = ["Ben Lauwens and volunteer maintainers"]
77
repo = "https://github.com/JuliaDynamics/ResumableFunctions.jl.git"
8-
version = "1.0.6"
8+
version = "1.0.7"
99

1010
[deps]
1111
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"

src/utils.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,45 @@ function scoping(s::Symbol, scope; new = false)
349349
return lookup_rhs!(s, scope)
350350
end
351351

352+
const _logging_macros = (
353+
Symbol("@debug"),
354+
Symbol("@info"),
355+
Symbol("@warn"),
356+
Symbol("@error"),
357+
Symbol("@logmsg"),
358+
)
359+
360+
function _is_logging_macro(name)
361+
name in _logging_macros
362+
end
363+
364+
function _is_logging_macro(name::Expr)
365+
name.head === :. || return false
366+
macro_name = name.args[end]
367+
macro_name = macro_name isa QuoteNode ? macro_name.value : macro_name
368+
macro_name in _logging_macros
369+
end
370+
371+
function _logging_macro_name(name)
372+
return name
373+
end
374+
375+
function _logging_macro_name(name::Expr)
376+
macro_name = name.args[end]
377+
return macro_name isa QuoteNode ? macro_name.value : macro_name
378+
end
379+
380+
function scope_logging_macro_arg(expr, scope; metadata = false)
381+
if expr isa Expr && expr.head === :(=)
382+
expr.args[2] = scoping(expr.args[2], scope)
383+
return expr
384+
end
385+
if metadata && expr isa Symbol
386+
return Expr(:(=), expr, scoping(expr, scope))
387+
end
388+
scoping(expr, scope)
389+
end
390+
352391
function scope_generator_inner(expr, scope)
353392
for i in 2:length(expr.args)
354393
!(expr.args[i] isa Expr && expr.args[i].head === :(=)) &&
@@ -487,6 +526,15 @@ function scoping(expr::Expr, scope)
487526
return expr
488527
end
489528
if expr.head === :macrocall
529+
if _is_logging_macro(expr.args[1])
530+
metadata_start = _logging_macro_name(expr.args[1]) === Symbol("@logmsg") ? 5 : 4
531+
for i in 2:length(expr.args)
532+
expr.args[i] = scope_logging_macro_arg(
533+
expr.args[i], scope; metadata = i >= metadata_start
534+
)
535+
end
536+
return expr
537+
end
490538
for i in 2:length(expr.args)
491539
expr.args[i] = scoping(expr.args[i], scope)
492540
end

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
33
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
44
ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7"
55
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
6+
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
67
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
78
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
89
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ println("Starting tests with $(Threads.nthreads()) threads out of `Sys.CPU_THREA
3737
@doset "selfreferencing_functional"
3838
@doset "coverage_preservation"
3939
@doset "performance"
40+
@doset "logging"
4041
VERSION >= v"1.8" && @doset "doctests"
4142
VERSION >= v"1.8" && @doset "aqua"
4243
isempty(VERSION.prerelease) && VERSION >= v"1.12" && @doset "jet"

test/test_logging.jl

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using Logging
2+
using ResumableFunctions
3+
using Test
4+
5+
mutable struct CaptureLogger <: AbstractLogger
6+
records::Vector{NamedTuple}
7+
end
8+
9+
Logging.min_enabled_level(::CaptureLogger) = Logging.Debug
10+
Logging.shouldlog(::CaptureLogger, _level, _module, _group, _id) = true
11+
Logging.catch_exceptions(::CaptureLogger) = false
12+
13+
function Logging.handle_message(
14+
logger::CaptureLogger,
15+
level,
16+
message,
17+
_module,
18+
group,
19+
id,
20+
file,
21+
line;
22+
kwargs...,
23+
)
24+
push!(logger.records, (; level, message, group, metadata=(; kwargs...)))
25+
end
26+
27+
@resumable function logging_generator()
28+
round = 3
29+
pair_id = 7
30+
attempts = 2
31+
@debug(
32+
"Logging from a resumable function",
33+
_group=:protocol,
34+
event=:round_started,
35+
round=round,
36+
pair_id,
37+
attempts,
38+
)
39+
@yield round
40+
end
41+
42+
@resumable function qualified_logging_generator()
43+
value = 4
44+
Logging.@info(
45+
"Qualified logging macro",
46+
_group=:qualified,
47+
event=:qualified_record,
48+
value,
49+
)
50+
@logmsg(
51+
Logging.Warn,
52+
"Logging with an explicit level",
53+
_group=:logmsg,
54+
event=:logmsg_record,
55+
value,
56+
)
57+
@yield value
58+
end
59+
60+
@testset "structured logging metadata" begin
61+
logger = CaptureLogger(NamedTuple[])
62+
values = Logging.with_logger(logger) do
63+
collect(logging_generator())
64+
end
65+
66+
@test values == [3]
67+
@test length(logger.records) == 1
68+
record = only(logger.records)
69+
@test record.level == Logging.Debug
70+
@test record.message == "Logging from a resumable function"
71+
@test record.group == :protocol
72+
@test record.metadata == (
73+
event=:round_started,
74+
round=3,
75+
pair_id=7,
76+
attempts=2,
77+
)
78+
79+
empty!(logger.records)
80+
values = Logging.with_logger(logger) do
81+
collect(qualified_logging_generator())
82+
end
83+
84+
@test values == [4]
85+
@test length(logger.records) == 2
86+
qualified_record, logmsg_record = logger.records
87+
@test qualified_record.level == Logging.Info
88+
@test qualified_record.group == :qualified
89+
@test qualified_record.metadata == (event=:qualified_record, value=4)
90+
@test logmsg_record.level == Logging.Warn
91+
@test logmsg_record.group == :logmsg
92+
@test logmsg_record.metadata == (event=:logmsg_record, value=4)
93+
end

0 commit comments

Comments
 (0)