lumberjack_sidekiq 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +12 -0
- data/.github/workflows/continuous_integration.yml +47 -0
- data/.standard.yml +8 -0
- data/CHANGE_LOG.md +11 -0
- data/MIT_LICENSE.txt +20 -0
- data/README.md +197 -0
- data/VERSION +1 -0
- data/lib/lumberjack/sidekiq/job_logger.rb +171 -0
- data/lib/lumberjack/sidekiq/message_formatter.rb +127 -0
- data/lib/lumberjack/sidekiq/tag_passthrough_middleware.rb +54 -0
- data/lib/lumberjack/sidekiq.rb +11 -0
- data/lib/lumberjack_sidekiq.rb +3 -0
- data/lumberjack_sidekiq.gemspec +33 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: af05f3d7358c3375820ed3e3d4b09ea6830de363c6ce38600236a873002d297d
|
4
|
+
data.tar.gz: 69e09c53f370c63009e5612add2d3f471ac1e3bb134dbd94e35a2580f36a3bf8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4ae4f2b993a862c4eca32d4634412fdd9c6178d06698d67fd29038de73ca623528053aad65a04e6407196bb2cc120b65aa23620cceb1a070d14547bbc97b33cb
|
7
|
+
data.tar.gz: 92392ec7a0b70eaac5cd20b8bc71c864dd517c49ee91d0dbaf3890dae58f42c2bef06668b883a1add2f98e221bc7282c0b868f0e72d04649168b057eb1f4b531
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Dependabot update strategy
|
2
|
+
version: 2
|
3
|
+
updates:
|
4
|
+
- package-ecosystem: bundler
|
5
|
+
directory: "/"
|
6
|
+
schedule:
|
7
|
+
interval: weekly
|
8
|
+
allow:
|
9
|
+
# Automatically keep all runtime dependencies updated
|
10
|
+
- dependency-name: "*"
|
11
|
+
dependency-type: "production"
|
12
|
+
versioning-strategy: lockfile-only
|
@@ -0,0 +1,47 @@
|
|
1
|
+
name: Continuous Integration
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- master
|
7
|
+
- actions-*
|
8
|
+
tags:
|
9
|
+
- v*
|
10
|
+
pull_request:
|
11
|
+
branches-ignore:
|
12
|
+
- actions-*
|
13
|
+
workflow_dispatch:
|
14
|
+
|
15
|
+
env:
|
16
|
+
BUNDLE_CLEAN: "true"
|
17
|
+
BUNDLE_PATH: vendor/bundle
|
18
|
+
BUNDLE_JOBS: 3
|
19
|
+
BUNDLE_RETRY: 3
|
20
|
+
|
21
|
+
jobs:
|
22
|
+
build:
|
23
|
+
name: ${{ matrix.ruby }} build
|
24
|
+
runs-on: ubuntu-latest
|
25
|
+
strategy:
|
26
|
+
matrix:
|
27
|
+
include:
|
28
|
+
- ruby: "ruby"
|
29
|
+
standardrb: true
|
30
|
+
- ruby: "3.0"
|
31
|
+
appraisal: "sidekiq_8"
|
32
|
+
- ruby: "2.7"
|
33
|
+
appraisal: "sidekiq_7"
|
34
|
+
steps:
|
35
|
+
- uses: actions/checkout@v2
|
36
|
+
- name: Set up Ruby
|
37
|
+
uses: ruby/setup-ruby@v1
|
38
|
+
with:
|
39
|
+
ruby-version: ${{ matrix.ruby}}
|
40
|
+
- name: Install gems
|
41
|
+
run: |
|
42
|
+
bundle install
|
43
|
+
- name: Run Tests
|
44
|
+
run: bundle exec rake
|
45
|
+
- name: standardrb
|
46
|
+
if: matrix.standardrb
|
47
|
+
run: bundle exec standardrb
|
data/.standard.yml
ADDED
data/CHANGE_LOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## 1.0.0
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- Initial release
|
data/MIT_LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2025 Brian Durand
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
# Lumberjack Sidekiq
|
2
|
+
|
3
|
+
[](https://github.com/bdurand/lumberjack_sidekiq/actions/workflows/continuous_integration.yml)
|
4
|
+
[](https://github.com/testdouble/standard)
|
5
|
+
[](https://badge.fury.io/rb/lumberjack_sidekiq)
|
6
|
+
|
7
|
+
This gem provides an enhanced logging setup for [Sidekiq](https://github.com/mperham/sidekiq) using the [lumberjack](https://github.com/bdurand/lumberjack) structured logging framework. It replaces Sidekiq's default job logging behavior with one that provides rich structured logging with automatic tagging, timing information, and context propagation.
|
8
|
+
|
9
|
+
**Key Features:**
|
10
|
+
|
11
|
+
- **Structured Job Logging**: Automatically adds structured tags for job metadata (class, job ID, queue, duration, etc.)
|
12
|
+
- **Context Propagation**: Pass log tags from client to server to maintain request context across job execution
|
13
|
+
- **Flexible Configuration**: Control logging behavior per job with options for log levels, argument filtering, and custom tags
|
14
|
+
- **Performance Tracking**: Automatic timing of job execution and queue wait times
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
### Job Logger
|
19
|
+
|
20
|
+
The `Lumberjack::Sidekiq::JobLogger` provides structured logging for Sidekiq jobs with automatic tagging and timing information.
|
21
|
+
|
22
|
+
To use it, configure Sidekiq to use the Lumberjack job logger:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require 'lumberjack_sidekiq'
|
26
|
+
|
27
|
+
# Firat you'll need a Lumberjack logger instance
|
28
|
+
logger = Lumberjack::Logger.new(STDOUT)
|
29
|
+
|
30
|
+
# Configure Sidekiq to use Lumberjack
|
31
|
+
Sidekiq.configure_server do |config|
|
32
|
+
config.logger = logger
|
33
|
+
config[:job_logger] = Lumberjack::Sidekiq::JobLogger
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
The job logger automatically adds structured tags to your log entries:
|
38
|
+
|
39
|
+
- `class` - The worker class name
|
40
|
+
- `jid` - The job ID
|
41
|
+
- `bid` - The batch ID (if using Sidekiq batch)
|
42
|
+
- `queue` - The queue name
|
43
|
+
- `duration` - Job execution time in seconds
|
44
|
+
- `enqueued_ms` - Time the job was queued before execution
|
45
|
+
- `retry_count` - Number of retries (if > 0)
|
46
|
+
- `tags` - Any custom Sidekiq tags
|
47
|
+
|
48
|
+
You can add an optional prefix to all tags:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
Sidekiq.configure_server do |config|
|
52
|
+
config[:log_tag_prefix] = "sidekiq."
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
### Tag Passthrough Middleware
|
57
|
+
|
58
|
+
The `Lumberjack::Sidekiq::TagPassthroughMiddleware` allows you to pass log tags from the client (where jobs are enqueued) to the server (where jobs are executed). This is useful for maintaining context like user IDs or request IDs across the job execution.
|
59
|
+
|
60
|
+
Configure the middleware on the client side:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
Sidekiq.configure_client do |config|
|
64
|
+
config.client_middleware do |chain|
|
65
|
+
# Pass through :user_id and :request_id tags to the job logger
|
66
|
+
chain.add(Lumberjack::Sidekiq::TagPassthroughMiddleware, :user_id, :request_id)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
Now when you enqueue a job with those tags in the current logging context, they will be propagated to the logs when the job runs.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
logger.tag(user_id: 123, request_id: "abc-def") do
|
75
|
+
MyWorker.perform_async(params)
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
### Adding Additional Metadata
|
80
|
+
|
81
|
+
You can add additional metadata to your job logs by adding your own server middleware. Job logging sets up a tag context so any tags you add in your middleware will be included in the job log when it finishes.
|
82
|
+
|
83
|
+
Tags added before the `yield` in your middleware will be included in all logs for the job processing. Tags added after the `yield` will only be included in the final final job lifecycle event log.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
class MyLogTaggingMiddleware
|
87
|
+
include Sidekiq::ServerMiddleware
|
88
|
+
|
89
|
+
def call(worker, job, queue)
|
90
|
+
# Add tag_1 to all logs for this job.
|
91
|
+
Sidekiq.logger.tag(tag_1: job["value_1"]) if Sidekiq.logger.is_a?(Lumberjack::Logger)
|
92
|
+
|
93
|
+
yield
|
94
|
+
|
95
|
+
# Add tag_2 only to the final job log only.
|
96
|
+
Sidekiq.logger.tag(tag_2: job["value_2"]) if Sidekiq.logger.is_a?(Lumberjack::Logger)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
Sidekiq.configure_server do |config|
|
101
|
+
config.server_middleware do |chain|
|
102
|
+
chain.add MyLogTaggingMiddleware
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
### Job-Level Logging Options
|
108
|
+
|
109
|
+
You can control logging behavior on a per-job basis by setting logging options:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class MyWorker
|
113
|
+
include Sidekiq::Worker
|
114
|
+
|
115
|
+
sidekiq_options logging: {
|
116
|
+
level: "warn", # Set log level for this job
|
117
|
+
skip: false, # Skip logging lifecycle events for this job
|
118
|
+
skip_start: true, # Skip the "Start job" lifecycle log message
|
119
|
+
args: ["param1"], # Only log specific arguments by name; can specify false to omit all args
|
120
|
+
tags: {custom: "value"} # Add custom tags to job logs
|
121
|
+
}
|
122
|
+
|
123
|
+
def perform(param1, param2)
|
124
|
+
# Your job logic here
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
### Configuration Options
|
130
|
+
|
131
|
+
You can globally disable logging job start events by setting `:skip_start_job_logging` to `true` in the Sidekiq configuration.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
Sidekiq.configure_server do |config|
|
135
|
+
config[:skip_start_job_logging] = true
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
You can add a prefix to all automatically generated log tags by setting `:log_tag_prefix`.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
Sidekiq.configure_server do |config|
|
143
|
+
config[:log_tag_prefix] = "sidekiq."
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
You can disable logging the enqueued time by setting `:skip_enqueued_time_logging` to `true`.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
Sidekiq.configure_server do |config|
|
151
|
+
config[:skip_enqueued_time_logging] = true
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
You can disable logging any job arguments by setting `:skip_logging_job_arguments` to `true`.
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
Sidekiq.configure_server do |config|
|
159
|
+
config[:skip_logging_job_arguments] = true
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
You can customize the message format by implementing your own `Lumberjack::Sidekiq::MessageFormatter` and setting it in the configuration. You can use this if you existing log processing pipeline is expecting specific message formats.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
Sidekiq.configure_server do |config|
|
167
|
+
config[:job_logger_message_formatter] = MyCustomMessageFormatter.new(config)
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
## Installation
|
172
|
+
|
173
|
+
Add this line to your application's Gemfile:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
gem "lumberjack_sidekiq"
|
177
|
+
```
|
178
|
+
|
179
|
+
And then execute:
|
180
|
+
```bash
|
181
|
+
$ bundle install
|
182
|
+
```
|
183
|
+
|
184
|
+
Or install it yourself as:
|
185
|
+
```bash
|
186
|
+
$ gem install lumberjack_sidekiq
|
187
|
+
```
|
188
|
+
|
189
|
+
## Contributing
|
190
|
+
|
191
|
+
Open a pull request on GitHub.
|
192
|
+
|
193
|
+
Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
|
194
|
+
|
195
|
+
## License
|
196
|
+
|
197
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is a replacement for Sidekiq's built in JobLogger. Like the built in JobLogger, it
|
4
|
+
# will log job lifecycle events (start, end, failure) with timing information and job metadata.
|
5
|
+
# It the standard metadata for jobs:
|
6
|
+
# - Job class name
|
7
|
+
# - Job ID
|
8
|
+
# - Duration of job execution
|
9
|
+
# - Tags from the current Sidekiq context
|
10
|
+
#
|
11
|
+
# It will also include additional metadata:
|
12
|
+
# - Queue name
|
13
|
+
# - Retry count
|
14
|
+
# - Enqueued time in milliseconds (if available)
|
15
|
+
#
|
16
|
+
# Log messages will also include more information to be human readable include the jog arguments:
|
17
|
+
#
|
18
|
+
# Finished Sidekiq job MyWorker.perform("foo", 12)`
|
19
|
+
#
|
20
|
+
# You can specify at the worker level if you want to suppress arguments with the `logging => args` option:
|
21
|
+
#
|
22
|
+
# sidekiq_options logging: {args: [:arg1]} # only `arg1` will appear in the logs
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# Sidekiq.configure_server do |config|
|
26
|
+
# config.logger = Lumberjack::Sidekiq::JobLogger.new(config)
|
27
|
+
# end
|
28
|
+
class Lumberjack::Sidekiq::JobLogger
|
29
|
+
def initialize(config)
|
30
|
+
@config = config
|
31
|
+
@logger = @config.logger
|
32
|
+
@prefix = @config[:log_tag_prefix] || ""
|
33
|
+
@message_formatter = @config[:job_logger_message_formatter] || Lumberjack::Sidekiq::MessageFormatter.new(@config)
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(job, _queue)
|
37
|
+
enqueued_time = enqueued_time_ms(job) unless skip_enqueued_time_logging?
|
38
|
+
begin
|
39
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
40
|
+
log_start_job(job) unless skip_start_job_logging?(job)
|
41
|
+
|
42
|
+
yield
|
43
|
+
|
44
|
+
log_end_job(job, start, enqueued_time) unless skip_logging?(job)
|
45
|
+
rescue Exception => err # rubocop:disable Lint/RescueException
|
46
|
+
log_failed_job(job, err, start, enqueued_time) unless skip_logging?(job)
|
47
|
+
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# If true don't log the start of the job.
|
53
|
+
def skip_start_job_logging?(job)
|
54
|
+
return true if @config[:skip_start_job_logging]
|
55
|
+
return true if skip_logging?(job)
|
56
|
+
|
57
|
+
logging_options = job["logging"]
|
58
|
+
return false unless logging_options.is_a?(Hash)
|
59
|
+
|
60
|
+
!!logging_options["skip_start"]
|
61
|
+
end
|
62
|
+
|
63
|
+
def skip_logging?(job)
|
64
|
+
logging_options = job["logging"]
|
65
|
+
return false unless logging_options.is_a?(Hash)
|
66
|
+
|
67
|
+
!!logging_options["skip"]
|
68
|
+
end
|
69
|
+
|
70
|
+
def skip_enqueued_time_logging?
|
71
|
+
@config[:skip_enqueued_time_logging] || false
|
72
|
+
end
|
73
|
+
|
74
|
+
def prepare(job, &block)
|
75
|
+
return yield unless @logger.is_a?(Lumberjack::Logger)
|
76
|
+
|
77
|
+
tags = {
|
78
|
+
"#{@prefix}class" => worker_class(job),
|
79
|
+
"#{@prefix}jid" => job["jid"]
|
80
|
+
}
|
81
|
+
tags["#{@prefix}bid"] = job["bid"] if job.include?("bid")
|
82
|
+
tags["#{@prefix}tags"] = job["tags"] if job.include?("tags")
|
83
|
+
|
84
|
+
persisted_tags = passthrough_tags(job)
|
85
|
+
tags.merge!(persisted_tags) if persisted_tags.is_a?(Hash)
|
86
|
+
|
87
|
+
@logger.tag(tags) do
|
88
|
+
level = job.dig("logging", "level") || job["log_level"]
|
89
|
+
if level
|
90
|
+
@logger.silence(level, &block)
|
91
|
+
else
|
92
|
+
yield
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def log_start_job(job)
|
100
|
+
message = @message_formatter.start_job(job)
|
101
|
+
if @logger.is_a?(Lumberjack::Logger)
|
102
|
+
tags = job_tags(job)
|
103
|
+
@logger.info(message, tags)
|
104
|
+
else
|
105
|
+
@logger.info(message)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def log_end_job(job, start, enqueued_time)
|
110
|
+
message = @message_formatter.end_job(job, elapsed_time(start))
|
111
|
+
if @logger.is_a?(Lumberjack::Logger)
|
112
|
+
tags = job_tags(job)
|
113
|
+
tags["#{@prefix}duration"] = elapsed_time(start)
|
114
|
+
tags["#{@prefix}enqueued_ms"] = enqueued_time if enqueued_time
|
115
|
+
@logger.info(message, tags)
|
116
|
+
else
|
117
|
+
@logger.info(message)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def log_failed_job(job, err, start, enqueued_time)
|
122
|
+
message = @message_formatter.failed_job(job, err, elapsed_time(start))
|
123
|
+
if @logger.is_a?(Lumberjack::Logger)
|
124
|
+
tags = job_tags(job)
|
125
|
+
tags["#{@prefix}duration"] = elapsed_time(start)
|
126
|
+
tags["#{@prefix}enqueued_ms"] = enqueued_time if enqueued_time
|
127
|
+
@logger.error(message, tags)
|
128
|
+
else
|
129
|
+
@logger.error(message)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def elapsed_time(start)
|
134
|
+
(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start).round(6)
|
135
|
+
end
|
136
|
+
|
137
|
+
def enqueued_time_ms(job)
|
138
|
+
enqueued_at = job["enqueued_at"]
|
139
|
+
return nil unless enqueued_at.is_a?(Numeric)
|
140
|
+
|
141
|
+
# Older versions of Sidekiq stored the time as the number of seconds in a float.
|
142
|
+
# As of Sidekiq 8 it is stored as an integer in milliseconds.
|
143
|
+
enqueued_at = (enqueued_at * 1000).round if enqueued_at.is_a?(Float)
|
144
|
+
enqueued_ms = ((Time.now.to_f * 1000) - enqueued_at).round
|
145
|
+
enqueued_ms = 0 if enqueued_ms < 0
|
146
|
+
enqueued_ms
|
147
|
+
end
|
148
|
+
|
149
|
+
def job_tags(job)
|
150
|
+
tags = {}
|
151
|
+
|
152
|
+
retry_count = job["retry_count"]
|
153
|
+
tags["#{@prefix}retry_count"] = retry_count if retry_count && retry_count > 0
|
154
|
+
|
155
|
+
tags["#{@prefix}queue"] = job["queue"] if job["queue"]
|
156
|
+
|
157
|
+
::Sidekiq::Context.current&.each do |tag, value|
|
158
|
+
tags["#{@prefix}#{tag}"] = value
|
159
|
+
end
|
160
|
+
|
161
|
+
tags
|
162
|
+
end
|
163
|
+
|
164
|
+
def worker_class(job)
|
165
|
+
job["display_class"] || job["wrapped"] || job["class"]
|
166
|
+
end
|
167
|
+
|
168
|
+
def passthrough_tags(job)
|
169
|
+
job.dig("logging", "tags")
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lumberjack::Sidekiq
|
4
|
+
# This class formats log messages for Sidekiq jobs. Out of the box it will log messages like these:
|
5
|
+
#
|
6
|
+
# - Start Sidekiq job MyWorker.perform("foo", 12)
|
7
|
+
# - Finished Sidekiq job MyWorker.perform("foo", 12) in 123.4ms
|
8
|
+
# - Failed Sidekiq job MyWorker.perform("foo", 12) due to RuntimeError in 123.4ms
|
9
|
+
#
|
10
|
+
# You can control the arguments that are logged by setting the `logging.args` option in your worker:
|
11
|
+
#
|
12
|
+
# sidekiq_options logging: {args: [:arg1]} # only `arg1` will appear in the logs
|
13
|
+
# sidekiq_options logging: {args: false} # no arguments will appear in the logs
|
14
|
+
#
|
15
|
+
# Argument logging can be disabled globally by setting the `skip_logging_job_arguments` option in your
|
16
|
+
# Sidekiq configuration.
|
17
|
+
#
|
18
|
+
# You can override this class or provide your own implementation that implements the `start_job`,
|
19
|
+
# `end_job`, and `failed_job` methods and set it in your Sidekiq configuration:
|
20
|
+
#
|
21
|
+
# Sidekiq.configure_server do |config|
|
22
|
+
# config.job_logger_message_formatter = MyCustomFormatter.new(config)
|
23
|
+
# end
|
24
|
+
class MessageFormatter
|
25
|
+
# @param config [::Sidekiq::Config] The Sidekiq configuration.
|
26
|
+
def initialize(config)
|
27
|
+
@config = config
|
28
|
+
end
|
29
|
+
|
30
|
+
# Formats the start job message.
|
31
|
+
#
|
32
|
+
# @param job [Hash] The job data.
|
33
|
+
# @return [String] The formatted start job message.
|
34
|
+
def start_job(job)
|
35
|
+
"Start Sidekiq job #{job_info(job)}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Formats the end job message.
|
39
|
+
#
|
40
|
+
# @param job [Hash] The job data.
|
41
|
+
# @param elapsed_time [Float] The elapsed time in seconds.
|
42
|
+
# @return [String] The formatted end job message.
|
43
|
+
def end_job(job, elapsed_time)
|
44
|
+
"Finished Sidekiq job #{job_info(job)} in #{(elapsed_time * 1000).round(1)}ms"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Formats the failed job message.
|
48
|
+
#
|
49
|
+
# @param job [Hash] The job data.
|
50
|
+
# @param error [Exception] The exception that was raised.
|
51
|
+
# @param elapsed_time [Float] The elapsed time in seconds.
|
52
|
+
# @return [String] The formatted failed job message.
|
53
|
+
def failed_job(job, error, elapsed_time)
|
54
|
+
"Failed Sidekiq job #{job_info(job)} due to #{error.class.name} in #{(elapsed_time * 1000).round(1)}ms"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Helper method to get the method called on the job worker and format the arguments.
|
58
|
+
#
|
59
|
+
# @param job [Hash] The job data.
|
60
|
+
# @return [String] The formatted job information.
|
61
|
+
# @note If `skip_logging_job_arguments?` is true, it will only return the worker class name.
|
62
|
+
def job_info(job)
|
63
|
+
return worker_class(job) if skip_logging_job_arguments?
|
64
|
+
|
65
|
+
display_args = job_display_args(job)
|
66
|
+
"#{worker_class(job)}.perform(#{display_args.join(", ")})"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Helper method to get the job arguments for logging. The return value is an array
|
70
|
+
# of strings representing the inspect of each argument (i.e. `["foo", 12]` will be
|
71
|
+
# returned as `['"foo"'', '12']`).
|
72
|
+
#
|
73
|
+
# Arguments can be filtered by the `logging.args` option in the worker sidekiq options.
|
74
|
+
#
|
75
|
+
# @param job [Hash] The job data.
|
76
|
+
# @return [Array<String>] The formatted job arguments.
|
77
|
+
def job_display_args(job)
|
78
|
+
logger_options = job["logging"] || {}
|
79
|
+
args_filter = logger_options["args"]
|
80
|
+
args = job["args"]
|
81
|
+
return [] if args.nil?
|
82
|
+
return args.collect(&:inspect) if args_filter == true || args_filter.nil?
|
83
|
+
|
84
|
+
if args_filter == false
|
85
|
+
["..."]
|
86
|
+
else
|
87
|
+
args_filter = Array(args_filter)
|
88
|
+
filtered_args(job, args, args_filter)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns true of job arguments should never be logged.
|
93
|
+
#
|
94
|
+
# @return [Boolean] True if job arguments should not be logged.
|
95
|
+
def skip_logging_job_arguments?
|
96
|
+
@config[:skip_logging_job_arguments] || false
|
97
|
+
end
|
98
|
+
|
99
|
+
# Helper method to get the job worker class name. If the job has a `display_class` or `wrapped` key,
|
100
|
+
# it will return that value for logging purposes.
|
101
|
+
# #
|
102
|
+
# @param job [Hash] The job data.
|
103
|
+
# @return [String] The worker class name.
|
104
|
+
def worker_class(job)
|
105
|
+
job["display_class"] || job["wrapped"] || job["class"]
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def filtered_args(job, args, args_filter)
|
111
|
+
class_name = job["wrapped"] || job["class"]
|
112
|
+
klass = Object.const_get(class_name) if class_name && Object.const_defined?(class_name)
|
113
|
+
return ["..."] unless klass.is_a?(Class)
|
114
|
+
return ["..."] unless klass.instance_methods.include?(:perform)
|
115
|
+
|
116
|
+
perform_args = klass.instance_method(:perform).parameters
|
117
|
+
args.each_with_index.map do |arg, index|
|
118
|
+
arg_name = perform_args[index][1] if perform_args[index]
|
119
|
+
if args_filter.include?(arg_name.to_s)
|
120
|
+
arg.inspect
|
121
|
+
else
|
122
|
+
"-"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
# Sidekiq client middleware that can pass through log tags from the current Lumberjack
|
6
|
+
# logger to job logger when the job is executed on the Sidekiq server. This can be
|
7
|
+
# useful to maintain context in logs when a job is executed.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# Sidekiq.configure_client do |config|
|
11
|
+
# config.client_middleware do |chain|
|
12
|
+
# # Pass through :user_id and :request_id tags to the job logger.
|
13
|
+
# chain.add(Lumberjack::Sidekiq::TagPassthroughMiddleware, :user_id, :request_id)
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
class Lumberjack::Sidekiq::TagPassthroughMiddleware
|
17
|
+
include ::Sidekiq::ClientMiddleware
|
18
|
+
|
19
|
+
JSON_SAFE_TYPES = [String, Integer, Float, TrueClass, FalseClass].freeze
|
20
|
+
|
21
|
+
# @param pass_through_tags [Array<String, Symbol>] Log tags to pass through to the job logger when the job is executed.
|
22
|
+
def initialize(*pass_through_tags)
|
23
|
+
@pass_through_tags = pass_through_tags.flatten.map(&:to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(job_class_or_string, job, queue, redis_pool)
|
27
|
+
return yield unless Sidekiq.logger.is_a?(Lumberjack::Logger)
|
28
|
+
|
29
|
+
job["logging"] ||= {}
|
30
|
+
tags = job["logging"]["tags"] || {}
|
31
|
+
|
32
|
+
@pass_through_tags.each do |tag|
|
33
|
+
value = json_value(Sidekiq.logger.tag_value(tag))
|
34
|
+
tags[tag] = value unless value.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
job["logging"]["tags"] = tags unless tags.empty?
|
38
|
+
|
39
|
+
yield
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def json_value(value)
|
45
|
+
return nil if value.nil?
|
46
|
+
return value if JSON_SAFE_TYPES.include?(value.class)
|
47
|
+
|
48
|
+
begin
|
49
|
+
JSON.parse(JSON.generate(value))
|
50
|
+
rescue JSON::JSONError
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "lumberjack_sidekiq"
|
3
|
+
spec.version = File.read(File.join(__dir__, "VERSION")).strip
|
4
|
+
spec.authors = ["Brian Durand"]
|
5
|
+
spec.email = ["bbdurand@gmail.com"]
|
6
|
+
|
7
|
+
spec.summary = "Structured logging for Sidekiq jobs using the Lumberjack framework with automatic tagging, timing, and context propagation."
|
8
|
+
spec.homepage = "https://github.com/bdurand/lumberjack_sidekiq"
|
9
|
+
spec.license = "MIT"
|
10
|
+
|
11
|
+
# Specify which files should be added to the gem when it is released.
|
12
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
13
|
+
ignore_files = %w[
|
14
|
+
.gitignore
|
15
|
+
.travis.yml
|
16
|
+
Appraisals
|
17
|
+
Gemfile
|
18
|
+
Gemfile.lock
|
19
|
+
Rakefile
|
20
|
+
gemfiles/
|
21
|
+
spec/
|
22
|
+
]
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
|
25
|
+
end
|
26
|
+
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.required_ruby_version = ">= 2.7"
|
30
|
+
|
31
|
+
spec.add_dependency "lumberjack", ">=1.3"
|
32
|
+
spec.add_dependency "sidekiq", ">=7.0"
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lumberjack_sidekiq
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Durand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-07-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: lumberjack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sidekiq
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7.0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- bbdurand@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".github/dependabot.yml"
|
49
|
+
- ".github/workflows/continuous_integration.yml"
|
50
|
+
- ".standard.yml"
|
51
|
+
- CHANGE_LOG.md
|
52
|
+
- MIT_LICENSE.txt
|
53
|
+
- README.md
|
54
|
+
- VERSION
|
55
|
+
- lib/lumberjack/sidekiq.rb
|
56
|
+
- lib/lumberjack/sidekiq/job_logger.rb
|
57
|
+
- lib/lumberjack/sidekiq/message_formatter.rb
|
58
|
+
- lib/lumberjack/sidekiq/tag_passthrough_middleware.rb
|
59
|
+
- lib/lumberjack_sidekiq.rb
|
60
|
+
- lumberjack_sidekiq.gemspec
|
61
|
+
homepage: https://github.com/bdurand/lumberjack_sidekiq
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata: {}
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '2.7'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubygems_version: 3.4.10
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: Structured logging for Sidekiq jobs using the Lumberjack framework with automatic
|
84
|
+
tagging, timing, and context propagation.
|
85
|
+
test_files: []
|