hubbado-log 1.3.0 → 1.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49c303fe323be43f901ed8ef3958b87c79296af3a15efb11c9efb8a9b57156aa
4
- data.tar.gz: 97bc2f947b3a4fc0cfd1845c34d722d6608cf36c1daf853334d3b3b03c4171b5
3
+ metadata.gz: 99367c1895c07697aa0e83dc1e068f35cf2052d75dc6c471610b47099f27fd7f
4
+ data.tar.gz: 58a148cfa36ea3709a2c36fbb228f089d3b176476d27ede4a74ec26a651bdf12
5
5
  SHA512:
6
- metadata.gz: fde7c3bf0995e34348f47a8441df1b95cf9505c909b08bac8fcc409f994ce8c8eb28cb05343e24b4066977a804434ced09cad3fb36e75908e375d3bddbc81304
7
- data.tar.gz: 4f4cae84446c3d1bdf8a9ebb2b986f82a558c623af90a3278c62fa67ee8a23a55163129efc787758b5434faf101346e15187c9f112007333b7295cb26cf3648f
6
+ metadata.gz: 5a53b16af1babaa5c90125bfddec34a02c5256661004833a63b5b54066a914bf764d2c3c738d0a0d2e556d4bd9ef86d96d7e40419e7acd810f2fb7ca4fe58788
7
+ data.tar.gz: 6f77c77d30c8a1ef7f89b2b73c8e09585aa3f39d6dd5d9046796220f699f2926c31debd5fb5b8f514e06c7a2fb66e2b5e20be6cf7f86b95e56bba6218a2ef5e5
data/ChangeLog.md CHANGED
@@ -4,6 +4,79 @@ 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
+ # [1.4.1 - 2026-08-16]
8
+ ## Fixed
9
+ - A subclass of `Hubbado::Log` can be used as a dependency. It answered `nil`
10
+ from `config` and raised `undefined method 'loggers' for nil` on its first
11
+ line, because the configuration was held in a class-level instance variable
12
+ and those are not inherited.
13
+
14
+ ```ruby
15
+ module Messaging
16
+ class Log < Hubbado::Log; end
17
+ end
18
+
19
+ class Handler
20
+ include Messaging::Log::Dependency
21
+ end
22
+ ```
23
+
24
+ The configuration is the process's now, and holds the handlers built from it,
25
+ so every class that writes reads the one a command configured and one
26
+ reconfiguration reaches all of them.
27
+
28
+ # [1.4.0 - 2026-08-16]
29
+ ## Added
30
+ - A substitute for a logger. `Controls::Logger.example` returns one, and a spec
31
+ assigns it where a class's logger goes:
32
+
33
+ ```ruby
34
+ instance.logger = Hubbado::Log::Controls::Logger.example
35
+
36
+ instance.()
37
+
38
+ assert instance.logger.logged?(:error)
39
+ ```
40
+
41
+ It records what it was told rather than writing, so no handler is involved and
42
+ neither the level nor the tag list decides what a spec can read back.
43
+
44
+ Three questions, each taking an optional severity, and each answering in the
45
+ terms its name promises:
46
+
47
+ | Call | Answers |
48
+ |---|---|
49
+ | `logged?(:warn)` | whether anything was written at that severity |
50
+ | `messages(:warn)` | what it said — the message strings |
51
+ | `logged(:warn)` | everything about what it said — `severity`, `message`, `data` |
52
+
53
+ `messages` and `logged?` are both derived from `logged`, so the three cannot
54
+ disagree about what counts as written at a severity.
55
+
56
+ A severity reaches a logger two ways — as the generated method, or as `#log`'s
57
+ first argument — and both answer the same question, compared as symbols.
58
+
59
+ - `evt-subst_attr` as a runtime dependency. The substitute is a mimic of `Logger`
60
+ extended with `Logger::Substitute`, so it answers `is_a?(Logger)` for a class
61
+ that checks, and gains any method `Logger` gains.
62
+
63
+ ## Deprecated
64
+ - `Controls::LogHandler` as the way a consumer reads back what a class logged.
65
+ It builds a real `Logger` and then passes `level: :trace` and `tags: Tags::ALL`
66
+ to switch off the filtering it just built — which is a substitute, reached the
67
+ long way round. Use `Controls::Logger.example`. The handler control stays for
68
+ what it is good at: specs where a handler receiving, or not receiving, a
69
+ message is the subject, as `'A message the filter left out'` is.
70
+
71
+ ## Changed
72
+ - `Controls::LogHandler` names `Log::Logger` where it said `Logger`. With
73
+ `Controls::Logger` defined, a bare `Logger` inside `Controls` resolves to the
74
+ control rather than to the class.
75
+
76
+ ## Compatibility
77
+ Nothing that exists breaks. `Controls::LogHandler` keeps `.attach`, `.logger`,
78
+ `messages`, `logged?`, `reset` and the attributes, unchanged.
79
+
7
80
  # [1.3.0 - 2026-08-15]
8
81
  ## Added
9
82
  - Tags, a second filtering axis beside the level. A message names its concern
data/README.md CHANGED
@@ -18,6 +18,36 @@ Or install it yourself as:
18
18
 
19
19
  $ gem install hubbado-log
20
20
 
21
+ ## A class's own logger
22
+
23
+ A class that includes the dependency module gets a `logger` writing under its own name:
24
+
25
+ ```ruby
26
+ class MyService
27
+ include Hubbado::Log::Dependency
28
+
29
+ def call
30
+ logger.info('Did the thing')
31
+ end
32
+ end
33
+ ```
34
+
35
+ A subclass carries a dependency module of its own, so a library or a component can name where
36
+ its classes take their logger from:
37
+
38
+ ```ruby
39
+ module Messaging
40
+ class Log < Hubbado::Log; end
41
+ end
42
+
43
+ class Handler
44
+ include Messaging::Log::Dependency
45
+ end
46
+ ```
47
+
48
+ Either way the logger writes through the handlers the process was configured with, at the level
49
+ and tags it was configured for.
50
+
21
51
  ## Level
22
52
 
23
53
  A message below the level reaches no handler.
@@ -111,6 +141,67 @@ knowing before adopting tags:
111
141
  list decides for both, and an application whose messages are all untagged goes silent unless the
112
142
  list contains `_untagged`.
113
143
 
144
+ ## Reading back what a class logged
145
+
146
+ A spec assigns a substitute where the class's logger goes, and then asks what the class said:
147
+
148
+ ```ruby
149
+ require 'hubbado/log/controls'
150
+
151
+ instance.logger = Hubbado::Log::Controls::Logger.example
152
+
153
+ instance.()
154
+
155
+ assert instance.logger.logged?(:error)
156
+ ```
157
+
158
+ For a class handed a logger rather than carrying one — the shape a CLI usually takes — it is the
159
+ same object, passed in:
160
+
161
+ ```ruby
162
+ logger = Hubbado::Log::Controls::Logger.example
163
+
164
+ CLI.run(argv, logger: logger)
165
+
166
+ assert logger.logged?(:error)
167
+ ```
168
+
169
+ It records what it was told rather than writing, so no handler is involved and neither the
170
+ configured level nor `LOG_TAGS` decides what can be read back.
171
+
172
+ Three questions, each taking an optional severity:
173
+
174
+ | Call | Answers |
175
+ |---|---|
176
+ | `logged?` / `logged?(:warn)` | whether anything was written, at all or at that severity |
177
+ | `messages` / `messages(:warn)` | what it said — the message strings, in order |
178
+ | `logged` / `logged(:warn)` | everything about what it said |
179
+
180
+ `logged` answers with entries carrying `severity`, `message` and `data`, for the assertion that
181
+ needs more than the text:
182
+
183
+ ```ruby
184
+ assert logger.logged(:error).first.data.equal?(exception)
185
+ ```
186
+
187
+ `messages` and `logged?` are both derived from `logged`, so the three cannot disagree about what
188
+ counts as written at a severity.
189
+
190
+ A severity reaches a logger two ways — `logger.warn('…')` names it as the method,
191
+ `logger.log(:warn, '…')` as an argument — and both answer the same question, compared as symbols.
192
+
193
+ The substitute is a mimic of `Logger`, so it answers `is_a?(Hubbado::Log::Logger)` for a class
194
+ that checks, and gains any method `Logger` gains.
195
+
196
+ Prefer `logged?` to reaching into `messages` where it will do. That a failure was reported is
197
+ usually the contract; the wording of the line usually is not.
198
+
199
+ ### Testing a handler
200
+
201
+ `Controls::LogHandler` is for specs where a handler receiving — or not receiving — a message is
202
+ itself the subject, which in practice means this gem's own tests of level and tag filtering. A
203
+ consumer asserting that its class logged something wants the substitute above.
204
+
114
205
  ## Development
115
206
 
116
207
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/hubbado-log.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "hubbado-log"
3
- s.version = "1.3.0"
3
+ s.version = "1.4.1"
4
4
  s.summary = "Lightweight pluggable logging system"
5
5
 
6
6
  s.authors = ["Hubbado Devs"]
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
25
25
  s.required_ruby_version = '>= 3.2'
26
26
 
27
27
  s.add_runtime_dependency 'evt-dependency'
28
+ s.add_runtime_dependency 'evt-subst_attr'
28
29
 
29
30
  s.add_development_dependency "debug"
30
31
  s.add_development_dependency "hubbado-style"
@@ -5,16 +5,45 @@ module Hubbado
5
5
  # for a human watching one run without it also reaching every unattended log.
6
6
  DEFAULT_LEVEL = :info
7
7
 
8
- attr_accessor :loggers
8
+ attr_reader :loggers
9
9
  attr_reader :level
10
10
  attr_reader :tags
11
11
 
12
+ # The configuration is the process's rather than a class's, so a subclass of Log reads the
13
+ # one a command wrote and there is a single set of handlers to build from it.
14
+ #
15
+ # The one place the environment is read. A command names its level and tags here or not at
16
+ # all, and everything downstream is handed the value rather than the variable.
17
+ def self.instance
18
+ @instance ||= new(level: ENV[LEVEL_VARIABLE], tags: ENV[TAGS_VARIABLE])
19
+ end
20
+
12
21
  def initialize(level: nil, tags: nil)
13
- @loggers = []
22
+ self.loggers = []
14
23
  self.level = level || DEFAULT_LEVEL
15
24
  self.tags = tags
16
25
  end
17
26
 
27
+ # Whatever the block leaves behind is what the process is configured for, and the handlers
28
+ # are built again from it. Named here rather than left to the caller, because a block is
29
+ # free to mutate the list of loggers in place and never reach the writer below.
30
+ def change
31
+ yield self
32
+
33
+ @log_handlers = nil
34
+ end
35
+
36
+ # The handlers themselves, one set for the process. A handler holds what it has been told,
37
+ # so a second set built somewhere else would be a second place to read it back from.
38
+ def log_handlers
39
+ @log_handlers ||= loggers.map(&:new)
40
+ end
41
+
42
+ def loggers=(value)
43
+ @log_handlers = nil
44
+ @loggers = value
45
+ end
46
+
18
47
  # Takes the LOG_TAGS string as readily as a list, so nothing upstream has to know the
19
48
  # syntax in order to hand a value in.
20
49
  def tags=(value)
@@ -20,7 +20,7 @@ module Hubbado
20
20
  end
21
21
 
22
22
  new.tap do |handler|
23
- instance.logger = Logger.new(logger.subject, [handler], level: level, tags: tags)
23
+ instance.logger = Log::Logger.new(logger.subject, [handler], level: level, tags: tags)
24
24
  end
25
25
  end
26
26
 
@@ -28,7 +28,7 @@ module Hubbado
28
28
  def self.logger(subject = Subject.example, level: :trace, tags: Tags::ALL)
29
29
  handler = new
30
30
 
31
- [handler, Logger.new(subject, [handler], level: level, tags: tags)]
31
+ [handler, Log::Logger.new(subject, [handler], level: level, tags: tags)]
32
32
  end
33
33
 
34
34
  def log(subject, severity, message, data = nil, stacktrace = nil)
@@ -0,0 +1,16 @@
1
+ require 'subst_attr'
2
+
3
+ module Hubbado
4
+ class Log
5
+ module Controls
6
+ module Logger
7
+ # A logger a spec assigns in place of a class's own, and then asks what the class said.
8
+ # Named here because the substitute is reached by building a mimic of Log::Logger, which
9
+ # is machinery a spec should not have to name.
10
+ def self.example
11
+ SubstAttr::Substitute.build(Log::Logger)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -4,3 +4,4 @@ require_relative 'controls/message'
4
4
  require_relative 'controls/subject'
5
5
 
6
6
  require_relative 'controls/log_handler'
7
+ require_relative 'controls/logger'
@@ -11,22 +11,16 @@ module Hubbado
11
11
  LEVEL_VARIABLE = "LOG_LEVEL".freeze
12
12
  TAGS_VARIABLE = "LOG_TAGS".freeze
13
13
 
14
- # The one place the environment is read. A command names its level and tags here or not at
15
- # all, and everything downstream is handed the value rather than the variable.
16
- @config = Configuration.new(level: ENV[LEVEL_VARIABLE], tags: ENV[TAGS_VARIABLE])
17
-
18
14
  class << self
19
- attr_reader :config
20
-
21
- def configuration
22
- yield @config
15
+ # Held by the configuration itself rather than here, because a class-level instance
16
+ # variable is not inherited: a subclass asking this class would answer nil.
17
+ def config = Configuration.instance
23
18
 
24
- @loggers = nil
19
+ def configuration(&block)
20
+ config.change(&block)
25
21
  end
26
22
 
27
- def loggers
28
- @loggers ||= config.loggers.map(&:new)
29
- end
23
+ def loggers = config.log_handlers
30
24
 
31
25
  def logger
32
26
  @logger = Logger.new('', loggers)
@@ -0,0 +1,50 @@
1
+ module Hubbado
2
+ class Log
3
+ class Logger
4
+ # What a logger was told rather than what it wrote. Extended onto a mimic of Logger, so a
5
+ # class under test is handed something that answers as a logger and keeps what it was given.
6
+ module Substitute
7
+ Entry = Data.define(:severity, :message, :data)
8
+
9
+ # Everything about what a class said, in order. Named with a severity, only what it said
10
+ # at that one.
11
+ #
12
+ # A severity reaches a logger two ways: as the method, from the generated severity
13
+ # methods, or as #log's first argument. Both are compared as symbols, because #log takes
14
+ # a String as readily and passes on what it was given.
15
+ def logged(severity = nil)
16
+ entries = invocations.map { |invocation| entry(invocation) }
17
+
18
+ return entries if severity.nil?
19
+
20
+ entries.select { |entry| entry.severity == severity.to_s.to_sym }
21
+ end
22
+
23
+ # What a class said, where #logged is everything about it.
24
+ def messages(severity = nil) = logged(severity).map(&:message)
25
+
26
+ # Whether, where #logged and #messages ask what. Without a severity, whether anything was
27
+ # written at all.
28
+ def logged?(severity = nil) = !logged(severity).empty?
29
+
30
+ private
31
+
32
+ def entry(invocation)
33
+ arguments = invocation.arguments
34
+
35
+ Entry.new(
36
+ severity: severity(invocation).to_s.to_sym,
37
+ message: arguments[:msg],
38
+ data: arguments[:data]
39
+ )
40
+ end
41
+
42
+ def severity(invocation)
43
+ return invocation.arguments.fetch(:severity) if invocation.method_name == :log
44
+
45
+ invocation.method_name
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/hubbado/log.rb CHANGED
@@ -3,6 +3,7 @@ require "hubbado/log/tags"
3
3
  require "hubbado/log/configuration"
4
4
  require "hubbado/log/log"
5
5
  require "hubbado/log/logger"
6
+ require "hubbado/log/logger/substitute"
6
7
  require "hubbado/log/log_handler"
7
8
 
8
9
  module Hubbado
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: 1.3.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hubbado Devs
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: evt-subst_attr
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: debug
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -81,11 +95,13 @@ files:
81
95
  - lib/hubbado/log/controls/data.rb
82
96
  - lib/hubbado/log/controls/exception.rb
83
97
  - lib/hubbado/log/controls/log_handler.rb
98
+ - lib/hubbado/log/controls/logger.rb
84
99
  - lib/hubbado/log/controls/message.rb
85
100
  - lib/hubbado/log/controls/subject.rb
86
101
  - lib/hubbado/log/log.rb
87
102
  - lib/hubbado/log/log_handler.rb
88
103
  - lib/hubbado/log/logger.rb
104
+ - lib/hubbado/log/logger/substitute.rb
89
105
  - lib/hubbado/log/tags.rb
90
106
  homepage: https://www.github.com/hubbado/hubbado-log
91
107
  licenses: