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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +85 -24
- data/lib/logsy/configuration.rb +19 -3
- data/lib/logsy/job_hooks.rb +70 -0
- data/lib/logsy/json_formatter.rb +17 -10
- data/lib/logsy/railtie.rb +29 -3
- data/lib/logsy/sidekiq_middleware/server.rb +5 -2
- data/lib/logsy/sidekiq_middleware.rb +28 -10
- data/lib/logsy/version.rb +1 -1
- data/lib/logsy.rb +30 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 980d9365473b25931ea00736958f1980b5872ec25901dbd09a58347f4eca8c70
|
|
4
|
+
data.tar.gz: 0646f18af7c441b599e61ec5dfb133268cf4d371594b8a08363c6511a7f758d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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'
|
|
137
|
-
Logsy[:user_id]
|
|
138
|
-
Logsy.
|
|
139
|
-
Logsy.
|
|
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 #
|
|
265
|
+
bundle exec rspec # 64 specs
|
|
205
266
|
bundle exec rubocop # lint
|
|
206
267
|
```
|
|
207
268
|
|
data/lib/logsy/configuration.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/logsy/json_formatter.rb
CHANGED
|
@@ -42,7 +42,11 @@ module Logsy
|
|
|
42
42
|
level: severity
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
|
|
8
|
-
#
|
|
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.
|
|
8
|
-
# railtie) to make {Client} and {Server} available, then register them
|
|
9
|
-
# with Sidekiq:
|
|
7
|
+
# Sidekiq middleware namespace.
|
|
10
8
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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
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.
|
|
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
|