hubbado-log 1.5.0 → 2.0.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/ChangeLog.md +109 -0
- data/README.md +70 -24
- data/hubbado-log.gemspec +1 -1
- data/lib/hubbado/log/controls/log_handler.rb +15 -10
- data/lib/hubbado/log/display.rb +17 -0
- data/lib/hubbado/log/log_handler.rb +6 -1
- data/lib/hubbado/log/logger/substitute.rb +41 -22
- data/lib/hubbado/log/logger.rb +12 -24
- data/lib/hubbado/log/notify_rollbar.rb +3 -1
- data/lib/hubbado/log/rails_logger.rb +5 -1
- data/lib/hubbado/log/stderr_logger.rb +8 -1
- data/lib/hubbado/log/tags.rb +1 -1
- data/lib/hubbado/log.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 583b7b7da5c4402a941dc3383961ac3af980078123d1ea2618ee89dced2d431b
|
|
4
|
+
data.tar.gz: 8268fb5b20c2ba01a9b402cf6a6ea950139bb34e5573bf666e38a6689187acec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8893a6eb651bfbecef02fb88d0b0c51c468f77ae946ddf57522858e5a82fe4aea7d3468d80e1023fd15fc0b11973e0f8bc6104eb113d774a29d941b0b5010f4
|
|
7
|
+
data.tar.gz: d448240a1a0f37894a8e79d1558406cf16d327c8d99a7b5222ba29dd1285dc35680c2311b8d31ea33af4d07e32ac2c905225562953c978611d91f55ff8a7fb4e
|
data/ChangeLog.md
CHANGED
|
@@ -4,6 +4,115 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
# [2.0.0 - 2026-08-29]
|
|
8
|
+
## Fixed
|
|
9
|
+
- `LOG_TAGS` and `LOG_LEVEL` no longer decide whether a failure is reported.
|
|
10
|
+
Both were applied in `Logger`, above the handler fan-out, so an operator
|
|
11
|
+
narrowing the log to the step they were debugging, or raising the level to cut
|
|
12
|
+
what an unattended log costs to keep, also stopped `NotifyRollbar` filing the
|
|
13
|
+
incident. A crash in any other step went unreported.
|
|
14
|
+
|
|
15
|
+
`StderrLogger` and `RailsLogger` now ask `Display` for themselves before
|
|
16
|
+
writing. `NotifyRollbar` does not ask, so it is reached whatever the settings
|
|
17
|
+
say, and declines anything below `warn` on its own as before.
|
|
18
|
+
|
|
19
|
+
- A failure written with a String severity is given a stacktrace, as one written
|
|
20
|
+
with a symbol always was. `#log` validates the severity as a symbol but
|
|
21
|
+
compared it raw when deciding whether to synthesise a caller stack, so
|
|
22
|
+
`Hubbado::Log.log('error', msg)` was accepted, logged, and arrived with no
|
|
23
|
+
stack under it.
|
|
24
|
+
|
|
25
|
+
Latent until now — every call site across Hubbado names its severity as a
|
|
26
|
+
symbol — and worth fixing here because such a message could previously be
|
|
27
|
+
filtered away entirely. Now that every warning and error reaches Rollbar, it
|
|
28
|
+
would instead be filed as an item nobody can act on.
|
|
29
|
+
|
|
30
|
+
## Added
|
|
31
|
+
- `Hubbado::Log::Display`, which answers whether the operator asked to be shown
|
|
32
|
+
a message. A handler that writes where a person reads asks it; one that
|
|
33
|
+
reports an incident does not.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
class MyHandler < Hubbado::Log::LogHandler
|
|
37
|
+
def log(subject, severity, message, data = nil, stacktrace = nil, tags = nil)
|
|
38
|
+
return unless Hubbado::Log::Display.shows?(severity, tags)
|
|
39
|
+
|
|
40
|
+
...
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A printing handler that forgets to ask prints everything, whatever the
|
|
46
|
+
operator set.
|
|
47
|
+
|
|
48
|
+
- `LogHandler#traces?(severity, tags)`, asked before a stacktrace is made.
|
|
49
|
+
`Kernel.caller` costs more than the rest of a log call put together — around
|
|
50
|
+
19µs at a Rails stack depth against 0.8µs — and it was being paid for messages
|
|
51
|
+
no handler would use.
|
|
52
|
+
|
|
53
|
+
It answers `true`, so a handler written before this existed is asked nothing
|
|
54
|
+
and keeps the stacktrace it always had. `StderrLogger` answers `false` below
|
|
55
|
+
`error`, which is where it stops printing one; `NotifyRollbar` answers `false`
|
|
56
|
+
below `warn`, which is where it stops reporting.
|
|
57
|
+
|
|
58
|
+
| | Was | Is now |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| an error the tag list left out | 29.2µs | 1.9µs |
|
|
61
|
+
| a warning on a terminal | 19µs | 1.6µs |
|
|
62
|
+
|
|
63
|
+
The second of those is older than this release: the gem synthesised a
|
|
64
|
+
stacktrace for every `warn`, and `StderrLogger` has always declined to print
|
|
65
|
+
one below `error`.
|
|
66
|
+
|
|
67
|
+
- The logger substitute names a line by its message and its tags, not only by
|
|
68
|
+
its severity. `logged?`, `messages` and `logged` all take the same criteria.
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
assert logger.logged?(:info, message: /handed back/, tags: %i[rescoring sweep])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Named together rather than one at a time, because a run writes several lines
|
|
75
|
+
and a message and a tag list asserted apart can each be true of a different
|
|
76
|
+
one. Before this, a spec asking about a line's tags reached past the
|
|
77
|
+
substitute into `invocations.first.arguments.fetch(:tag)` — which bound the
|
|
78
|
+
assertion to which keyword the call site happened to use, so rewriting
|
|
79
|
+
`tag: :claim` to `tags: [:claim]` broke a spec without changing any behaviour.
|
|
80
|
+
|
|
81
|
+
`message:` matches a String in full or a Regexp in part. `tags:` takes one
|
|
82
|
+
symbol or a list, reads both keywords as the logger does, and names every tag
|
|
83
|
+
the line carries and no others, in any order.
|
|
84
|
+
|
|
85
|
+
Entries answered by `logged` carry `tags` alongside `severity`, `message` and
|
|
86
|
+
`data`.
|
|
87
|
+
|
|
88
|
+
## Breaking
|
|
89
|
+
- **`LOG_LEVEL` and `LOG_TAGS` no longer hold Rollbar volume down.** That is the
|
|
90
|
+
point of the fix above, and it is a change to what an operator can do: a
|
|
91
|
+
process running `LOG_LEVEL=error`, or a narrowed list, to keep the incident
|
|
92
|
+
count low loses that lever on upgrade. Every `warn` and above now files,
|
|
93
|
+
whatever the settings say, and `NotifyRollbar`'s own `warn` floor is the only
|
|
94
|
+
one left. Rollbar's own rate limiting is where volume is held now.
|
|
95
|
+
- **`LogHandler#log` takes a sixth argument**, the tags the message was written
|
|
96
|
+
with, as symbols. A handler defining five parameters raises when called — and
|
|
97
|
+
raises from inside the logging call, so it takes down the operation being
|
|
98
|
+
logged rather than only the log line. A keyword with a default would not have
|
|
99
|
+
softened this: Ruby folds a trailing hash into a sixth positional for a method
|
|
100
|
+
that declares no keywords, and it raises identically.
|
|
101
|
+
- **The per-logger `level:` and `tags:` overrides are gone.** `Logger.new` takes
|
|
102
|
+
a subject and its handlers, and nothing else; a handler reads the process's
|
|
103
|
+
configuration. Nothing across Hubbado set either — only this gem's own
|
|
104
|
+
controls did. This is a deliberate divergence from Eventide's log gem, which
|
|
105
|
+
keeps that seam: restoring it would mean putting the effective level and list
|
|
106
|
+
back onto the handler contract that this release widened.
|
|
107
|
+
- **`Controls::LogHandler.attach` and `.logger` no longer take `level:` or
|
|
108
|
+
`tags:`.** The control records rather than displays, so nothing filters what a
|
|
109
|
+
spec attaching one can read.
|
|
110
|
+
|
|
111
|
+
## Changed
|
|
112
|
+
- A message tagged `:*` now means only "display this whatever the list says",
|
|
113
|
+
which is what it means in Eventide's log gem. It is no longer what keeps an
|
|
114
|
+
incident alive, because nothing can silence one.
|
|
115
|
+
|
|
7
116
|
# [1.5.0 - 2026-08-16]
|
|
8
117
|
## Added
|
|
9
118
|
- Three handlers, replacing eight hand-rolled copies across four projects and
|
data/README.md
CHANGED
|
@@ -65,7 +65,9 @@ the application's — a Rails logger, a Rollbar token. Write one by subclassing
|
|
|
65
65
|
|
|
66
66
|
```ruby
|
|
67
67
|
class MyHandler < Hubbado::Log::LogHandler
|
|
68
|
-
def log(subject, severity, message, data = nil, stacktrace = nil)
|
|
68
|
+
def log(subject, severity, message, data = nil, stacktrace = nil, tags = nil)
|
|
69
|
+
return unless Hubbado::Log::Display.shows?(severity, tags)
|
|
70
|
+
|
|
69
71
|
...
|
|
70
72
|
end
|
|
71
73
|
end
|
|
@@ -73,7 +75,21 @@ end
|
|
|
73
75
|
|
|
74
76
|
`data` is whatever the call site passed as the second argument, and is an `Exception` when it
|
|
75
77
|
logged one. `stacktrace` is the exception's `full_message` in that case, and otherwise the caller
|
|
76
|
-
stack, synthesised for `warn`, `error`, `fatal` and `unknown` only.
|
|
78
|
+
stack, synthesised for `warn`, `error`, `fatal` and `unknown` only. `tags` is what the message was
|
|
79
|
+
tagged with, as symbols.
|
|
80
|
+
|
|
81
|
+
**A handler that writes where a person reads asks `Display.shows?` first**, and one that reports
|
|
82
|
+
an incident does not — see [Display and reporting](#display-and-reporting).
|
|
83
|
+
|
|
84
|
+
A handler is also asked whether it would use a stacktrace, before one is made:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
def traces?(severity, tags = nil) = Hubbado::Log::Display.shows?(severity, tags)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It answers `true` unless you say otherwise, so a handler that ignores it keeps what it always had.
|
|
91
|
+
Answering honestly is worth it — `Kernel.caller` costs more than the rest of a log call put
|
|
92
|
+
together, and a message no handler traces should not pay for it.
|
|
77
93
|
|
|
78
94
|
### `Hubbado::Log::StderrLogger`
|
|
79
95
|
|
|
@@ -180,7 +196,8 @@ back.
|
|
|
180
196
|
|
|
181
197
|
## Level
|
|
182
198
|
|
|
183
|
-
A message below the level reaches
|
|
199
|
+
A message below the level is not displayed. It still reaches a handler that reports — see
|
|
200
|
+
[Display and reporting](#display-and-reporting).
|
|
184
201
|
|
|
185
202
|
```ruby
|
|
186
203
|
Hubbado::Log.configuration do |config|
|
|
@@ -206,9 +223,6 @@ Levels, lowest first:
|
|
|
206
223
|
| `error` | Message logged just prior to raising an error |
|
|
207
224
|
| `fatal` | Message recorded, when possible, as the process is terminating due to an error |
|
|
208
225
|
|
|
209
|
-
A single logger can name its own with
|
|
210
|
-
`Hubbado::Log::Logger.new(subject, handlers, level: :debug)`.
|
|
211
|
-
|
|
212
226
|
`LOG_LEVEL` is shared with Eventide's log gem, which writes names this gem does not
|
|
213
227
|
know. A name that is not one of `debug`, `info`, `warn`, `error`, `fatal` or
|
|
214
228
|
`unknown` leaves the level at `info` rather than raising.
|
|
@@ -228,7 +242,7 @@ logger.trace('Row read', tag: :data)
|
|
|
228
242
|
|
|
229
243
|
### `LOG_TAGS`
|
|
230
244
|
|
|
231
|
-
Which tagged messages are
|
|
245
|
+
Which tagged messages are displayed is decided by `LOG_TAGS`, a comma-separated list:
|
|
232
246
|
|
|
233
247
|
$ LOG_TAGS='_untagged,-data,billing,invoicing' ./my-command
|
|
234
248
|
|
|
@@ -247,20 +261,14 @@ Eventide's behaviour, kept deliberately so one `LOG_TAGS` means the same thing t
|
|
|
247
261
|
|
|
248
262
|
Tags compose with the level rather than replacing it: both filters have to pass, so a tag
|
|
249
263
|
cannot raise a message above the level and the level cannot rescue one the list leaves out.
|
|
250
|
-
|
|
251
|
-
A single logger can name its own list, as it can name its own level, so one component can be
|
|
252
|
-
read without turning up everything around it:
|
|
253
|
-
|
|
254
|
-
```ruby
|
|
255
|
-
Hubbado::Log::Logger.new(subject, handlers, level: :trace, tags: '_all')
|
|
256
|
-
```
|
|
264
|
+
Both decide what is displayed, and neither decides what is reported.
|
|
257
265
|
|
|
258
266
|
The syntax and its behaviour are Eventide's log gem, copied deliberately so that a string an
|
|
259
267
|
operator writes means the same thing in both codebases. Two consequences of that are worth
|
|
260
268
|
knowing before adopting tags:
|
|
261
269
|
|
|
262
|
-
- **`LOG_TAGS` is an allow-list.** A tagged message is
|
|
263
|
-
a tag to a call site therefore *
|
|
270
|
+
- **`LOG_TAGS` is an allow-list.** A tagged message is displayed only if the list names it. Adding
|
|
271
|
+
a tag to a call site therefore *hides* that message everywhere `LOG_TAGS` has not been
|
|
264
272
|
updated — cron, CI and production included. Ship the variable with the tag.
|
|
265
273
|
- **There is no way to mute one concern and keep the rest.** `-name` subtracts only from
|
|
266
274
|
messages an include has already matched, and `_all` is answered before any exclusion, so
|
|
@@ -271,6 +279,27 @@ knowing before adopting tags:
|
|
|
271
279
|
list decides for both, and an application whose messages are all untagged goes silent unless the
|
|
272
280
|
list contains `_untagged`.
|
|
273
281
|
|
|
282
|
+
## Display and reporting
|
|
283
|
+
|
|
284
|
+
`LOG_LEVEL` and `LOG_TAGS` decide what is displayed, not what is reported: an operator narrowing
|
|
285
|
+
to the step they are debugging is asking to be shown less, not for a crash elsewhere to go
|
|
286
|
+
unreported.
|
|
287
|
+
|
|
288
|
+
The logger fans every message out to every handler. One that writes where a person reads asks
|
|
289
|
+
first; one that reports does not:
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
Hubbado::Log::Display.shows?(severity, tags) # => true if the level and the list both admit it
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`StderrLogger` and `RailsLogger` ask. `NotifyRollbar` does not, and declines below `warn` itself.
|
|
296
|
+
|
|
297
|
+
**A printing handler that forgets to ask prints everything.** That is the thing to remember when
|
|
298
|
+
writing one.
|
|
299
|
+
|
|
300
|
+
Tagging a `warn` no longer hides it from Rollbar, only from the terminal, and `:*` now means
|
|
301
|
+
"always display" — what it means in Eventide's log gem.
|
|
302
|
+
|
|
274
303
|
## Reading back what a class logged
|
|
275
304
|
|
|
276
305
|
A spec assigns a substitute where the class's logger goes, and then asks what the class said:
|
|
@@ -299,23 +328,40 @@ assert logger.logged?(:error)
|
|
|
299
328
|
It records what it was told rather than writing, so no handler is involved and neither the
|
|
300
329
|
configured level nor `LOG_TAGS` decides what can be read back.
|
|
301
330
|
|
|
302
|
-
Three questions, each taking
|
|
331
|
+
Three questions, each taking the same criteria:
|
|
303
332
|
|
|
304
333
|
| Call | Answers |
|
|
305
334
|
|---|---|
|
|
306
|
-
| `logged?`
|
|
307
|
-
| `messages`
|
|
308
|
-
| `logged`
|
|
335
|
+
| `logged?` | whether a line matching was written |
|
|
336
|
+
| `messages` | what those lines said — the message strings, in order |
|
|
337
|
+
| `logged` | everything about them |
|
|
338
|
+
|
|
339
|
+
| Criterion | Names a line by |
|
|
340
|
+
|---|---|
|
|
341
|
+
| a severity, positionally | `logged?(:warn)` |
|
|
342
|
+
| `message:` | a String matching in full, or a Regexp matching part |
|
|
343
|
+
| `tags:` | the tags it carries, compared as a set — one symbol or a list |
|
|
344
|
+
|
|
345
|
+
**Name them together rather than one at a time.** A run writes several lines, and a message and a
|
|
346
|
+
tag list asserted apart can each be true of a different one:
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
assert logger.logged?(:info, message: /handed back/, tags: %i[rescoring sweep])
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`tags:` names every tag the line carries and no others, in any order. It reads both keywords as
|
|
353
|
+
the logger does, so a call site rewritten from `tag: :claim` to `tags: [:claim]` — a change with
|
|
354
|
+
no behaviour in it — does not break the spec.
|
|
309
355
|
|
|
310
|
-
`logged` answers with entries carrying `severity`, `message` and `
|
|
311
|
-
needs more than
|
|
356
|
+
`logged` answers with entries carrying `severity`, `message`, `data` and `tags`, for the assertion
|
|
357
|
+
that needs more than a yes:
|
|
312
358
|
|
|
313
359
|
```ruby
|
|
314
360
|
assert logger.logged(:error).first.data.equal?(exception)
|
|
315
361
|
```
|
|
316
362
|
|
|
317
|
-
`messages` and `logged?` are both derived from `logged`, so the three cannot disagree about
|
|
318
|
-
|
|
363
|
+
`messages` and `logged?` are both derived from `logged`, so the three cannot disagree about which
|
|
364
|
+
lines are being talked about.
|
|
319
365
|
|
|
320
366
|
A severity reaches a logger two ways — `logger.warn('…')` names it as the method,
|
|
321
367
|
`logger.log(:warn, '…')` as an argument — and both answer the same question, compared as symbols.
|
data/hubbado-log.gemspec
CHANGED
|
@@ -8,10 +8,11 @@ module Hubbado
|
|
|
8
8
|
@messages ||= []
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
# Replaces a class's logger with one writing here, keeping its subject.
|
|
12
|
-
#
|
|
13
|
-
# read: it is asking what the class said, and the process's filters are
|
|
14
|
-
|
|
11
|
+
# Replaces a class's logger with one writing here, keeping its subject. This handler
|
|
12
|
+
# records rather than displays, so the operator's settings do not decide what a spec
|
|
13
|
+
# attaching one can read: it is asking what the class said, and the process's filters are
|
|
14
|
+
# not its subject.
|
|
15
|
+
def self.attach(instance)
|
|
15
16
|
logger = instance.logger
|
|
16
17
|
|
|
17
18
|
if logger.nil?
|
|
@@ -20,24 +21,28 @@ module Hubbado
|
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
new.tap do |handler|
|
|
23
|
-
instance.logger = Log::Logger.new(logger.subject, [handler]
|
|
24
|
+
instance.logger = Log::Logger.new(logger.subject, [handler])
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
# For a class handed a logger rather than carrying one, and for a spec that wants both.
|
|
28
|
-
def self.logger(subject =
|
|
29
|
+
def self.logger(subject = nil)
|
|
30
|
+
subject ||= Subject.example
|
|
29
31
|
handler = new
|
|
30
32
|
|
|
31
|
-
[handler, Log::Logger.new(subject, [handler]
|
|
33
|
+
[handler, Log::Logger.new(subject, [handler])]
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
def log(subject, severity, message, data = nil, stacktrace = nil)
|
|
36
|
+
def log(subject, severity, message, data = nil, stacktrace = nil, tags = nil)
|
|
37
|
+
tags ||= []
|
|
38
|
+
|
|
35
39
|
messages << {
|
|
36
40
|
subject: subject,
|
|
37
41
|
severity: severity,
|
|
38
42
|
message: message,
|
|
39
43
|
data: data,
|
|
40
|
-
stacktrace: stacktrace
|
|
44
|
+
stacktrace: stacktrace,
|
|
45
|
+
tags: tags
|
|
41
46
|
}
|
|
42
47
|
end
|
|
43
48
|
|
|
@@ -57,7 +62,7 @@ module Hubbado
|
|
|
57
62
|
|
|
58
63
|
# The most recent message. Derived rather than assigned alongside `messages`, so the two
|
|
59
64
|
# cannot disagree about which message is the latest.
|
|
60
|
-
%i[subject severity message data stacktrace].each do |field|
|
|
65
|
+
%i[subject severity message data stacktrace tags].each do |field|
|
|
61
66
|
define_method(field) { messages.last&.fetch(field) }
|
|
62
67
|
end
|
|
63
68
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Hubbado
|
|
2
|
+
class Log
|
|
3
|
+
# Whether the operator asked to be shown a message. A handler that prints asks this; one that
|
|
4
|
+
# reports an incident does not, so narrowing a log cannot silence a failure.
|
|
5
|
+
class Display
|
|
6
|
+
# Both have to pass: a tag cannot raise a message above the level, nor the level rescue one
|
|
7
|
+
# the list leaves out.
|
|
8
|
+
def self.shows?(severity, tags = nil)
|
|
9
|
+
config = Log.config
|
|
10
|
+
|
|
11
|
+
return false if SEVERITIES.fetch(severity.to_sym) < SEVERITIES.fetch(config.level)
|
|
12
|
+
|
|
13
|
+
config.tags.write?(tags)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
module Hubbado
|
|
2
2
|
class Log
|
|
3
3
|
class LogHandler
|
|
4
|
-
|
|
4
|
+
# Whether this handler would use a synthesised stacktrace. Answered before one is made,
|
|
5
|
+
# because Kernel.caller costs more than the rest of a log call put together. True here, so a
|
|
6
|
+
# handler written before this existed is asked nothing and keeps what it always had.
|
|
7
|
+
def traces?(_severity, _tags = nil) = true
|
|
8
|
+
|
|
9
|
+
def log(_subject, _severity, _msg, _data = nil, _stacktrace = nil, _tags = nil)
|
|
5
10
|
raise NotImplementedError
|
|
6
11
|
end
|
|
7
12
|
end
|
|
@@ -4,46 +4,65 @@ module Hubbado
|
|
|
4
4
|
# What a logger was told rather than what it wrote. Extended onto a mimic of Logger, so a
|
|
5
5
|
# class under test is handed something that answers as a logger and keeps what it was given.
|
|
6
6
|
module Substitute
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
# One line, as the logger read it rather than as the call site typed it.
|
|
8
|
+
Entry = Data.define(:severity, :message, :data, :tags) do
|
|
9
|
+
# As a symbol, because #log takes a String as readily.
|
|
10
|
+
def at?(name) = severity == name.to_s.to_sym
|
|
11
|
+
|
|
12
|
+
# A String names the line in full; a Regexp names enough of it to tell it from the
|
|
13
|
+
# others, without writing a record id into the spec.
|
|
14
|
+
def says?(pattern)
|
|
15
|
+
return pattern.match?(message.to_s) if pattern.is_a?(Regexp)
|
|
16
|
+
|
|
17
|
+
message == pattern
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Every tag the line carries, and no others. Order is not compared: a call site writes
|
|
21
|
+
# them in whatever order reads well, and the allow-list never sees one either.
|
|
22
|
+
def tagged?(names) = tags.uniq.sort == names.uniq.sort
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Everything about what a class said, in order, narrowed by whichever criteria a spec
|
|
26
|
+
# names. Named together, because a run writes several lines and a message and a tag list
|
|
27
|
+
# asserted apart can each be true of a different one.
|
|
28
|
+
def logged(severity = nil, message: nil, tags: nil)
|
|
16
29
|
entries = invocations.map { |invocation| entry(invocation) }
|
|
17
30
|
|
|
18
|
-
|
|
31
|
+
named = symbols(tags)
|
|
19
32
|
|
|
20
|
-
entries.select { |entry| entry.severity
|
|
33
|
+
entries = entries.select { |entry| entry.at?(severity) } unless severity.nil?
|
|
34
|
+
entries = entries.select { |entry| entry.says?(message) } unless message.nil?
|
|
35
|
+
entries = entries.select { |entry| entry.tagged?(named) } unless tags.nil?
|
|
36
|
+
|
|
37
|
+
entries
|
|
21
38
|
end
|
|
22
39
|
|
|
23
40
|
# What a class said, where #logged is everything about it.
|
|
24
|
-
def messages(
|
|
41
|
+
def messages(...) = logged(...).map(&:message)
|
|
25
42
|
|
|
26
|
-
# Whether, where
|
|
27
|
-
|
|
28
|
-
def logged?(severity = nil) = !logged(severity).empty?
|
|
43
|
+
# Whether, where the two above ask what. Named nothing, whether anything was written.
|
|
44
|
+
def logged?(...) = !logged(...).empty?
|
|
29
45
|
|
|
30
46
|
private
|
|
31
47
|
|
|
48
|
+
# A severity arrives as the method name, from the generated severity methods, or as #log's
|
|
49
|
+
# first argument. Both tags keywords land in one list, as the logger lands them, so a spec
|
|
50
|
+
# does not break when a call site moves between the two.
|
|
32
51
|
def entry(invocation)
|
|
33
52
|
arguments = invocation.arguments
|
|
34
53
|
|
|
54
|
+
severity = arguments.fetch(:severity, invocation.method_name)
|
|
55
|
+
|
|
35
56
|
Entry.new(
|
|
36
|
-
severity: severity
|
|
57
|
+
severity: severity.to_s.to_sym,
|
|
37
58
|
message: arguments[:msg],
|
|
38
|
-
data: arguments[:data]
|
|
59
|
+
data: arguments[:data],
|
|
60
|
+
tags: symbols(arguments[:tags]) + symbols(arguments[:tag])
|
|
39
61
|
)
|
|
40
62
|
end
|
|
41
63
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
invocation.method_name
|
|
46
|
-
end
|
|
64
|
+
# One name or a list of them, however a call site or a spec wrote it.
|
|
65
|
+
def symbols(names) = Array(names).map { |name| name.to_s.to_sym }
|
|
47
66
|
end
|
|
48
67
|
end
|
|
49
68
|
end
|
data/lib/hubbado/log/logger.rb
CHANGED
|
@@ -4,31 +4,16 @@ module Hubbado
|
|
|
4
4
|
attr_accessor :log_handlers
|
|
5
5
|
attr_accessor :subject
|
|
6
6
|
|
|
7
|
-
def initialize(subject, log_handlers =
|
|
7
|
+
def initialize(subject, log_handlers = nil)
|
|
8
8
|
self.subject = subject
|
|
9
9
|
self.log_handlers = Array(log_handlers)
|
|
10
|
-
@level = level
|
|
11
|
-
@tags = tags
|
|
12
10
|
end
|
|
13
11
|
|
|
14
|
-
# A logger built without one follows the configuration, which is every logger the gem
|
|
15
|
-
# builds itself: `Log.configure` names neither, so a class using the Dependency module
|
|
16
|
-
# takes whatever the process was configured for.
|
|
17
|
-
def level = @level || Log.config.level
|
|
18
|
-
|
|
19
|
-
# Named per logger as well as per process, as the level is and as Eventide's log gem has
|
|
20
|
-
# it, so one component can be read without turning up everything around it.
|
|
21
|
-
def tags = @tags.nil? ? Log.config.tags : Tags.parse(@tags)
|
|
22
|
-
|
|
23
12
|
def log(severity, msg, data = nil, tag: nil, tags: nil)
|
|
24
13
|
unless SEVERITIES.keys.include? severity.to_sym
|
|
25
14
|
raise ArgumentError, "Unknown serverity #{severity}"
|
|
26
15
|
end
|
|
27
16
|
|
|
28
|
-
# Read after the severity is checked, never before: the level decides what is printed,
|
|
29
|
-
# not what may be said, so quietening a logger must not turn a typo into silence.
|
|
30
|
-
return if SEVERITIES.fetch(severity.to_sym) < SEVERITIES.fetch(level)
|
|
31
|
-
|
|
32
17
|
# Singular and plural are both accepted and both kept, following Eventide's log gem,
|
|
33
18
|
# where real call sites use either and occasionally hand an array to the singular one.
|
|
34
19
|
#
|
|
@@ -36,20 +21,14 @@ module Hubbado
|
|
|
36
21
|
# match nothing and its message would go missing with nothing said about it.
|
|
37
22
|
message_tags = (Array(tags) + Array(tag)).map { |name| name.to_s.to_sym }
|
|
38
23
|
|
|
39
|
-
# Both filters have to pass. A tag cannot raise a message above the level, and the level
|
|
40
|
-
# cannot rescue one the list leaves out.
|
|
41
|
-
#
|
|
42
|
-
# `self.` because the `tags:` keyword above shadows the reader.
|
|
43
|
-
return unless self.tags.write?(message_tags)
|
|
44
|
-
|
|
45
24
|
stacktrace = if data.is_a?(Exception)
|
|
46
25
|
data.full_message
|
|
47
|
-
elsif
|
|
26
|
+
elsif traced?(severity, message_tags)
|
|
48
27
|
format_stacktrace Kernel.caller
|
|
49
28
|
end
|
|
50
29
|
|
|
51
30
|
log_handlers.each do |handler|
|
|
52
|
-
handler.log(subject, severity, msg, data, stacktrace)
|
|
31
|
+
handler.log(subject, severity, msg, data, stacktrace, message_tags)
|
|
53
32
|
end
|
|
54
33
|
end
|
|
55
34
|
|
|
@@ -61,6 +40,15 @@ module Hubbado
|
|
|
61
40
|
|
|
62
41
|
private
|
|
63
42
|
|
|
43
|
+
# Kernel.caller is the most expensive thing in a log call, so it is not paid for a message
|
|
44
|
+
# every handler would drop — or for a warning, which this gem synthesises one for and the
|
|
45
|
+
# terminal then declines to print.
|
|
46
|
+
def traced?(severity, message_tags)
|
|
47
|
+
return false unless STACKTRACE_SEVERITIES.include?(severity.to_sym)
|
|
48
|
+
|
|
49
|
+
log_handlers.any? { |handler| handler.traces?(severity, message_tags) }
|
|
50
|
+
end
|
|
51
|
+
|
|
64
52
|
def format_stacktrace(stacktrace)
|
|
65
53
|
stacktrace.join("\n")
|
|
66
54
|
end
|
|
@@ -17,7 +17,9 @@ module Hubbado
|
|
|
17
17
|
@notifier = notifier
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def traces?(severity, _tags = nil) = LEVELS.key?(severity.to_sym)
|
|
21
|
+
|
|
22
|
+
def log(subject, severity, message, data = nil, stacktrace = nil, _tags = nil)
|
|
21
23
|
level = LEVELS[severity.to_sym]
|
|
22
24
|
return if level.nil?
|
|
23
25
|
|
|
@@ -14,7 +14,11 @@ module Hubbado
|
|
|
14
14
|
@rails_logger = rails_logger
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def traces?(severity, tags = nil) = Display.shows?(severity, tags)
|
|
18
|
+
|
|
19
|
+
def log(subject, severity, message, data = nil, stacktrace = nil, tags = nil)
|
|
20
|
+
return unless Display.shows?(severity, tags)
|
|
21
|
+
|
|
18
22
|
rails_severity = RAILS_SEVERITIES.fetch(severity.to_sym, severity)
|
|
19
23
|
|
|
20
24
|
rails_logger.send(rails_severity, "#{subject}: #{message}")
|
|
@@ -19,7 +19,14 @@ module Hubbado
|
|
|
19
19
|
# One write, because a line and the detail under it belong together. Three writes let a
|
|
20
20
|
# second thread put its own line between them, and a stacktrace filed under the wrong
|
|
21
21
|
# message is worse than no stacktrace.
|
|
22
|
-
|
|
22
|
+
# Only a failure earns one on a terminal, and only if the operator is being shown it.
|
|
23
|
+
def traces?(severity, tags = nil)
|
|
24
|
+
FAILURE_SEVERITIES.include?(severity.to_sym) && Display.shows?(severity, tags)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def log(subject, severity, message, data = nil, stacktrace = nil, tags = nil)
|
|
28
|
+
return unless Display.shows?(severity, tags)
|
|
29
|
+
|
|
23
30
|
lines = ["#{severity.to_s.upcase} #{subject}: #{message}"]
|
|
24
31
|
lines << detail(data, stacktrace) unless data.nil?
|
|
25
32
|
lines << stacktrace if print_stacktrace?(severity, data, stacktrace)
|
data/lib/hubbado/log/tags.rb
CHANGED
data/lib/hubbado/log.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hubbado-log
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hubbado Devs
|
|
@@ -116,6 +116,7 @@ files:
|
|
|
116
116
|
- lib/hubbado/log/controls/rollbar.rb
|
|
117
117
|
- lib/hubbado/log/controls/stacktrace.rb
|
|
118
118
|
- lib/hubbado/log/controls/subject.rb
|
|
119
|
+
- lib/hubbado/log/display.rb
|
|
119
120
|
- lib/hubbado/log/log.rb
|
|
120
121
|
- lib/hubbado/log/log_handler.rb
|
|
121
122
|
- lib/hubbado/log/logger.rb
|