rails_semantic_logging 0.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 486b6add6a541fe35dfc1f09e09c6c028f4e9dfa1d62ac49e219d668f26b6a40
4
- data.tar.gz: 85c08bdb85fb2372486966cc278b6d1d82e9c89879dc121da9d4a17ddd1f3086
3
+ metadata.gz: 45760af9983f2c22b841521dede71966f21d51b02ac68909adc334a9c59d1347
4
+ data.tar.gz: d143a49b3a2044e30ac92e20d0ea85d81c4634322526e1e03480e4b9f731e0e0
5
5
  SHA512:
6
- metadata.gz: a5d4cae0b1752ba768a769c435a4f46c2bb43c3ac51b3e9bc4178b2e950dc39cb758091d7a6ca2e813d53a80e5932990d0225307d7bdf1f2bc41d60763ba2c95
7
- data.tar.gz: f6640bcfffb3adc4c2773112b2e85b1da7238a8992bafc54b3ec5123b06ccb4373926f378be43851836f1578e228aeead1377d129dd8b1fb4d486d4771963024
6
+ metadata.gz: cd642159648180526467d511be9f232571e9f7fffcac3840c02706eb1e4f0a18143921d612d8a3cb356d04ed8b51127fa2a05d746791f3a2655eb1caa87a58a2
7
+ data.tar.gz: cf2ce998170d859f106e83b9b11648d1135a5015abd4a8790c67e4ce076bfaf685f1e3622eaaddf9a7127720afbc2378d01fcb8b320672ac4a4a39e9b3536c9e
data/README.md CHANGED
@@ -7,7 +7,7 @@ Opinionated Rails semantic logger configuration with Datadog support. Provides a
7
7
  - **Datadog formatter** with [Standard Attributes](https://docs.datadoghq.com/standard-attributes/) mapping
8
8
  - **Default payload enrichment** for controllers (host, user_agent, referer) mapped to `http.*`
9
9
  - **JSON formatter** for structured logging without Datadog-specific fields
10
- - **ActiveJob integration** with named tags (`job_class`, `job_id`, `queue`) instead of array tags
10
+ - **ActiveJob integration** with named tags (`job_class`, `job_id`, `queue`, `executions`, and `provider_job_id` when the adapter assigns one) instead of array tags
11
11
  - **Sidekiq integration** with job context in all log lines
12
12
  - **Configurable** via [anyway_config](https://github.com/palkan/anyway_config) (YAML, env vars, or code)
13
13
  - **Environment-aware** defaults (Datadog JSON in production, color in development, fatal in test)
@@ -130,6 +130,17 @@ When `default_payload` is enabled (default), controller request logs include:
130
130
 
131
131
  When Datadog tracing is active, the formatter injects: `dd.trace_id`, `dd.span_id`, `dd.env`, `dd.service`, `dd.version`.
132
132
 
133
+ #### Routing error enrichment
134
+
135
+ `rails_semantic_logger` logs unmatched routes as a real `ActionController::RoutingError`.
136
+ When the formatter sees one, it parses the standard `No route matches [GET] "/foo"`
137
+ message and populates `http.method`, `http.url_details.path` and `http.status_code`
138
+ (the canonical `404`) on the log event. The exception message is also used as the
139
+ top-level `message` so the log line is never empty. This keeps unmatched-route logs
140
+ correlated with the original request URL in Datadog without requiring a custom
141
+ server-side log pipeline. Actual request data (when present in the payload) always
142
+ wins over the message-derived fields.
143
+
133
144
  ### Example: Complete request log (production)
134
145
 
135
146
  ```json
@@ -163,11 +174,21 @@ When Datadog tracing is active, the formatter injects: `dd.trace_id`, `dd.span_i
163
174
  "named_tags": {
164
175
  "job_class": "ImportBikesJob",
165
176
  "job_id": "abc-123",
166
- "queue": "default"
177
+ "queue": "default",
178
+ "executions": 0,
179
+ "provider_job_id": "9632"
167
180
  }
168
181
  }
169
182
  ```
170
183
 
184
+ `executions` is the ActiveJob attempt counter (0 on the first run), so retries
185
+ are visible directly in the logs. `provider_job_id` is the queue backend's
186
+ native identifier — the `solid_queue_jobs` row id under Solid Queue, the `jid`
187
+ under Sidekiq — which lets a log line be correlated with the job in Mission
188
+ Control and the backend's own tables. It is only present once the job has been
189
+ enqueued, so synchronous (`perform_now` / `:inline` / `:test`) executions omit
190
+ it.
191
+
171
192
  ### Example: Error log
172
193
 
173
194
  ```json
@@ -204,9 +225,9 @@ Plain structured JSON without Datadog-specific field mapping. Useful for non-Dat
204
225
 
205
226
  Uses the built-in `SemanticLogger::Formatters::Color` for human-readable development output.
206
227
 
207
- ## RSpec Matcher
228
+ ## RSpec Matchers
208
229
 
209
- Include the matcher module in your spec config:
230
+ Include the matchers module in your spec config:
210
231
 
211
232
  ```ruby
212
233
  require 'rails_semantic_logging/rspec/matchers'
@@ -216,7 +237,11 @@ RSpec.configure do |config|
216
237
  end
217
238
  ```
218
239
 
219
- Usage:
240
+ Two matchers are provided. They cover complementary use cases.
241
+
242
+ ### `log_semantic` — declarative "was X logged?" assertion
243
+
244
+ Returns a boolean: true if **any** captured log line matches all the supplied criteria. Use it when the question is "did this happen?".
220
245
 
221
246
  ```ruby
222
247
  expect { logger.info("hello") }.to log_semantic(level: :info, message: /hello/)
@@ -224,6 +249,34 @@ expect { logger.warn("oops", key: "val") }.to log_semantic(payload: { key: "val"
224
249
  expect { do_work }.to log_semantic(named_tags: { job_class: 'MyJob' })
225
250
  ```
226
251
 
252
+ ### `have_logged_message` — find a specific log and assert on it
253
+
254
+ Finds the first log event whose message matches the expected pattern, then hands the raw `SemanticLogger::Log` (and, optionally, a formatted version) over to a user block so you can make arbitrary assertions on it.
255
+
256
+ The expected message can be a String, a Regexp, or any object with `===` (so RSpec built-in matchers like `a_string_starting_with("...")` work too). An optional second argument constrains the level.
257
+
258
+ ```ruby
259
+ expect { service.call }.to have_logged_message("Imported")
260
+ expect { service.call }.to have_logged_message(/import/, :warn)
261
+
262
+ expect { service.call }
263
+ .to have_logged_message("Imported").with_formatted_event { |event, formatted|
264
+ expect(event.payload).to include(items: 3)
265
+ expect(event.named_tags).to include(tenant: 'eu')
266
+ expect(formatted.dig('http', 'status_code')).to eq(200)
267
+ }
268
+ ```
269
+
270
+ `with_formatted_event` defaults to the gem's Datadog formatter; pass any `SemanticLogger::Formatters::Base` subclass to use a different one:
271
+
272
+ ```ruby
273
+ .with_formatted_event(SemanticLogger::Formatters::Json) { |event, formatted| ... }
274
+ ```
275
+
276
+ `formatted` is the formatter's JSON output, re-parsed with indifferent access — so both `formatted[:status]` and `formatted['status']` work.
277
+
278
+ The matcher passes a **duplicated** copy of the log event to the formatter, so a formatter that mutates `log.payload` / `log.named_tags` cannot leak those mutations into the event you receive in the block.
279
+
227
280
  ## Puma Integration
228
281
 
229
282
  When using Puma in clustered mode, reopen log appenders after forking:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.2
1
+ 0.2.0
@@ -1,7 +1,22 @@
1
- # Monkey patch for Datadog's ActiveJob LogInjection to use hash-style
2
- # correlation tags instead of string-style. This is necessary because
3
- # we patch ActiveJob::Logging to use named tags (job_class, job_id, queue),
4
- # so Datadog must also use hash-style tags for compatibility with SemanticLogger.
1
+ # Monkey patches Datadog's ActiveJob LogInjection to use hash-style
2
+ # correlation tags instead of string-style.
3
+ #
4
+ # Background: Datadog ships LogInjection sub-modules (PerformNowPatch for
5
+ # Rails 6+, EnqueuePatch for the singleton enqueue path) and patches
6
+ # ActiveJob::Base by `prepend`ing them. By default they call
7
+ # `Datadog::Tracing.log_correlation`, which returns a flat string
8
+ # ("dd.trace_id=... dd.span_id=...") and ends up under `event.tags`.
9
+ #
10
+ # Once we patch ActiveJob::Logging#tag_logger to emit named tags, we want
11
+ # Datadog correlation to land in `named_tags` too, so traces and logs
12
+ # correlate properly in the Datadog UI. We achieve that by re-defining
13
+ # `perform_now` and `enqueue` on Datadog's own patch modules to call
14
+ # `Datadog::Tracing.correlation.to_h` and pass the resulting hash to
15
+ # `logger.tagged`.
16
+ #
17
+ # Because Datadog prepends those modules into ActiveJob::Base, our
18
+ # re-definitions take effect on every method dispatch — independent of
19
+ # load order, as long as `apply!` runs before any job is executed.
5
20
 
6
21
  module RailsSemanticLogging
7
22
  module Datadog
@@ -9,24 +24,37 @@ module RailsSemanticLogging
9
24
  def self.apply!
10
25
  return unless defined?(::Datadog::Tracing::Contrib::ActiveJob::LogInjection)
11
26
 
12
- # Ensure the SemanticLogger ActiveJob extension is loaded first
27
+ # Ensure the SemanticLogger ActiveJob extension is loaded before we
28
+ # override Datadog's patches, so the named-tags `tag_logger` is in place.
13
29
  require 'rails_semantic_logger/extensions/active_job/logging'
14
30
 
15
- # Replace the original module with our patched version
16
- ::Datadog::Tracing::Contrib::ActiveJob.send(:remove_const, :LogInjection)
17
- ::Datadog::Tracing::Contrib::ActiveJob.const_set(:LogInjection, PatchedLogInjection)
31
+ patch_perform_now
32
+ patch_enqueue
18
33
  end
19
34
 
20
- # Replacement module that uses correlation.to_h instead of log_correlation
21
- module PatchedLogInjection
22
- def self.included(base)
23
- base.class_eval do
24
- around_perform do |_, block|
25
- if ::Datadog.configuration.tracing.log_injection && logger.respond_to?(:tagged)
26
- logger.tagged(::Datadog::Tracing.correlation.to_h, &block)
27
- else
28
- block.call
29
- end
35
+ def self.patch_perform_now
36
+ return unless defined?(::Datadog::Tracing::Contrib::ActiveJob::LogInjection::PerformNowPatch)
37
+
38
+ ::Datadog::Tracing::Contrib::ActiveJob::LogInjection::PerformNowPatch.module_eval do
39
+ define_method(:perform_now) do
40
+ if ::Datadog.configuration.tracing.log_injection && logger.respond_to?(:tagged)
41
+ logger.tagged(::Datadog::Tracing.correlation.to_h) { super() }
42
+ else
43
+ super()
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.patch_enqueue
50
+ return unless defined?(::Datadog::Tracing::Contrib::ActiveJob::LogInjection::EnqueuePatch)
51
+
52
+ ::Datadog::Tracing::Contrib::ActiveJob::LogInjection::EnqueuePatch.module_eval do
53
+ define_method(:enqueue) do |*args, **kwargs, &block|
54
+ if ::Datadog.configuration.tracing.log_injection && logger.respond_to?(:tagged)
55
+ logger.tagged(::Datadog::Tracing.correlation.to_h) { super(*args, **kwargs, &block) }
56
+ else
57
+ super(*args, **kwargs, &block)
30
58
  end
31
59
  end
32
60
  end
@@ -33,12 +33,27 @@ module RailsSemanticLogging
33
33
  user_role: :role
34
34
  }.freeze
35
35
 
36
+ # Matches ActionController::RoutingError messages like:
37
+ # No route matches [GET] "/some/path"
38
+ # Used to extract http.method and http.url_details.path from the exception.
39
+ ROUTING_ERROR_MESSAGE = /\ANo route matches \[(?<method>\w+)\]\s+"(?<path>[^"]+)"/
40
+
36
41
  def initialize(time_format: :iso_8601, time_key: :timestamp, **args) # rubocop:disable Naming/VariableNumber
37
42
  super(time_format:, time_key:, log_application: false, log_host: true, log_environment: false, **args)
38
43
  end
39
44
 
40
45
  def call(log, logger)
41
46
  super
47
+ # SemanticLogger::Formatters::Raw assigns log.payload and log.named_tags
48
+ # to hash[:payload] / hash[:named_tags] BY REFERENCE. We mutate those
49
+ # below (remap_http_payload deletes :host/:method/etc., remap_named_tags
50
+ # deletes :request_id/:client_ip/:dd/etc., deep_compact_blank! drops
51
+ # every blank value). Without these dups the formatter strips keys off
52
+ # the underlying log event, which breaks any consumer that reads
53
+ # log.payload / log.named_tags after formatting (other appenders,
54
+ # RSpec matchers that re-inspect the captured event).
55
+ hash[:payload] = hash[:payload].dup if hash[:payload].is_a?(Hash)
56
+ hash[:named_tags] = hash[:named_tags].dup if hash[:named_tags].is_a?(Hash)
42
57
  remap_named_tags
43
58
  remap_http_payload
44
59
  parse_url_details
@@ -59,6 +74,16 @@ module RailsSemanticLogging
59
74
  hash[:status] = log.level
60
75
  end
61
76
 
77
+ # Fall back to the exception message when the log carries no message of
78
+ # its own. rails_semantic_logger logs unmatched routes (and other
79
+ # rescued exceptions) via ActionDispatch::DebugExceptions by passing only
80
+ # the exception object, so without this the Datadog `message` field would
81
+ # be empty for those events.
82
+ def message
83
+ super
84
+ hash[:message] ||= log.exception&.message
85
+ end
86
+
62
87
  def duration
63
88
  # Propagate duration from payload if not set on log
64
89
  log.duration = log.payload[:duration] if log.duration.nil? && log.payload&.dig(:duration)
@@ -78,10 +103,45 @@ module RailsSemanticLogging
78
103
  message: log.exception.message,
79
104
  stack: log.exception.backtrace&.join("\n")
80
105
  }
106
+
107
+ parse_routing_error
108
+ end
109
+
110
+ # SemanticLogger's :dimensions log attribute carries metric tags
111
+ # (e.g. `logger.info('Processed feed', metric: 'feed_size', metric_amount: 12,
112
+ # dimensions: { feed: 'amazon' })`). The Raw formatter sets :metric and
113
+ # :metric_amount but not :dimensions — surface it at the top level so the
114
+ # Datadog UI can filter on those tags directly without diving into named_tags.
115
+ def metric
116
+ super
117
+ hash[:dimensions] = log.dimensions if log.dimensions.respond_to?(:any?) && log.dimensions.any?
81
118
  end
82
119
 
83
120
  private
84
121
 
122
+ # Extracts http.method and http.url_details.path from
123
+ # ActionController::RoutingError messages so logs for unmatched routes
124
+ # are still correlated with the request URL in Datadog.
125
+ #
126
+ # rails_semantic_logger patches ActionDispatch::DebugExceptions to log the
127
+ # real exception object, so unmatched-route requests reach this formatter
128
+ # as a RoutingError carrying a message like: No route matches [GET] "/foo".
129
+ def parse_routing_error
130
+ # Fully qualified so the constant resolves to Rails' ActionController and
131
+ # not the gem's own RailsSemanticLogging::ActionController namespace.
132
+ return unless log.exception.is_a?(::ActionController::RoutingError)
133
+ return unless (match = ROUTING_ERROR_MESSAGE.match(log.exception.message))
134
+
135
+ hash[:http] ||= {}
136
+ hash[:http][:method] ||= match[:method]
137
+ # The DebugExceptions log carries no HTTP status (there is no completed
138
+ # request), so map the exception to its canonical status the same way
139
+ # rails_semantic_logger does (RoutingError => 404).
140
+ hash[:http][:status_code] ||= ::ActionDispatch::ExceptionWrapper.status_code_for_exception(log.exception.class.name)
141
+ url_details = hash[:http][:url_details] ||= {}
142
+ url_details[:path] ||= match[:path]
143
+ end
144
+
85
145
  # Parses http.url into url_details with host, path and queryString
86
146
  def parse_url_details
87
147
  return unless hash.dig(:http, :url)
@@ -176,14 +236,25 @@ module RailsSemanticLogging
176
236
  hash[:http].is_a?(Hash) ? hash[:http].merge!(http) : hash[:http] = http
177
237
  end
178
238
 
179
- # Recursively removes blank values from a hash
239
+ # Recursively removes empty values from a hash. nil, "", [], {} are dropped
240
+ # to keep the log payload tidy, but `false` and `0` are kept because they
241
+ # are meaningful values in things like `bot: false` or `count: 0`.
242
+ # (ActiveSupport's `blank?` treats `false` as blank, which would silently
243
+ # strip boolean flags from payloads.)
180
244
  def deep_compact_blank!(h)
181
245
  h.each do |key, value|
182
246
  deep_compact_blank!(value) if value.is_a?(Hash)
183
- h.delete(key) if value.blank?
247
+ h.delete(key) if blank_value?(value)
184
248
  end
185
249
  h
186
250
  end
251
+
252
+ def blank_value?(value)
253
+ return true if value.nil?
254
+ return value.empty? if value.respond_to?(:empty?)
255
+
256
+ false
257
+ end
187
258
  end
188
259
  end
189
260
  end
@@ -4,13 +4,23 @@ module RailsSemanticLogging
4
4
  module JobLogging
5
5
  # ActiveJob patch to provide named tags instead of array tags.
6
6
  # Converts the default tag_logger(class, id) call into named tags
7
- # (job_class, job_id, queue) for structured logging.
7
+ # (job_class, job_id, queue, provider_job_id, executions) for
8
+ # structured logging.
9
+ #
10
+ # `provider_job_id` is the queue backend's native id (e.g. the
11
+ # solid_queue_jobs row id, or the Sidekiq jid) and lets a log line be
12
+ # correlated with the job in Mission Control / the solid_queue_* tables.
13
+ # It is only present once the job has been enqueued, so it is added
14
+ # conditionally. `executions` is the ActiveJob attempt counter, which
15
+ # makes retries visible in the logs.
8
16
  module ActiveJobPatch
9
17
  extend ActiveSupport::Concern
10
18
 
11
19
  def tag_logger(job_class = nil, job_id = nil, &)
12
20
  if job_class && job_id
13
- super(job_class: job_class, job_id: job_id, queue: queue_name, &)
21
+ tags = { job_class: job_class, job_id: job_id, queue: queue_name, executions: executions }
22
+ tags[:provider_job_id] = provider_job_id if provider_job_id
23
+ super(**tags, &)
14
24
  else
15
25
  super(&)
16
26
  end
@@ -1,4 +1,6 @@
1
+ require 'json'
1
2
  require 'semantic_logger'
3
+ require_relative '../formatters/datadog'
2
4
 
3
5
  module RailsSemanticLogging
4
6
  module RSpec
@@ -111,10 +113,144 @@ module RailsSemanticLogging
111
113
  end
112
114
  end
113
115
 
116
+ # Block matcher that finds a specific log event by message and exposes both
117
+ # the raw SemanticLogger::Log and an optionally formatted version to a user
118
+ # block, so the test can make arbitrary assertions on the event.
119
+ #
120
+ # Complementary to `log_semantic`:
121
+ # * `log_semantic` is declarative ("was anything matching these criteria
122
+ # logged?") and returns a boolean.
123
+ # * `have_logged_message` is imperative — it finds the event, hands it over
124
+ # to the test, and lets the test assert whatever it needs.
125
+ #
126
+ # Usage:
127
+ # expect { logger.info("hello", key: "val") }.to have_logged_message("hello")
128
+ # expect { do_work }.to have_logged_message(/import/, :warn)
129
+ #
130
+ # expect { service.call }
131
+ # .to have_logged_message("Completed").with_formatted_event { |event, formatted|
132
+ # expect(event.payload).to include(order_id: order.id)
133
+ # expect(formatted.dig("http", "status_code")).to eq(200)
134
+ # }
135
+ #
136
+ # The optional `with_formatted_event` block receives `(event, formatted)`
137
+ # where `formatted` is the JSON output of the chosen formatter parsed back
138
+ # with indifferent access. By default the gem's Datadog formatter is used;
139
+ # pass any `SemanticLogger::Formatters::Base` subclass to override:
140
+ #
141
+ # .with_formatted_event(SemanticLogger::Formatters::Json) { |event, formatted| ... }
142
+ class HaveLoggedMessageMatcher
143
+ attr_reader :event_block
144
+
145
+ DEFAULT_FORMATTER = ::RailsSemanticLogging::Formatters::Datadog
146
+
147
+ def initialize(expected, expected_level = nil)
148
+ @expected_message = expected
149
+ @expected_level = expected_level
150
+ @formatter_class = DEFAULT_FORMATTER
151
+ end
152
+
153
+ def matches?(block)
154
+ # Capture the live appender BEFORE we attach our test appender. The
155
+ # Datadog formatter reads `logger.host` on the appender it receives, so
156
+ # the formatter call later on needs a real one (any subscriber will do).
157
+ @live_appender = ::SemanticLogger.appenders.first
158
+ @log_event = capture_first_matching_event(block)
159
+ return false unless @log_event
160
+ return false if @expected_level && @log_event.level != @expected_level
161
+
162
+ @event_block&.call(@log_event, formatted)
163
+ true
164
+ rescue ::RSpec::Expectations::ExpectationNotMetError => e
165
+ @error = e
166
+ false
167
+ end
168
+
169
+ def supports_block_expectations?
170
+ true
171
+ end
172
+
173
+ # Attach a block that receives the matched `(log_event, formatted)` pair.
174
+ # `formatted` is the JSON output of the configured formatter, parsed back
175
+ # with `HashWithIndifferentAccess` so callers can navigate the structure
176
+ # with either string or symbol keys.
177
+ def with_formatted_event(formatter_class = nil, &block)
178
+ raise ArgumentError, 'block is required' unless block
179
+ if formatter_class && !(formatter_class < ::SemanticLogger::Formatters::Base)
180
+ raise ArgumentError,
181
+ "formatter must inherit from SemanticLogger::Formatters::Base, got #{formatter_class.inspect}"
182
+ end
183
+
184
+ @formatter_class = formatter_class if formatter_class
185
+ @event_block = block
186
+ self
187
+ end
188
+
189
+ def failure_message
190
+ return @error.message if @error
191
+ if @log_event && @expected_level && @log_event.level != @expected_level
192
+ return "expected log level #{@expected_level.inspect}, got #{@log_event.level.inspect}"
193
+ end
194
+
195
+ "expected block to log a message matching #{@expected_message.inspect}"
196
+ end
197
+
198
+ def failure_message_when_negated
199
+ "expected block NOT to log a message matching #{@expected_message.inspect}, but a matching event was emitted"
200
+ end
201
+
202
+ private
203
+
204
+ def capture_first_matching_event(block)
205
+ capture = InMemoryAppender.new
206
+ with_capture_appender(capture) { block.call }
207
+ capture.logs.find { |evt| message_matches?(evt.message) }
208
+ end
209
+
210
+ def with_capture_appender(capture)
211
+ ::SemanticLogger.add_appender(appender: capture)
212
+ previous_level = ::SemanticLogger.default_level
213
+ ::SemanticLogger.default_level = :trace
214
+ yield
215
+ ::SemanticLogger.flush
216
+ ensure
217
+ ::SemanticLogger.default_level = previous_level
218
+ ::SemanticLogger.remove_appender(capture)
219
+ end
220
+
221
+ # Matches String, Regex and any object that implements `===` (incl. RSpec
222
+ # built-in matchers like `a_string_starting_with("...")`).
223
+ def message_matches?(actual)
224
+ @expected_message === actual.to_s # rubocop:disable Style/CaseEquality
225
+ end
226
+
227
+ # Format a *copy* of the log event so a buggy formatter that mutates
228
+ # log.payload / log.named_tags cannot leak back into the event we hand
229
+ # off to the user block.
230
+ def formatted
231
+ @formatted ||= begin
232
+ safe_log = duped_log_event
233
+ ::JSON.parse(@formatter_class.new.call(safe_log, @live_appender)).with_indifferent_access
234
+ end
235
+ end
236
+
237
+ def duped_log_event
238
+ copy = @log_event.dup
239
+ copy.payload = @log_event.payload.dup if @log_event.payload.is_a?(::Hash)
240
+ copy.named_tags = @log_event.named_tags.dup if @log_event.named_tags.is_a?(::Hash)
241
+ copy.tags = @log_event.tags.dup if @log_event.tags.is_a?(::Array)
242
+ copy
243
+ end
244
+ end
245
+
114
246
  module Matchers
115
247
  def log_semantic(expected = {})
116
248
  LogSemanticMatcher.new(expected)
117
249
  end
250
+
251
+ def have_logged_message(expected, level = nil) # rubocop:disable Naming/PredicatePrefix
252
+ HaveLoggedMessageMatcher.new(expected, level)
253
+ end
118
254
  end
119
255
  end
120
256
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_semantic_logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fabio Napoleoni