betterlog 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +5 -0
- data/Dockerfile +46 -0
- data/Gemfile +5 -1
- data/LICENSE +10 -199
- data/Makefile +78 -0
- data/Rakefile +7 -3
- data/TODO.md +1 -0
- data/VERSION +1 -1
- data/betterlog/healthz.go +71 -0
- data/betterlog.gemspec +20 -10
- data/bin/betterlog +176 -175
- data/bin/betterlog_pusher +34 -0
- data/cmd/betterlog-server/LICENSE +13 -0
- data/cmd/betterlog-server/main.go +165 -0
- data/{log.yml → config/log.yml} +5 -35
- data/lib/betterlog/global_metadata.rb +9 -14
- data/lib/betterlog/log/event.rb +135 -133
- data/lib/betterlog/log/event_formatter.rb +99 -97
- data/lib/betterlog/log/severity.rb +38 -36
- data/lib/betterlog/log.rb +163 -146
- data/lib/betterlog/log_event_formatter.rb +41 -39
- data/lib/betterlog/logger.rb +88 -0
- data/lib/betterlog/notifiers.rb +28 -0
- data/lib/betterlog/railtie.rb +8 -0
- data/lib/betterlog/version.rb +1 -1
- data/lib/betterlog.rb +13 -7
- data/spec/betterlog/global_metadata_spec.rb +38 -0
- data/spec/betterlog/log_spec.rb +221 -0
- data/spec/betterlog/logger_spec.rb +65 -0
- data/spec/spec_helper.rb +13 -0
- metadata +82 -28
- data/betterdocs.gemspec +0 -53
- data/lib/betterdocs/version.rb +0 -8
- data/lib/betterlog/betterlog_railtie.rb +0 -5
data/lib/betterlog/log.rb
CHANGED
@@ -4,178 +4,195 @@ require 'betterlog/log/event'
|
|
4
4
|
require 'betterlog/log/event_formatter'
|
5
5
|
require 'betterlog/log/severity'
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
module Betterlog
|
8
|
+
class Log
|
9
|
+
include Tins::SexySingleton
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
end
|
11
|
+
class_attr_accessor :default_logger
|
12
|
+
self.default_logger = Logger.new(STDERR)
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
# @param object this object is logged
|
17
|
-
# @param **rest additional data is logged as well.
|
18
|
-
# @return [ Log ] this object itself.
|
19
|
-
def info(object, **rest)
|
20
|
-
protect do
|
21
|
-
emit Log::Event.ify(object, severity: __method__, rest: rest)
|
14
|
+
def logger
|
15
|
+
defined?(Rails) && Rails.logger || self.class.default_logger
|
22
16
|
end
|
23
|
-
end
|
24
17
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
18
|
+
# Logs a message on severity info.
|
19
|
+
#
|
20
|
+
# @param object this object is logged
|
21
|
+
# @param **rest additional data is logged as well.
|
22
|
+
# @return [ Log ] this object itself.
|
23
|
+
def info(object, **rest)
|
24
|
+
protect do
|
25
|
+
emit Log::Event.ify(object, severity: __method__, rest: rest)
|
26
|
+
end
|
33
27
|
end
|
34
|
-
end
|
35
28
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
29
|
+
# Logs a message on severity warn.
|
30
|
+
#
|
31
|
+
# @param object this object is logged
|
32
|
+
# @param **rest additional data is logged as well.
|
33
|
+
# @return [ Log ] this object itself.
|
34
|
+
def warn(object, **rest)
|
35
|
+
protect do
|
36
|
+
emit Log::Event.ify(object, severity: __method__, rest: rest)
|
37
|
+
end
|
44
38
|
end
|
45
|
-
end
|
46
39
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
40
|
+
# Logs a message on severity debug.
|
41
|
+
#
|
42
|
+
# @param object this object is logged
|
43
|
+
# @param **rest additional data is logged as well.
|
44
|
+
# @return [ Log ] this object itself.
|
45
|
+
def debug(object, **rest)
|
46
|
+
protect do
|
47
|
+
emit Log::Event.ify(object, severity: __method__, rest: rest)
|
48
|
+
end
|
55
49
|
end
|
56
|
-
end
|
57
50
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
51
|
+
# Logs a message on severity error.
|
52
|
+
#
|
53
|
+
# @param object this object is logged
|
54
|
+
# @param **rest additional data is logged as well.
|
55
|
+
# @return [ Log ] this object itself.
|
56
|
+
def error(object, **rest)
|
57
|
+
protect do
|
58
|
+
emit Log::Event.ify(object, severity: __method__, rest: rest)
|
59
|
+
end
|
66
60
|
end
|
67
|
-
end
|
68
61
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
62
|
+
# Logs a message on severity fatal.
|
63
|
+
#
|
64
|
+
# @param object this object is logged
|
65
|
+
# @param **rest additional data is logged as well.
|
66
|
+
# @return [ Log ] this object itself.
|
67
|
+
def fatal(object, **rest)
|
68
|
+
protect do
|
69
|
+
emit Log::Event.ify(object, severity: __method__, rest: rest)
|
70
|
+
end
|
78
71
|
end
|
79
|
-
end
|
80
72
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
event = build_metric(metric: metric, type: type, value: value, **rest)
|
92
|
-
emit event
|
73
|
+
# Logs a message on severity debug, by default, this can be changed by
|
74
|
+
# passing the severity: keyword.
|
75
|
+
#
|
76
|
+
# @param object this object is logged
|
77
|
+
# @param **rest additional data is logged as well.
|
78
|
+
# @return [ Log ] this object itself.
|
79
|
+
def output(object, **rest)
|
80
|
+
protect do
|
81
|
+
emit Log::Event.ify(object, severity: rest[:severity], rest: rest)
|
82
|
+
end
|
93
83
|
end
|
94
|
-
end
|
95
84
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
rescue => error
|
110
|
-
e = Log::Event.ify(error)
|
111
|
-
rest |= e.as_hash.subhash(:error_class, :backtrace, :message)
|
112
|
-
rest[:message] = "#{rest[:message]} while measuring metric #{metric}"
|
113
|
-
raise error
|
114
|
-
ensure
|
115
|
-
protect do
|
116
|
-
event = build_metric(metric: metric, type: 'seconds', value: timed_duration, **rest)
|
117
|
-
emit event
|
85
|
+
# Logs a metric on severity debug, by default, this can be changed by passing
|
86
|
+
# the severity: keyword.
|
87
|
+
#
|
88
|
+
# @param metric the name of the recorded metric.
|
89
|
+
# @param type of the recorded metric.
|
90
|
+
# @param value of the recorded metric.
|
91
|
+
# @param **rest additional rest is logged as well.
|
92
|
+
# @return [ Log ] this object itself.
|
93
|
+
def metric(metric:, type:, value:, **rest)
|
94
|
+
protect do
|
95
|
+
event = build_metric(metric: metric, type: type, value: value, **rest)
|
96
|
+
emit event
|
97
|
+
end
|
118
98
|
end
|
119
|
-
end
|
120
99
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
100
|
+
# Logs a time measure on severity debug, by default, this can be changed by
|
101
|
+
# passing the severity: keyword.
|
102
|
+
#
|
103
|
+
# If an error occurs during measurement details about it are added to the
|
104
|
+
# metric event.
|
105
|
+
#
|
106
|
+
# @param metric the name of the recorded metric.
|
107
|
+
# @param **rest additional rest is logged as well.
|
108
|
+
# @param block the block around which the measure is teaken.
|
109
|
+
# @return [ Log ] this object itself.
|
110
|
+
def measure(metric:, **rest, &block)
|
111
|
+
raise ArgumentError, 'must be called with a block' unless block_given?
|
112
|
+
time_block { yield }
|
113
|
+
rescue => error
|
114
|
+
e = Log::Event.ify(error)
|
115
|
+
rest |= e.as_hash.subhash(:error_class, :backtrace, :message)
|
116
|
+
rest[:message] = "#{rest[:message]} while measuring metric #{metric}"
|
117
|
+
raise error
|
118
|
+
ensure
|
119
|
+
protect do
|
120
|
+
event = build_metric(metric: metric, type: 'seconds', value: timed_duration, **rest)
|
121
|
+
emit event
|
122
|
+
end
|
135
123
|
end
|
136
|
-
self
|
137
|
-
end
|
138
124
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
type: type,
|
148
|
-
value: value,
|
149
|
-
} | rest,
|
150
|
-
severity: severity
|
151
|
-
)
|
152
|
-
end
|
125
|
+
def context(data_hash)
|
126
|
+
GlobalMetadata.add data_hash
|
127
|
+
self
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.context(data_hash)
|
131
|
+
instance.context(data_hash)
|
132
|
+
end
|
153
133
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
134
|
+
private
|
135
|
+
|
136
|
+
def protect
|
137
|
+
yield
|
138
|
+
rescue => e
|
139
|
+
begin
|
140
|
+
# Try logging e once by ourselves
|
141
|
+
emit Log::Event.ify(e, severity: :fatal)
|
142
|
+
rescue
|
143
|
+
# Ok, I give up let's use logger directly instead
|
144
|
+
logger.fatal(
|
145
|
+
"Crashed during logging with #{e.class}: #{e.message}):\n"\
|
146
|
+
"#{e.backtrace * ?\n}"
|
147
|
+
)
|
148
|
+
end
|
149
|
+
self
|
150
|
+
end
|
151
|
+
|
152
|
+
def build_metric(metric:, type:, value:, **rest)
|
153
|
+
severity = rest.fetch(:severity, :debug)
|
154
|
+
rest |= {
|
155
|
+
message: "a metric #{metric} of type #{type}",
|
156
|
+
}
|
157
|
+
Log::Event.ify(
|
158
|
+
{
|
159
|
+
metric: metric,
|
160
|
+
type: type,
|
161
|
+
value: value,
|
162
|
+
} | rest,
|
163
|
+
severity: severity
|
164
|
+
)
|
160
165
|
end
|
161
|
-
|
162
|
-
|
166
|
+
|
167
|
+
def emit(event)
|
168
|
+
l = caller_locations.reverse_each.each_cons(3).find { |c, n1, n2|
|
169
|
+
n2.absolute_path =~ /betterlog\/log\.rb/ and break c # TODO check if this still works
|
170
|
+
}
|
171
|
+
if l
|
172
|
+
event[:location] = [ l.absolute_path, l.lineno ] * ?:
|
173
|
+
end
|
174
|
+
event[:emitter] = self.class.name.downcase
|
163
175
|
notify(event)
|
176
|
+
logger.send(event.severity.to_sym, event.to_json)
|
177
|
+
self
|
178
|
+
ensure
|
179
|
+
GlobalMetadata.data.clear
|
164
180
|
end
|
165
|
-
logger.send(event.severity.to_sym, event.to_json)
|
166
|
-
self
|
167
|
-
end
|
168
181
|
|
169
|
-
|
170
|
-
|
171
|
-
|
182
|
+
def notify(event)
|
183
|
+
if event.notify?
|
184
|
+
Notifiers.notify(event)
|
185
|
+
self
|
186
|
+
end
|
187
|
+
end
|
172
188
|
|
173
|
-
|
189
|
+
thread_local :timed_duration
|
174
190
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
191
|
+
def time_block
|
192
|
+
s = Time.now
|
193
|
+
yield
|
194
|
+
ensure
|
195
|
+
self.timed_duration = Time.now - s
|
196
|
+
end
|
180
197
|
end
|
181
198
|
end
|
@@ -1,47 +1,49 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Betterlog
|
2
|
+
class LogEventFormatter < ActiveSupport::Logger::Formatter
|
3
|
+
include ActiveSupport::TaggedLogging::Formatter
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
5
|
+
def emitter
|
6
|
+
'legacy'
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
9
|
+
def call(severity, timestamp, program, message)
|
10
|
+
super
|
11
|
+
message = message.to_s
|
12
|
+
if cc.log.legacy_supported
|
13
|
+
if message.blank?
|
14
|
+
return ''
|
15
|
+
elsif !Log::Event.is?(message)
|
16
|
+
m = message.sub(/\s+$/, '')
|
17
|
+
timestamp = timestamp.utc.iso8601(3)
|
18
|
+
event = Log::Event.new(
|
19
|
+
emitter: emitter,
|
20
|
+
timestamp: timestamp,
|
21
|
+
message: m,
|
22
|
+
severity: severity.to_s.downcase,
|
23
|
+
# tags: current_tags,
|
24
|
+
)
|
25
|
+
if backtrace = m.grep(/^\s*([^:]+):(\d+)/)
|
26
|
+
if backtrace.size > 1
|
27
|
+
event[:backtrace] = backtrace.map(&:chomp)
|
28
|
+
event[:message] = 'a logged backtrace'
|
29
|
+
end
|
28
30
|
end
|
31
|
+
if l = caller_locations.reverse_each.each_cons(2).find { |c, n|
|
32
|
+
n.absolute_path =~ /\/lib\/ruby\/.*?\/logger\.rb/ and break c
|
33
|
+
}
|
34
|
+
then
|
35
|
+
event[:location] = [ l.absolute_path, l.lineno ] * ?:
|
36
|
+
end
|
37
|
+
program and event[:program] = program
|
38
|
+
message = event.to_json
|
29
39
|
end
|
30
|
-
if l = caller_locations.reverse_each.each_cons(2).find { |c, n|
|
31
|
-
n.absolute_path =~ /\/lib\/ruby\/.*?\/logger\.rb/ and break c
|
32
|
-
}
|
33
|
-
then
|
34
|
-
event[:location] = [ l.absolute_path, l.lineno ] * ?:
|
35
|
-
end
|
36
|
-
program and event[:program] = program
|
37
|
-
message = event.to_json
|
38
40
|
end
|
41
|
+
rescue => e
|
42
|
+
Betterlog::Log.logger.error(e)
|
43
|
+
ensure
|
44
|
+
# Do not "message << ?\n" - A frozn string may be passed in
|
45
|
+
message.end_with?(?\n) or message = "#{message}\n"
|
46
|
+
return message
|
39
47
|
end
|
40
|
-
rescue => e
|
41
|
-
Rails.logger.error(e)
|
42
|
-
ensure
|
43
|
-
# Do not "message << ?\n" - A frozn string may be passed in
|
44
|
-
message.end_with?(?\n) or message = "#{message}\n"
|
45
|
-
return message
|
46
48
|
end
|
47
49
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Betterlog
|
4
|
+
class Logger < ::Logger
|
5
|
+
def initialize(redis, shift_age = 0, shift_size = 1048576, name: nil, **opts)
|
6
|
+
@redis = redis
|
7
|
+
@name = name || self.class.name
|
8
|
+
super(@logdev, shift_age, shift_size, **opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
private def redis_write(msg)
|
12
|
+
# Redis string limit is at 512MB, stop before that after warning a lot.
|
13
|
+
if @redis.strlen(@name) > 511 * 1024 ** 2
|
14
|
+
return nil
|
15
|
+
end
|
16
|
+
if @redis.strlen(@name) > 510 * 1024 ** 2
|
17
|
+
@redis.append @name, "\nRedis memory limit will soon be reached =>"\
|
18
|
+
" Log output to redis stops now unless log data is pushed away!\n"
|
19
|
+
return nil
|
20
|
+
end
|
21
|
+
@redis.append @name, msg
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def add(severity, message = nil, progname = nil)
|
27
|
+
severity ||= UNKNOWN
|
28
|
+
if severity < @level
|
29
|
+
return true
|
30
|
+
end
|
31
|
+
if progname.nil?
|
32
|
+
progname = @progname
|
33
|
+
end
|
34
|
+
if message.nil?
|
35
|
+
if block_given?
|
36
|
+
message = yield
|
37
|
+
else
|
38
|
+
message = progname
|
39
|
+
progname = @progname
|
40
|
+
end
|
41
|
+
end
|
42
|
+
redis_write(
|
43
|
+
format_message(format_severity(severity), Time.now, progname, message))
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def <<(msg)
|
48
|
+
redis_write(msg)
|
49
|
+
end
|
50
|
+
|
51
|
+
def clear
|
52
|
+
@redis.del @name
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def each_chunk(chunk_size: 100 * 1024, &block)
|
57
|
+
chunk_size > 0 or raise ArgumentError, 'chunk_size > 0 required'
|
58
|
+
@redis.exists(@name) or return Enumerator.new {}
|
59
|
+
Enumerator.new do |y|
|
60
|
+
name_tmp = "#{@name}_#{rand}"
|
61
|
+
@redis.rename @name, name_tmp
|
62
|
+
s = 0
|
63
|
+
e = @redis.strlen(name_tmp) - 1
|
64
|
+
until s > e
|
65
|
+
y.yield @redis.getrange(name_tmp, s, s + chunk_size - 1)
|
66
|
+
s += chunk_size
|
67
|
+
end
|
68
|
+
@redis.del name_tmp
|
69
|
+
end.each(&block)
|
70
|
+
end
|
71
|
+
|
72
|
+
def each(chunk_size: 100 * 1024, &block)
|
73
|
+
chunk_size > 0 or raise ArgumentError, 'chunk_size > 0 required'
|
74
|
+
Enumerator.new do |y|
|
75
|
+
buffer = ''
|
76
|
+
each_chunk(chunk_size: chunk_size) do |chunk|
|
77
|
+
buffer << chunk
|
78
|
+
buffer.gsub!(/\A(.*?#$/)/) do |line|
|
79
|
+
y.yield(line)
|
80
|
+
''
|
81
|
+
end
|
82
|
+
end
|
83
|
+
buffer.length > 0 and y.yield(buffer)
|
84
|
+
end.each(&block)
|
85
|
+
end
|
86
|
+
include Enumerable
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Betterlog
|
2
|
+
module Notifiers
|
3
|
+
|
4
|
+
class_attr_accessor :notifiers
|
5
|
+
|
6
|
+
self.notifiers = Set[]
|
7
|
+
|
8
|
+
def self.register(notifier)
|
9
|
+
notifier.respond_to?(:notify) or raise TypeError,
|
10
|
+
"notifier has to respond to notify(message, hash) interface"
|
11
|
+
notifiers << notifier
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.notify(event)
|
16
|
+
notifiers.each do |notifier|
|
17
|
+
notifier.notify(event.notify?, event.as_hash)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.context(data_hash)
|
22
|
+
notifiers.each do |notifier|
|
23
|
+
notifier.respond_to?(:context) or next
|
24
|
+
notifier.context(data_hash)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/betterlog/version.rb
CHANGED
data/lib/betterlog.rb
CHANGED
@@ -2,13 +2,19 @@ require 'tins/xt'
|
|
2
2
|
require 'json'
|
3
3
|
require 'logger'
|
4
4
|
require 'time'
|
5
|
-
require 'tins'
|
6
|
-
require 'rails'
|
7
|
-
|
8
|
-
require 'active_support'
|
9
5
|
require 'term/ansicolor'
|
10
6
|
|
11
|
-
|
12
|
-
|
7
|
+
module Betterlog
|
8
|
+
end
|
9
|
+
|
13
10
|
require 'betterlog/log'
|
14
|
-
require 'betterlog/
|
11
|
+
require 'betterlog/notifiers'
|
12
|
+
require 'betterlog/global_metadata'
|
13
|
+
require 'betterlog/logger'
|
14
|
+
|
15
|
+
if defined? Rails
|
16
|
+
require 'betterlog/log_event_formatter'
|
17
|
+
require 'betterlog/railtie'
|
18
|
+
end
|
19
|
+
|
20
|
+
Log = Betterlog::Log
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Betterlog::GlobalMetadata do
|
4
|
+
let :notifier do
|
5
|
+
Class.new do
|
6
|
+
def notify(message, hash) end
|
7
|
+
|
8
|
+
def context(data_hash) end
|
9
|
+
end.new
|
10
|
+
end
|
11
|
+
|
12
|
+
around do |example|
|
13
|
+
Betterlog::Notifiers.register(notifier)
|
14
|
+
example.run
|
15
|
+
ensure
|
16
|
+
Betterlog::Notifiers.notifiers.clear
|
17
|
+
described_class.data.clear
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'can haz empty data' do
|
21
|
+
expect(described_class.data).to eq({})
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'can haz some data' do
|
25
|
+
described_class.data |= { foo: 'bar' }
|
26
|
+
expect(described_class.data).to eq({ foo: 'bar' })
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'can "add" data' do
|
30
|
+
expect(notifier).to receive(:context).with(foo: 'bar')
|
31
|
+
expect(described_class.add(foo: 'bar')).to eq described_class.instance
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'can "add" data via Log.context' do
|
35
|
+
expect(notifier).to receive(:context).with(foo: 'bar')
|
36
|
+
Betterlog::Log.context(foo: 'bar')
|
37
|
+
end
|
38
|
+
end
|