-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
main.cr
189 lines (160 loc) · 5.14 KB
/
main.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Log
# Creates a `Log` for the given source.
# If *level* is given, it will override the configuration.
def self.for(source : String, level : Severity? = nil) : Log
log = builder.for(source)
log.level = level if level
log
end
# Creates a `Log` for the given nested source.
# If *level* is given, it will override the configuration.
def for(child_source : String, level : Severity? = nil) : Log
return ::Log.for(child_source) if source.blank?
return ::Log.for(source) if child_source.blank?
::Log.for("#{source}.#{child_source}", level)
end
# Creates a `Log` for the given type.
# A type `Foo::Bar(Baz)` corresponds to the source `foo.bar`.
# If *level* is given, it will override the configuration.
def self.for(type : Class, level : Severity? = nil) : Log
source = type.name.underscore.gsub("::", ".")
# remove generic arguments
paren = source.index('(')
source = source[0...paren] if paren
::Log.for(source, level)
end
# :ditto:
def for(type : Class, level : Severity? = nil) : Log
::Log.for(type, level)
end
private Top = Log.for("")
{% for method in %i(trace debug info notice warn error fatal) %}
# See `Log#{{method.id}}`.
def self.{{method.id}}(*, exception : Exception? = nil)
Top.{{method.id}}(exception: exception) do |dsl|
yield dsl
end
end
{% end %}
@@builder = Builder.new
at_exit { @@builder.close }
# Returns the default `Log::Builder` used for `Log.for` calls.
def self.builder
@@builder
end
# Returns the current fiber logging context.
def self.context : Log::Context
Log::Context.new(Fiber.current.logging_context)
end
# Sets the current fiber logging context.
def self.context=(value : Log::Metadata)
Fiber.current.logging_context = value
end
# :ditto:
def self.context=(value : Log::Context)
# NOTE: There is a need for `Metadata` and `Context` setters in
# because `Log.context` returns a `Log::Context` for allowing DSL like `Log.context.set(a: 1)`
# but if the metadata is built manually the construct `Log.context = metadata` will be used.
Log.context = value.metadata
end
# Returns the current fiber logging context.
def context : Log::Context
Log.context
end
# Sets the current fiber logging context.
def context=(value : Log::Metadata | Log::Context)
Log.context = value
end
# Method to save and restore the current logging context.
#
# ```
# Log.context.set a: 1
# Log.info { %(message with {"a" => 1} context) }
# Log.with_context do
# Log.context.set b: 2
# Log.info { %(message with {"a" => 1, "b" => 2} context) }
# end
# Log.info { %(message with {"a" => 1} context) }
# ```
def self.with_context
previous = Log.context
begin
yield
ensure
Log.context = previous
end
end
# :ditto:
def with_context
self.class.with_context do
yield
end
end
struct Context
getter metadata : Metadata
def initialize(@metadata : Metadata)
end
# Clears the current `Fiber` logging context.
#
# ```
# Log.context.clear
# Log.info { "message with empty context" }
# ```
def clear
Fiber.current.logging_context = @metadata = Log::Metadata.empty
end
# Extends the current `Fiber` logging context.
#
# ```
# Log.context.set a: 1
# Log.context.set b: 2
# Log.info { %q(message with a: 1, b: 2 context") }
# h = {:c => "3"}
# Log.context.set extra: h
# Log.info { %q(message with a: 1, b: 2, extra: {"c" => "3"} context) }
# h = {"c" => 3}
# Log.context.set extra: h
# Log.info { %q(message with a: 1, b: 2, extra: {"c" => 3} context) }
# ```
def set(**kwargs)
extend_fiber_context(Fiber.current, kwargs)
end
# :ditto:
def set(values)
extend_fiber_context(Fiber.current, values)
end
private def extend_fiber_context(fiber : Fiber, values)
context = fiber.logging_context
fiber.logging_context = @metadata = context.extend(values)
end
end
# Helper DSL module for emitting log entries with data.
struct Emitter
# :nodoc:
def initialize(@source : String, @severity : Severity, @exception : Exception?)
end
# Emits a logs entry with a message, and data attached to
#
# ```
# Log.info &.emit("Program started") # No data, same as Log.info { "Program started" }
# Log.info &.emit("User logged in", user_id: 42) # With entry data
# Log.info &.emit(action: "Logged in", user_id: 42) # Empty string message, only data
# Log.error exception: ex, &.emit("Oopps", account: {id: 42}) # With data and exception
# ```
def emit(message : String) : Entry
emit(message, Metadata.empty)
end
def emit(message : String, **kwargs) : Entry
emit(message, kwargs)
end
def emit(message : String, data : Metadata | Hash | NamedTuple) : Entry
Entry.new(@source, @severity, message, Metadata.build(data), @exception)
end
def emit(**kwargs) : Entry
emit(kwargs)
end
def emit(data : Metadata | Hash | NamedTuple) : Entry
emit("", Metadata.build(data))
end
end
end