logsy 0.2.0 → 0.4.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: 8d9d026e66871c418c7315bbec120182f0aa3a2e4caa247e9fa533a4a6b99361
4
- data.tar.gz: caf2ad615f13cd2f75954e43dfcab5d61e93ec40b11c004bf9723bd8c4736721
3
+ metadata.gz: 980d9365473b25931ea00736958f1980b5872ec25901dbd09a58347f4eca8c70
4
+ data.tar.gz: 0646f18af7c441b599e61ec5dfb133268cf4d371594b8a08363c6511a7f758d8
5
5
  SHA512:
6
- metadata.gz: 9bed718fb7a7618cd420220579e7edd7ce92b44d46f538a0ef2459644014fc207febe0c09cc51a3b72624d083930dc78a37e00752bab2ef6fd34f4291c6677f0
7
- data.tar.gz: 2e2d8bd7a250600dedbf8e704e87227cd5ff590c8f9fee39276a1a909b8b1973d51db5f09e6a415ccd375a5222b9bef87039958e80a9c44b3718d4c5724c7d05
6
+ metadata.gz: 6687e938d72ff9054851a86f10cc7450046110d472a88043e125aef5b4c3797d236b9406b1c3645798cb8486b15614a1fc454aaa252cfd2ff2c39714e5a6221d
7
+ data.tar.gz: 6cd7238d880da15217483ecbaefe19f21f1bb2f6a591059b4a9feb3507e1b1942f21ec936b5af59ee5c35179d6d0efa22bada6e423b616bd2bf0fe6ae0782f78
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0] - 2026-06-29
4
+
5
+ - **Sidekiq middleware is now auto-registered** — the Railtie calls `Logsy::SidekiqMiddleware.install!` for you once Sidekiq is loaded, so `request_id` (and any `job_propagated_keys`) propagates into jobs with zero per-app wiring. No more `require 'logsy/sidekiq_middleware'` + three `chain.add` lines in `config/initializers/sidekiq.rb`. Opt out with `config.auto_register_sidekiq_middleware = false`. The registration initializer runs `after: :load_config_initializers`, so it works even when the app uses `gem 'sidekiq', require: false`.
6
+ - **`Logsy::SidekiqMiddleware.install!(sidekiq = ::Sidekiq)`**: new single entry point that registers the client + server middleware (client on both client and server chains, server on the server chain). Non-Rails apps now wire Sidekiq with one line instead of two config blocks. Idempotent — Sidekiq's `Chain#add` replaces, so it's safe alongside a stray manual registration.
7
+ - **`Logsy.tag(**tags)`**: set several tags at once (`Logsy.tag(user_id: u.id, plan: 'pro')`), with the same String coercion as `Logsy[]=`.
8
+ - **`Logsy.with(**tags) { ... }`**: set tags for the duration of a block and restore the previous values afterwards (deleting keys that were unset before), even if the block raises. Scopes context to a unit of work without leaking it — useful in service objects, rake tasks, and threads the Rails executor / Sidekiq middleware don't reset. Returns the block's value.
9
+ - `Configuration`: new `auto_register_sidekiq_middleware` (default `true`).
10
+ - **Performance** (`JsonFormatter`): no behaviour or output change, just less work on the hot path —
11
+ - Hash (wide-event) messages now skip the caller-location stack walk entirely. They're logged from a Logsy hook frame with no app frame to attribute, so the walk only ever scanned the full stack to emit no `file` anyway. Cuts a wide-event line with `include_caller_location` on from ~6.0µs to ~2.5µs (~2.4x) — and these fire once per request and once per job.
12
+ - The ANSI-code strip on String messages is now guarded by a cheap `include?("\e")` byte scan, so the common plain log line skips the regex and its allocation (~2.3x faster on that step).
13
+ - `app_root` is computed once per log line and threaded through the frame walk instead of being rebuilt for every stack frame inspected, removing an allocation per frame on the caller-location path.
14
+
15
+ ## [0.3.0] - 2026-06-21
16
+
17
+ - **`Logsy::JobHooks`**: new ActiveJob concern — the background-job counterpart of `ControllerHooks`. Emits one wide event (`event: "job"`) when each job finishes, carrying `job_class`, `queue`, `active_job_id`, `executions`, `duration_ms`, `status` (`"ok"`/`"error"`), `error`, and every Logsy tag set during the run. Emitted even when the job raises (then re-raised). Override `logsy_job_summary_extras` to add fields. Job arguments are deliberately omitted — ActiveJob already logs them at enqueue time, correlatable by `active_job_id`.
18
+ - **Auto-wiring**: the Railtie now auto-includes `Logsy::JobHooks` into `ActiveJob::Base` (via `ActiveSupport.on_load(:active_job)`), so jobs emit summaries with zero per-app setup. Opt out with `config.auto_include_job_hooks = false` and include it manually. (Non-Rails ActiveJob users include it themselves.)
19
+ - `SidekiqMiddleware::Server`: now also sets `Logsy[:job_class]` (from `job['wrapped']`, the ActiveJob class name, falling back to `job['class']` for plain Sidekiq workers), so every job log line — including ActiveJob's own "Performing ..." — is tagged with the class, not just the `job_id`/jid.
20
+ - `Configuration`: new `job_summary_event_name` (default `"job"`), mirroring `request_summary_event_name`; new `auto_include_job_hooks` (default `true`).
21
+ - **Default `ignored_caller_paths`** now also skips `app/middleware` / `app/middlewares` frames — Rack middlewares sit in every request's stack and would otherwise be picked as the "app frame" for lines emitted below them. (Previously each app had to add this itself.)
22
+
3
23
  ## [0.2.0] - 2026-06-07
4
24
 
5
25
  - **`Logsy::RackMiddleware` + Railtie**: request_id is now captured in Rack middleware (auto-inserted after `ActionDispatch::RequestId`) instead of a controller `before_action`. Every log line of a request now carries `request_id` — including "Started GET ..." and "Processing by ..." which a controller callback fires too late to tag. Falls back to `X-Request-Id`, then a generated UUID, so the tag is always present.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Tagged structured logging for Rails apps. JSON output, one line per log call.
4
4
 
5
- Logsy gives you **one wide JSON event per request** plus **correlated breadcrumb logs**, all tagged with whatever per-request context you set via `Logsy[:key] = value`. Tags propagate across background jobs (Sidekiq middleware bundled). Where you ship the JSON is your call — Logsy just writes structured lines to stdout.
5
+ Logsy gives you **one wide JSON event per request** plus **correlated breadcrumb logs**, all tagged with whatever per-request context you set via `Logsy[:key] = value`. Tags propagate across background jobs (Sidekiq middleware auto-wired — zero config). Where you ship the JSON is your call — Logsy just writes structured lines to stdout.
6
6
 
7
7
  ## Installation
8
8
 
@@ -53,26 +53,14 @@ end
53
53
 
54
54
  That's it. No `Current` model to define, no attribute declarations — just key/value.
55
55
 
56
- ### 5. (Optional) Background job propagation
56
+ ### 5. Background job propagation — automatic for Sidekiq
57
57
 
58
- If you use Sidekiq:
58
+ If you use Sidekiq, there's **nothing to do**: the Railtie registers the bundled middleware for you once Sidekiq is loaded, so `request_id` (and any other `job_propagated_keys`) flows from the enqueueing request into the job — and on into any jobs that job enqueues.
59
59
 
60
- ```ruby
61
- # config/initializers/sidekiq.rb
62
- require 'logsy/sidekiq_middleware'
63
-
64
- Sidekiq.configure_client do |config|
65
- config.client_middleware { |chain| chain.add(Logsy::SidekiqMiddleware::Client) }
66
- end
67
-
68
- Sidekiq.configure_server do |config|
69
- config.client_middleware { |chain| chain.add(Logsy::SidekiqMiddleware::Client) }
70
- config.server_middleware { |chain| chain.add(Logsy::SidekiqMiddleware::Server) }
71
- end
72
- ```
60
+ Want to propagate more than `request_id`? That's the only thing you'd configure:
73
61
 
74
62
  ```ruby
75
- # config/initializers/logsy.rb (optional — only if you want to propagate more than request_id)
63
+ # config/initializers/logsy.rb (optional)
76
64
  Logsy.configure do |c|
77
65
  c.job_propagated_keys = %i[request_id user_id tenant_id]
78
66
  end
@@ -82,9 +70,39 @@ The bundled middleware:
82
70
  - **Client side**: when a job is enqueued, copies the configured tags from `Logsy[]` into the job payload
83
71
  - **Server side**: when the job runs, sets those tags back in `Logsy[]` AND sets `Logsy[:job_id] = job['jid']` automatically
84
72
 
85
- Anything the job code writes via `Logsy[:foo] = bar` while running shows up on every log line emitted during the job. Tags reset automatically after each job (no leak between jobs sharing a worker thread).
73
+ Anything the job code writes via `Logsy[:foo] = bar` while running shows up on every log line emitted during the job. Tags reset automatically after each job (no leak between jobs sharing a worker thread). The server middleware also sets `Logsy[:job_class]` (the wrapped ActiveJob class name, e.g. `"GenerateEodJob"`, falling back to the worker class for plain Sidekiq jobs).
74
+
75
+ To wire it up yourself (non-Rails, or after opting out with `config.auto_register_sidekiq_middleware = false`) it's a one-liner:
76
+
77
+ ```ruby
78
+ require 'logsy/sidekiq_middleware'
79
+ Logsy::SidekiqMiddleware.install! # registers client + server middleware
80
+ ```
81
+
82
+ For other job systems (Resque, GoodJob, custom), write your own middleware using the same pattern — Logsy's `[]=` / `[]` / `tag` / `with` / `tags` / `reset` API is generic.
83
+
84
+ ### 6. Job wide event — automatic
85
+
86
+ `Logsy::JobHooks` is the background-job counterpart of `ControllerHooks`. In a Rails app there's **nothing to do**: the Railtie auto-includes it into `ActiveJob::Base`, so every job emits one wide event with `event: "job"` when it finishes, carrying `job_class`, `queue`, `active_job_id`, `executions` (attempt count), `duration_ms`, `status` (`"ok"`/`"error"`), any `error` class/message — plus every tag set during the run (`job_id`/jid, propagated `request_id`, and anything you wrote via `Logsy[:key] = value`). It's emitted even when the job raises (then re-raised). Job arguments are deliberately omitted — ActiveJob already logs them at enqueue time, correlatable by `active_job_id`.
87
+
88
+ Override `logsy_job_summary_extras` in a job to add fields:
89
+
90
+ ```ruby
91
+ class SomeJob < ApplicationJob
92
+ private
93
+
94
+ def logsy_job_summary_extras
95
+ { tenant: account_id }
96
+ end
97
+ end
98
+ ```
99
+
100
+ To opt out of the auto-include and wire it yourself:
86
101
 
87
- For other job systems (Resque, GoodJob, custom), write your own middleware using the same pattern — Logsy's `[]=` / `[]` / `tags` / `reset` API is generic.
102
+ ```ruby
103
+ Logsy.configure { |c| c.auto_include_job_hooks = false }
104
+ # then `include Logsy::JobHooks` in the jobs you want
105
+ ```
88
106
 
89
107
  ## What it looks like in your logs
90
108
 
@@ -123,6 +141,25 @@ The wide event at end of request:
123
141
  }
124
142
  ```
125
143
 
144
+ The wide event at end of a job (with `Logsy::JobHooks`):
145
+
146
+ ```json
147
+ {
148
+ "ts":"2026-05-02T10:00:00.250Z",
149
+ "level":"INFO",
150
+ "event":"job",
151
+ "job_class":"SpgReverseTransactionJob",
152
+ "queue":"default",
153
+ "active_job_id":"09fe3546-211b-45cf-9434-fb2689c580c5",
154
+ "executions":1,
155
+ "duration_ms":842.10,
156
+ "status":"ok",
157
+ "job_id":"402797cb0cfc9324cc10cc7f",
158
+ "transaction_id":"019c0440-a797-7479-8d3a-4260ba896681",
159
+ "rrn":"123456789012"
160
+ }
161
+ ```
162
+
126
163
  Search your log store by `message_id` to find every request from that message. Pivot from a request's `request_id` to see every breadcrumb log line emitted while it ran.
127
164
 
128
165
  Notes on the fields:
@@ -133,14 +170,24 @@ Notes on the fields:
133
170
  ## API reference
134
171
 
135
172
  ```ruby
136
- Logsy[:user_id] = 'u-1' # set a tag
137
- Logsy[:user_id] # read it
138
- Logsy.tags # all tags as a hash
139
- Logsy.reset # clear all tags (the controller/middleware do this for you)
173
+ Logsy[:user_id] = 'u-1' # set a tag
174
+ Logsy[:user_id] # read it
175
+ Logsy.tag(user_id: 'u-1', plan: 'pro') # set several at once
176
+ Logsy.with(order_id: 'o-9') { ... } # set for the block, restore afterwards
177
+ Logsy.tags # all tags as a hash
178
+ Logsy.reset # clear all tags (the controller/middleware do this for you)
140
179
  ```
141
180
 
142
181
  Symbol and string keys are equivalent — `Logsy['user_id']` and `Logsy[:user_id]` access the same slot.
143
182
 
183
+ `Logsy.with` scopes tags to a block and restores the prior values on exit (even if the block raises) — handy in service objects, rake tasks, or threads the Rails executor / Sidekiq middleware don't reset for you:
184
+
185
+ ```ruby
186
+ Logsy.with(order_id: order.id) do
187
+ process(order) # every log line here carries order_id; gone afterwards
188
+ end
189
+ ```
190
+
144
191
  ## Customizing the wide event
145
192
 
146
193
  Override `logsy_request_summary_extras` in your controller to add fields:
@@ -184,6 +231,20 @@ Logsy.configure do |c|
184
231
 
185
232
  # The "event" name on the request summary. Default: "request"
186
233
  c.request_summary_event_name = 'http_request'
234
+
235
+ # The "event" name on the job summary (Logsy::JobHooks). Default: "job"
236
+ c.job_summary_event_name = 'background_job'
237
+
238
+ # Auto-include Logsy::JobHooks into ActiveJob::Base (the Railtie does this
239
+ # so jobs emit summaries with no setup). Set false to wire it yourself.
240
+ # Default: true
241
+ c.auto_include_job_hooks = true
242
+
243
+ # Register the bundled Sidekiq middleware automatically when Sidekiq is
244
+ # loaded (the Railtie does this so request_id propagates into jobs with
245
+ # no setup). Set false to call Logsy::SidekiqMiddleware.install! yourself.
246
+ # Default: true
247
+ c.auto_register_sidekiq_middleware = true
187
248
  end
188
249
  ```
189
250
 
@@ -201,7 +262,7 @@ Logsy fills the gap: a small, dictionary-style API (`Logsy[:foo] = bar`) plus a
201
262
 
202
263
  ```bash
203
264
  bundle install
204
- bundle exec rspec # 34 specs
265
+ bundle exec rspec # 64 specs
205
266
  bundle exec rubocop # lint
206
267
  ```
207
268
 
@@ -8,7 +8,12 @@ module Logsy
8
8
  %r{/lograge/},
9
9
  %r{/sprockets/},
10
10
  %r{/quiet_assets/},
11
- %r{/lib/logsy/}
11
+ %r{/lib/logsy/},
12
+ # Rack middlewares sit in every request's call stack, so without this
13
+ # they'd be picked as the "app frame" for lines emitted below them
14
+ # (e.g. "Started GET ..." would be attributed to a health_check
15
+ # middleware). Matches app/middleware and app/middlewares.
16
+ %r{/app/middlewares?/}
12
17
  ].freeze
13
18
 
14
19
  # Tag keys that should be carried across job boundaries (e.g. when a
@@ -17,18 +22,29 @@ module Logsy
17
22
  # specific (Logsy ships one for Sidekiq).
18
23
  attr_accessor :job_propagated_keys, :ignored_caller_paths,
19
24
  :include_caller_location, :request_summary_event_name,
20
- :caller_location_max_depth
25
+ :job_summary_event_name, :caller_location_max_depth,
26
+ :auto_include_job_hooks, :auto_register_sidekiq_middleware
21
27
 
22
28
  def initialize
23
29
  @job_propagated_keys = [:request_id]
24
30
  @ignored_caller_paths = DEFAULT_IGNORED_CALLER_PATHS.dup
25
31
  @include_caller_location = true
26
32
  @request_summary_event_name = 'request'
33
+ @job_summary_event_name = 'job'
34
+ # When true (the default), the Railtie auto-includes Logsy::JobHooks
35
+ # into ActiveJob::Base, so every job emits a summary wide event with
36
+ # no per-app setup. Set to false to opt out and include it manually.
37
+ @auto_include_job_hooks = true
38
+ # When true (the default), the Railtie registers the bundled Sidekiq
39
+ # middleware (client + server) for you when Sidekiq is loaded, so
40
+ # request_id and friends propagate into jobs with no per-app wiring.
41
+ # Set to false to register them yourself (or not at all).
42
+ @auto_register_sidekiq_middleware = true
27
43
  # How many stack frames to inspect before giving up on attributing
28
44
  # the log line to app code. App frames sit within a few dozen frames
29
45
  # of the logger; framework-only lines (no app frame at all) would
30
46
  # otherwise pay a full walk of a 100+ deep Rails stack on every line.
31
- @caller_location_max_depth = 64
47
+ @caller_location_max_depth = 64
32
48
  end
33
49
  end
34
50
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Logsy
6
+ # Include into your ApplicationJob (or specific jobs) to emit a single
7
+ # "wide event" log line when the job finishes — carrying the job class,
8
+ # queue, ActiveJob id, attempt count, duration_ms, status/error, and
9
+ # every Logsy tag set during the run (job_id/jid, propagated request_id,
10
+ # and anything the job wrote via `Logsy[:key] = value`, e.g. an RRN).
11
+ #
12
+ # This is the background-job counterpart of Logsy::ControllerHooks.
13
+ # Per-job context capture (job_id, job_class, propagated tags) lives in
14
+ # Logsy::SidekiqMiddleware::Server — not here — so even ActiveJob's own
15
+ # "Performing ..." line is already tagged before this hook runs.
16
+ #
17
+ # Usage:
18
+ #
19
+ # class ApplicationJob < ActiveJob::Base
20
+ # include Logsy::JobHooks
21
+ # end
22
+ #
23
+ # Override `logsy_job_summary_extras` to add custom fields to the event:
24
+ #
25
+ # def logsy_job_summary_extras
26
+ # { tenant: account_id }
27
+ # end
28
+ #
29
+ # Job arguments are intentionally omitted — ActiveJob already logs them
30
+ # once at enqueue time ("Enqueued ... with arguments: ..."), correlatable
31
+ # by active_job_id, so repeating them here would only duplicate (and risk
32
+ # leaking) payload data.
33
+ module JobHooks
34
+ extend ActiveSupport::Concern
35
+
36
+ included do
37
+ around_perform :_logsy_emit_job_summary
38
+ end
39
+
40
+ private
41
+
42
+ def _logsy_emit_job_summary
43
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
44
+ error = nil
45
+ yield
46
+ rescue StandardError => e
47
+ error = e
48
+ raise
49
+ ensure
50
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
51
+ payload = {
52
+ event: Logsy.configuration.job_summary_event_name,
53
+ job_class: self.class.name,
54
+ queue: queue_name,
55
+ active_job_id: job_id,
56
+ executions:,
57
+ duration_ms:,
58
+ status: error ? 'error' : 'ok',
59
+ error: error && "#{error.class}: #{error.message}"
60
+ }.merge(logsy_job_summary_extras).compact
61
+
62
+ Rails.logger.info(payload)
63
+ end
64
+
65
+ # Override in subclasses to add fields to the job summary wide event.
66
+ def logsy_job_summary_extras
67
+ {}
68
+ end
69
+ end
70
+ end
@@ -42,7 +42,11 @@ module Logsy
42
42
  level: severity
43
43
  }
44
44
 
45
- add_caller_location!(payload) if Logsy.configuration.include_caller_location
45
+ # Hash messages are structured wide-event emissions from our own
46
+ # ControllerHooks/JobHooks. They're logged from a logsy frame with no
47
+ # app frame above to attribute, so the caller walk only ever scans the
48
+ # full stack to emit no :file — skip it (output is identical either way).
49
+ add_caller_location!(payload) if Logsy.configuration.include_caller_location && !message.is_a?(::Hash)
46
50
  merge_context!(payload)
47
51
  merge_message!(payload, message)
48
52
 
@@ -54,10 +58,11 @@ module Logsy
54
58
  private
55
59
 
56
60
  def add_caller_location!(payload)
57
- location = find_user_frame
61
+ root = app_root # computed once per line, not rebuilt for every frame inspected
62
+ location = find_user_frame(root)
58
63
  return unless location
59
64
 
60
- payload[:file] = "#{relative_path(location.path)}:#{location.lineno}"
65
+ payload[:file] = "#{relative_path(location.path, root)}:#{location.lineno}"
61
66
  end
62
67
 
63
68
  # Walk the stack in small batches instead of capturing it all at once:
@@ -66,14 +71,14 @@ module Logsy
66
71
  # capturing everything costs ~3x more per log line. The walk is capped
67
72
  # at caller_location_max_depth so framework-only lines (no app frame
68
73
  # anywhere) don't pay for a full-stack scan either.
69
- def find_user_frame
74
+ def find_user_frame(root)
70
75
  start = 2 # skip find_user_frame and add_caller_location!
71
76
  limit = start + Logsy.configuration.caller_location_max_depth
72
77
  while start < limit
73
78
  batch = caller_locations(start, [FRAME_BATCH_SIZE, limit - start].min)
74
79
  return nil if batch.nil? || batch.empty?
75
80
 
76
- batch.each { |loc| return loc if user_frame?(loc.path) }
81
+ batch.each { |loc| return loc if user_frame?(loc.path, root) }
77
82
  return nil if batch.size < FRAME_BATCH_SIZE
78
83
 
79
84
  start += FRAME_BATCH_SIZE
@@ -85,10 +90,10 @@ module Logsy
85
90
  # stdlib, or logging machinery. With an app root (Rails), only frames
86
91
  # under it count (vendored gems under root excluded); without one,
87
92
  # any non-gem frame counts.
88
- def user_frame?(path)
93
+ def user_frame?(path, root)
89
94
  return false if Logsy.configuration.ignored_caller_paths.any? { |pattern| path.match?(pattern) }
90
95
 
91
- if (root = app_root)
96
+ if root
92
97
  path.start_with?(root) && !path.include?('/vendor/bundle/')
93
98
  else
94
99
  !gem_frame?(path)
@@ -105,8 +110,7 @@ module Logsy
105
110
  defined?(Rails) && Rails.respond_to?(:root) && Rails.root ? "#{Rails.root}/" : nil
106
111
  end
107
112
 
108
- def relative_path(path)
109
- root = app_root
113
+ def relative_path(path, root)
110
114
  return path unless root && path.start_with?(root)
111
115
 
112
116
  path[root.length..]
@@ -123,7 +127,10 @@ module Logsy
123
127
  def merge_message!(payload, message)
124
128
  case message
125
129
  when ::String
126
- payload[:msg] = message.gsub(ANSI_ESCAPE, '').strip
130
+ # Only ActiveRecord SQL lines carry ANSI codes; skip the regex (and
131
+ # its allocation) on the common plain line with a cheap byte scan.
132
+ cleaned = message.include?("\e") ? message.gsub(ANSI_ESCAPE, '') : message
133
+ payload[:msg] = cleaned.strip
127
134
  when ::Hash
128
135
  message.each { |k, v| payload[k.to_sym] = v }
129
136
  when ::Exception
data/lib/logsy/railtie.rb CHANGED
@@ -1,12 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Logsy
4
- # Inserts the request-id capturing middleware right after
5
- # ActionDispatch::RequestId, before Rails::Rack::Logger — early enough
6
- # that the "Started GET ..." line is already tagged.
4
+ # Wires Logsy into Rails with zero per-app setup:
5
+ #
6
+ # - Inserts the request-id capturing middleware right after
7
+ # ActionDispatch::RequestId, before Rails::Rack::Logger — early enough
8
+ # that the "Started GET ..." line is already tagged.
9
+ # - Includes Logsy::JobHooks into ActiveJob::Base so every job emits a
10
+ # summary wide event (disable via config.auto_include_job_hooks = false).
11
+ # - Registers the bundled Sidekiq middleware when Sidekiq is loaded
12
+ # (disable via config.auto_register_sidekiq_middleware = false).
7
13
  class Railtie < ::Rails::Railtie
8
14
  initializer 'logsy.insert_rack_middleware' do |app|
9
15
  app.middleware.insert_after(ActionDispatch::RequestId, Logsy::RackMiddleware)
10
16
  end
17
+
18
+ initializer 'logsy.include_job_hooks' do
19
+ # Fires when ActiveJob::Base loads (eager load in production, first
20
+ # reference in development) — after config/initializers have run, so
21
+ # an app's auto_include_job_hooks = false is respected.
22
+ ActiveSupport.on_load(:active_job) do
23
+ include(Logsy::JobHooks) if Logsy.configuration.auto_include_job_hooks
24
+ end
25
+ end
26
+
27
+ # Runs after config/initializers so it sees Sidekiq even when the app
28
+ # uses `gem 'sidekiq', require: false` and loads it in an initializer.
29
+ # Registering here still takes effect: Sidekiq reads its middleware
30
+ # chains when jobs are enqueued/run, which is always after boot.
31
+ initializer 'logsy.register_sidekiq_middleware', after: :load_config_initializers do
32
+ next unless Logsy.configuration.auto_register_sidekiq_middleware && defined?(::Sidekiq)
33
+
34
+ require 'logsy/sidekiq_middleware'
35
+ Logsy::SidekiqMiddleware.install!
36
+ end
11
37
  end
12
38
  end
@@ -4,14 +4,17 @@ module Logsy
4
4
  module SidekiqMiddleware
5
5
  # Server-side middleware: reads propagated tags back from the job
6
6
  # payload, sets them on the per-job Logsy store, sets job_id from
7
- # Sidekiq's `jid`, and resets the store after the job runs (even on
8
- # error) so tags don't leak between jobs sharing a worker thread.
7
+ # Sidekiq's `jid` and job_class from the worker (the wrapped ActiveJob
8
+ # class name when run through ActiveJob), and resets the store after the
9
+ # job runs (even on error) so tags don't leak between jobs sharing a
10
+ # worker thread.
9
11
  #
10
12
  # Anything the job code writes via `Logsy[:foo] = bar` while running
11
13
  # will appear on every log line emitted during the job.
12
14
  class Server
13
15
  def call(_worker_instance, job, _queue)
14
16
  Logsy[:job_id] = job['jid']
17
+ Logsy[:job_class] = job['wrapped'] || job['class']
15
18
  Logsy.configuration.job_propagated_keys.each do |key|
16
19
  value = job[key.to_s]
17
20
  Logsy[key] = value if value
@@ -4,18 +4,36 @@ require 'logsy/sidekiq_middleware/client'
4
4
  require 'logsy/sidekiq_middleware/server'
5
5
 
6
6
  module Logsy
7
- # Sidekiq middleware namespace. Require this file (or rely on a Rails
8
- # railtie) to make {Client} and {Server} available, then register them
9
- # with Sidekiq:
7
+ # Sidekiq middleware namespace.
10
8
  #
11
- # Sidekiq.configure_client do |config|
12
- # config.client_middleware { |chain| chain.add(Logsy::SidekiqMiddleware::Client) }
13
- # end
9
+ # In a Rails app there is nothing to do: the Railtie calls {install!} for
10
+ # you once Sidekiq is loaded (opt out with
11
+ # `config.auto_register_sidekiq_middleware = false`).
14
12
  #
15
- # Sidekiq.configure_server do |config|
16
- # config.client_middleware { |chain| chain.add(Logsy::SidekiqMiddleware::Client) }
17
- # config.server_middleware { |chain| chain.add(Logsy::SidekiqMiddleware::Server) }
18
- # end
13
+ # Outside Rails — or after opting out — require this file and register the
14
+ # middleware with a single call:
15
+ #
16
+ # require 'logsy/sidekiq_middleware'
17
+ # Logsy::SidekiqMiddleware.install!
19
18
  module SidekiqMiddleware
19
+ # Registers the bundled middleware with Sidekiq:
20
+ # - Client on the client chain (web/console enqueues a job)
21
+ # - Client on the server's client chain (a running job enqueues another)
22
+ # - Server on the server chain (a worker runs a job)
23
+ #
24
+ # Idempotent: Sidekiq's Chain#add replaces an existing entry, so calling
25
+ # this twice (e.g. auto-registration plus a stray manual call) won't
26
+ # duplicate the middleware. `sidekiq` defaults to the ::Sidekiq constant
27
+ # and is injectable for testing.
28
+ def self.install!(sidekiq = ::Sidekiq)
29
+ sidekiq.configure_client do |config|
30
+ config.client_middleware { |chain| chain.add(Client) }
31
+ end
32
+
33
+ sidekiq.configure_server do |config|
34
+ config.client_middleware { |chain| chain.add(Client) }
35
+ config.server_middleware { |chain| chain.add(Server) }
36
+ end
37
+ end
20
38
  end
21
39
  end
data/lib/logsy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Logsy
4
- VERSION = '0.2.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/logsy.rb CHANGED
@@ -5,6 +5,7 @@ require 'logsy/store'
5
5
  require 'logsy/configuration'
6
6
  require 'logsy/json_formatter'
7
7
  require 'logsy/controller_hooks'
8
+ require 'logsy/job_hooks'
8
9
  require 'logsy/rack_middleware'
9
10
  require 'logsy/railtie' if defined?(Rails::Railtie)
10
11
 
@@ -31,6 +32,35 @@ module Logsy
31
32
  Store.tags[key.to_sym] = value&.to_s
32
33
  end
33
34
 
35
+ # Set several tags at once. Keyword sugar over repeated `Logsy[]=`,
36
+ # so the same String coercion applies. Returns the given tags.
37
+ #
38
+ # Logsy.tag(user_id: user.id, order_id: order.id)
39
+ def tag(**tags)
40
+ tags.each { |key, value| self[key] = value }
41
+ tags
42
+ end
43
+
44
+ # Set tags for the duration of the block, then restore the previous
45
+ # values (deleting keys that were unset before). Use to scope context
46
+ # to a unit of work without leaking it to whatever runs next on the
47
+ # same thread/fiber — handy in service objects, rake tasks, or threads
48
+ # the Rails executor / Sidekiq middleware don't reset for you.
49
+ #
50
+ # Logsy.with(order_id: order.id) do
51
+ # process(order) # every log line here carries order_id
52
+ # end
53
+ #
54
+ # Returns the block's value.
55
+ def with(**tags)
56
+ keys = tags.keys.map(&:to_sym)
57
+ saved = keys.to_h { |key| [key, Store.tags[key]] }
58
+ tag(**tags)
59
+ yield
60
+ ensure
61
+ saved.each { |key, value| value.nil? ? Store.tags.delete(key) : Store.tags[key] = value }
62
+ end
63
+
34
64
  # All tags currently set, as a hash. Used by the formatter on every
35
65
  # log line; consumers can call it for debugging.
36
66
  def tags
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logsy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ruby_is_love
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '7.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activejob
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: rake
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +108,7 @@ files:
94
108
  - lib/logsy.rb
95
109
  - lib/logsy/configuration.rb
96
110
  - lib/logsy/controller_hooks.rb
111
+ - lib/logsy/job_hooks.rb
97
112
  - lib/logsy/json_formatter.rb
98
113
  - lib/logsy/rack_middleware.rb
99
114
  - lib/logsy/railtie.rb