sidekiq-memory_logger 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d0d86186cadbe56a86c0d9f503f95bffe809038454a011ebe2878a7caf9190cc
4
+ data.tar.gz: 74d82b63659d8562d314bd0c62eb75d54a1e58933ad37894b6335882d7a2713c
5
+ SHA512:
6
+ metadata.gz: 03b7ae7daf959020bf30ea8dfafb7d77b4f2cf34ea32099c3954f8a811defec17ba2ab6db8f7cdc39615bf6b88ef5598d136677ab9c6d52e27e64af34975c7ca
7
+ data.tar.gz: 7daa948bfdf7310df265b7174aad891ba0cdc7af27c5f9015602ded35971cd0df3c127ad5c5c34d120ab94ca633cc191b4c2396a227b61c08cf7703616ffe3d9
@@ -0,0 +1,9 @@
1
+ require:
2
+ - ./lib/rubocop/cop/sidekiq_memory_logger
3
+
4
+ SidekiqMemoryLogger/NoConfigureInTests:
5
+ Enabled: true
6
+ Include:
7
+ - 'test/**/*.rb'
8
+ Exclude:
9
+ - test/sidekiq/memory_logger/configure_method_test.rb
@@ -0,0 +1,154 @@
1
+ name: Benchmark
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ benchmark:
12
+ runs-on: ubuntu-latest
13
+
14
+ services:
15
+ redis:
16
+ image: redis:7.0
17
+ ports:
18
+ - 6379:6379
19
+ options: >-
20
+ --health-cmd "redis-cli ping"
21
+ --health-interval 10s
22
+ --health-timeout 5s
23
+ --health-retries 5
24
+
25
+ strategy:
26
+ matrix:
27
+ ruby-version: ['3.4']
28
+
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+
32
+ - name: Set up Ruby ${{ matrix.ruby-version }}
33
+ uses: ruby/setup-ruby@v1
34
+ with:
35
+ ruby-version: ${{ matrix.ruby-version }}
36
+ bundler-cache: true
37
+
38
+ - name: Cache Redis CLI
39
+ uses: actions/cache@v4
40
+ id: cache-redis-tools
41
+ with:
42
+ path: /usr/bin/redis-cli
43
+ key: redis-tools-${{ runner.os }}
44
+
45
+ - name: Install Redis CLI
46
+ if: steps.cache-redis-tools.outputs.cache-hit != 'true'
47
+ run: sudo apt-get update && sudo apt-get install -y redis-tools
48
+
49
+ - name: Wait for Redis
50
+ run: |
51
+ until redis-cli ping; do
52
+ echo "Waiting for Redis..."
53
+ sleep 1
54
+ done
55
+
56
+ - name: Run benchmark without memory logger
57
+ id: benchmark_without
58
+ run: |
59
+ echo "Running benchmark WITHOUT memory logger..."
60
+ output=$(ITER=50 timeout 120 bundle exec bin/sidekiqload 2>&1 || true)
61
+ echo "$output"
62
+
63
+ # Extract performance metrics
64
+ jobs_per_sec=$(echo "$output" | grep -oE '[0-9]+ jobs/sec' | grep -oE '[0-9]+' | head -1 || echo "0")
65
+ ending_rss=$(echo "$output" | grep 'Ending RSS:' | grep -oE '[0-9]+' | head -1 || echo "0")
66
+
67
+ echo "without_jobs_per_sec=$jobs_per_sec" >> $GITHUB_OUTPUT
68
+ echo "without_ending_rss=$ending_rss" >> $GITHUB_OUTPUT
69
+
70
+ # Save full output for comparison
71
+ echo "WITHOUT_OUTPUT<<EOF" >> $GITHUB_ENV
72
+ echo "$output" >> $GITHUB_ENV
73
+ echo "EOF" >> $GITHUB_ENV
74
+
75
+ - name: Run benchmark with memory logger
76
+ id: benchmark_with
77
+ run: |
78
+ echo "Running benchmark WITH memory logger..."
79
+ output=$(MEMORY_LOGGER=1 ITER=50 timeout 120 bundle exec bin/sidekiqload 2>&1 || true)
80
+ echo "$output"
81
+
82
+ # Extract performance metrics
83
+ jobs_per_sec=$(echo "$output" | grep -oE '[0-9]+ jobs/sec' | grep -oE '[0-9]+' | head -1 || echo "0")
84
+ ending_rss=$(echo "$output" | grep 'Ending RSS:' | grep -oE '[0-9]+' | head -1 || echo "0")
85
+
86
+ echo "with_jobs_per_sec=$jobs_per_sec" >> $GITHUB_OUTPUT
87
+ echo "with_ending_rss=$ending_rss" >> $GITHUB_OUTPUT
88
+
89
+ # Save full output for comparison
90
+ echo "WITH_OUTPUT<<EOF" >> $GITHUB_ENV
91
+ echo "$output" >> $GITHUB_ENV
92
+ echo "EOF" >> $GITHUB_ENV
93
+
94
+ - name: Compare results
95
+ run: |
96
+ echo "## Benchmark Results Comparison" >> $GITHUB_STEP_SUMMARY
97
+ echo "" >> $GITHUB_STEP_SUMMARY
98
+
99
+ echo "### Without Memory Logger" >> $GITHUB_STEP_SUMMARY
100
+ echo '```' >> $GITHUB_STEP_SUMMARY
101
+ echo "$WITHOUT_OUTPUT" >> $GITHUB_STEP_SUMMARY
102
+ echo '```' >> $GITHUB_STEP_SUMMARY
103
+ echo "" >> $GITHUB_STEP_SUMMARY
104
+
105
+ echo "### With Memory Logger" >> $GITHUB_STEP_SUMMARY
106
+ echo '```' >> $GITHUB_STEP_SUMMARY
107
+ echo "$WITH_OUTPUT" >> $GITHUB_STEP_SUMMARY
108
+ echo '```' >> $GITHUB_STEP_SUMMARY
109
+ echo "" >> $GITHUB_STEP_SUMMARY
110
+
111
+ # Performance comparison
112
+ without_jps=${{ steps.benchmark_without.outputs.without_jobs_per_sec }}
113
+ with_jps=${{ steps.benchmark_with.outputs.with_jobs_per_sec }}
114
+
115
+ echo "### Performance Metrics" >> $GITHUB_STEP_SUMMARY
116
+ echo "| Metric | Without Logger | With Logger | Difference |" >> $GITHUB_STEP_SUMMARY
117
+ echo "|--------|----------------|-------------|------------|" >> $GITHUB_STEP_SUMMARY
118
+ echo "| Jobs/sec | $without_jps | $with_jps | $(($with_jps - $without_jps)) |" >> $GITHUB_STEP_SUMMARY
119
+
120
+ # Calculate milliseconds per job
121
+ if [ "$without_jps" -gt 0 ] && [ "$with_jps" -gt 0 ]; then
122
+ without_ms_per_job=$(echo "scale=3; 1000.0 / $without_jps" | bc -l)
123
+ with_ms_per_job=$(echo "scale=3; 1000.0 / $with_jps" | bc -l)
124
+ ms_per_job_diff=$(echo "scale=3; $with_ms_per_job - $without_ms_per_job" | bc -l)
125
+ echo "| Milliseconds per job | $without_ms_per_job | $with_ms_per_job | $ms_per_job_diff |" >> $GITHUB_STEP_SUMMARY
126
+ fi
127
+
128
+ - name: Output results to console
129
+ run: |
130
+ echo "=== BENCHMARK RESULTS ==="
131
+ echo "Without Memory Logger:"
132
+ echo " Jobs/sec: ${{ steps.benchmark_without.outputs.without_jobs_per_sec }}"
133
+ echo " Ending RSS: ${{ steps.benchmark_without.outputs.without_ending_rss }} KB"
134
+ echo ""
135
+ echo "With Memory Logger:"
136
+ echo " Jobs/sec: ${{ steps.benchmark_with.outputs.with_jobs_per_sec }}"
137
+ echo " Ending RSS: ${{ steps.benchmark_with.outputs.with_ending_rss }} KB"
138
+ echo ""
139
+ echo "Differences:"
140
+ echo " Jobs/sec difference: $((${{ steps.benchmark_with.outputs.with_jobs_per_sec }} - ${{ steps.benchmark_without.outputs.without_jobs_per_sec }}))"
141
+ echo " RSS difference: $((${{ steps.benchmark_with.outputs.with_ending_rss }} - ${{ steps.benchmark_without.outputs.without_ending_rss }})) KB"
142
+ echo ""
143
+
144
+ # Calculate absolute latency added per job
145
+ without_jps=${{ steps.benchmark_without.outputs.without_jobs_per_sec }}
146
+ with_jps=${{ steps.benchmark_with.outputs.with_jobs_per_sec }}
147
+
148
+ if [ "$without_jps" -gt 0 ] && [ "$with_jps" -gt 0 ]; then
149
+ # Convert jobs/sec to milliseconds per job, then calculate difference
150
+ without_ms_per_job=$(echo "scale=6; 1000.0 / $without_jps" | bc -l)
151
+ with_ms_per_job=$(echo "scale=6; 1000.0 / $with_jps" | bc -l)
152
+ latency_added=$(echo "scale=3; $with_ms_per_job - $without_ms_per_job" | bc -l)
153
+ echo "Memory logger adds $latency_added milliseconds per job"
154
+ fi
@@ -0,0 +1,26 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4']
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Ruby ${{ matrix.ruby-version }}
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: ${{ matrix.ruby-version }}
23
+ bundler-cache: true
24
+
25
+ - name: Run tests
26
+ run: bundle exec rake
data/.standard.yml ADDED
@@ -0,0 +1,5 @@
1
+ ignore:
2
+ - 'bin/**/*'
3
+ extend_config:
4
+ - .custom_rubocop.yml
5
+ ruby_version: 2.7
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Nate Berkopec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # Sidekiq Memory Logger
2
+
3
+ Have you ever seen massive memory increases in your Sidekiq workers? Well, this gem helps you figure out which job is causing it.
4
+
5
+ ![memory](https://github.com/user-attachments/assets/6084306f-1f3e-4fdb-9c4a-fccc63a2942f)
6
+
7
+ ## How it works
8
+
9
+ Memory measurement is handled by the [get_process_mem](https://github.com/zombocom/get_process_mem) gem, which works across all platforms (Windows, macOS, Linux) and both inside and outside of containers. Object allocation tracking uses Ruby's built-in `GC.stat[:total_allocated_objects]`.
10
+
11
+ By default, this gem just logs at `info` level for every job:
12
+ ```
13
+ [MemoryLogger] job=MyJob queue=default memory_mb=15.2 objects=12345
14
+ ```
15
+
16
+ You can also parse this log and create a metric (e.g. with Sumo or Datadog) or change the callback we use (see Configuration below) to create metrics.
17
+
18
+ > [!WARNING]
19
+ > Each job runs on its own thread, but all threads share the same process heap. Since memory measurement is performed at the process level, concurrent job execution can lead to inaccurate memory attribution, since the measured memory usage will include memory increases from other jobs running simultaneously. For example, two jobs running at the same time will report the same memory increase, although only one of those jobs may have allocated any memory at all.
20
+ >
21
+ > **Workaround:** To work around this limitation, collect a large enough sample size and use 95th percentile or maximum metrics along with detailed logging to identify which job classes _consistently_ reproduce memory issues. This statistical approach will help you identify problematic jobs despite the measurement noise from concurrent execution.
22
+
23
+ ## Installation
24
+
25
+ ```ruby
26
+ gem 'sidekiq-memory_logger'
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Basic Setup
32
+
33
+ You must add the middleware to your Sidekiq server configuration:
34
+
35
+ ```ruby
36
+ # config/initializers/sidekiq.rb or similar
37
+ Sidekiq.configure_server do |config|
38
+ config.server_middleware do |chain|
39
+ chain.add Sidekiq::MemoryLogger::Middleware
40
+ end
41
+ end
42
+ ```
43
+
44
+ You're now ready to go.
45
+
46
+ ### Configuration
47
+
48
+ It will just work out of the box, but you can change some of your behavior if you like.
49
+
50
+ ```ruby
51
+ # config/initializers/sidekiq_memory_logger.rb or similar
52
+ Sidekiq::MemoryLogger.configure do |config|
53
+ # Change the logger (default uses Rails.logger if available, otherwise stdout)
54
+ config.logger = MyCustomLogger.new
55
+
56
+ # Specify which queues to monitor (empty array monitors all queues)
57
+ config.queues = ["critical", "mailers"] # Only monitor these queues
58
+ # config.queues = [] # Monitor all queues (default)
59
+
60
+ # Replace the default logging callback with custom behavior
61
+ # The callback now receives job arguments as the 5th parameter
62
+ config.callback = ->(job_class, queue, memory_diff_mb, objects_diff, args) do
63
+ # Example: Extract company_id from job arguments
64
+ # Assuming your job is called like: ProcessCompanyDataJob.perform_async(company_id, other_params)
65
+ company_id = args&.first
66
+
67
+ # StatsD example with company_id
68
+ StatsD.histogram('sidekiq.memory_usage', memory_diff_mb, tags: {
69
+ job_class: job_class,
70
+ queue: queue,
71
+ company_id: company_id
72
+ })
73
+
74
+ # Log with company context
75
+ Rails.logger.info "Job #{job_class} for company #{company_id} on queue #{queue} used #{memory_diff_mb} MB"
76
+
77
+ # Dogstatsd example
78
+ # $dogstatsd.histogram('sidekiq.memory_usage', memory_diff_mb, tags: [
79
+ # "job_class:#{job_class}",
80
+ # "queue:#{queue}",
81
+ # "company_id:#{company_id}"
82
+ # ])
83
+
84
+ # New Relic example
85
+ # NewRelic::Agent.record_metric("Custom/Sidekiq/MemoryUsage/#{queue}/#{job_class}", memory_diff_mb)
86
+ # NewRelic::Agent.add_custom_attributes({
87
+ # 'sidekiq.job_class' => job_class,
88
+ # 'sidekiq.queue' => queue,
89
+ # 'sidekiq.company_id' => company_id
90
+ # })
91
+
92
+ # Datadog tracing example - add attributes to current span
93
+ # span = Datadog::Tracing.active_span
94
+ # if span
95
+ # span.set_tag('sidekiq.memory_usage_mb', memory_diff_mb)
96
+ # span.set_tag('sidekiq.job_class', job_class)
97
+ # span.set_tag('sidekiq.queue', queue)
98
+ # span.set_tag('sidekiq.company_id', company_id)
99
+ # end
100
+ end
101
+
102
+ # The default callback logs memory usage like this:
103
+ # config.callback = ->(job_class, queue, memory_diff_mb, objects_diff, args) do
104
+ # config.logger.info("[MemoryLogger] job=#{job_class} queue=#{queue} memory_mb=#{memory_diff_mb}")
105
+ # end
106
+
107
+ # If you want custom metrics AND logging, include both in your callback:
108
+ config.callback = ->(job_class, queue, memory_diff_mb, objects_diff, args) do
109
+ # Your custom metrics collection
110
+ StatsD.histogram('sidekiq.memory_usage', memory_diff_mb, tags: {
111
+ job_class: job_class,
112
+ queue: queue
113
+ })
114
+
115
+ # Include logging if you still want it
116
+ Rails.logger.info "Job #{job_class} on queue #{queue} used #{memory_diff_mb} MB"
117
+ end
118
+ end
119
+ ```
120
+
121
+ ## Performance Overhead
122
+
123
+ The memory logger middleware introduces some performance overhead due to memory measurement and callback execution. We continuously benchmark this overhead using the official `sidekiqload` tool.
124
+
125
+ According to our benchmarks running in Github Actions ([view workflow](https://github.com/speedshop/sidekiq-memory_logger/actions/workflows/benchmark.yml)), the middleware **adds ~0.16ms of latency per job**. The memory footprint increase is negligible. Consider this overhead when deciding whether to enable the middleware in high-throughput production environments. Use the `queues` config setting to limit this middleware to only the queues you're trying to debug.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "standard/rake"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ end
12
+
13
+ task default: %i[standard test]
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module SidekiqMemoryLogger
6
+ class NoConfigureInTests < Base
7
+ MSG = "Do not use Sidekiq::MemoryLogger.configure in test files. Use Configuration objects instead."
8
+
9
+ def on_send(node)
10
+ if sidekiq_memory_logger_configure?(node)
11
+ add_offense(node)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def sidekiq_memory_logger_configure?(node)
18
+ node.send_type? &&
19
+ node.receiver&.const_type? &&
20
+ node.receiver.const_name == "Sidekiq::MemoryLogger" &&
21
+ node.method_name == :configure
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sidekiq_memory_logger/no_configure_in_tests"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module MemoryLogger
5
+ VERSION = "0.1.1"
6
+ end
7
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "get_process_mem"
4
+ require "sidekiq"
5
+ require_relative "memory_logger/version"
6
+
7
+ module Sidekiq
8
+ module MemoryLogger
9
+ class Error < StandardError; end
10
+
11
+ class Configuration
12
+ attr_accessor :logger, :callback, :queues
13
+
14
+ def initialize
15
+ @logger = default_logger
16
+ @callback = default_callback
17
+ @queues = []
18
+ end
19
+
20
+ private
21
+
22
+ def default_logger
23
+ rails_logger = defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
24
+ rails_logger || fallback_logger
25
+ end
26
+
27
+ def fallback_logger
28
+ require "logger"
29
+ ::Logger.new($stdout)
30
+ end
31
+
32
+ def default_callback
33
+ ->(job_class, queue, memory_diff_mb, objects_diff, args) do
34
+ @logger.info("[MemoryLogger] job=#{job_class} queue=#{queue} memory_mb=#{memory_diff_mb} objects=#{objects_diff}")
35
+ end
36
+ end
37
+ end
38
+
39
+ class << self
40
+ def configuration
41
+ @configuration ||= Configuration.new
42
+ end
43
+
44
+ def configure
45
+ yield configuration
46
+ end
47
+ end
48
+
49
+ class Middleware
50
+ include Sidekiq::ServerMiddleware
51
+
52
+ def initialize(config = nil)
53
+ @memory_logger_config = config || MemoryLogger.configuration
54
+ end
55
+
56
+ def call(worker_instance, job, queue)
57
+ return yield if should_skip_queue?(queue)
58
+
59
+ start_mem = GetProcessMem.new.mb
60
+ start_objects = GC.stat[:total_allocated_objects]
61
+
62
+ begin
63
+ yield
64
+ ensure
65
+ end_mem = GetProcessMem.new.mb
66
+ end_objects = GC.stat[:total_allocated_objects]
67
+ memory_diff = end_mem - start_mem
68
+ objects_diff = end_objects - start_objects
69
+
70
+ begin
71
+ @memory_logger_config.callback.call(job["class"], queue, memory_diff, objects_diff, job["args"])
72
+ rescue => e
73
+ @memory_logger_config.logger.error("Sidekiq memory logger callback failed: #{e.message}")
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def should_skip_queue?(queue)
81
+ return false if @memory_logger_config.queues.empty?
82
+ !@memory_logger_config.queues.include?(queue)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1 @@
1
+ require "sidekiq/memory_logger"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class TestSidekiqMemoryLoggerConfiguration < Minitest::Test
6
+ def setup
7
+ @config = Sidekiq::MemoryLogger::Configuration.new
8
+ end
9
+
10
+ def test_default_values
11
+ refute_nil @config.logger
12
+ assert_kind_of Logger, @config.logger
13
+ refute_nil @config.callback
14
+ assert_kind_of Proc, @config.callback
15
+ end
16
+
17
+ def test_setting_logger
18
+ logger = "test_logger"
19
+ @config.logger = logger
20
+ assert_equal logger, @config.logger
21
+ end
22
+
23
+ def test_setting_callback
24
+ callback = ->(job, queue, memory) { "test" }
25
+ @config.callback = callback
26
+ assert_equal callback, @config.callback
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class TestSidekiqMemoryLoggerConfigureMethod < Minitest::Test
6
+ def teardown
7
+ Sidekiq::MemoryLogger.instance_variable_set(:@configuration, nil)
8
+ end
9
+
10
+ def test_configuration
11
+ Sidekiq::MemoryLogger.configure do |config|
12
+ config.logger = "test_logger"
13
+ config.callback = "test_callback"
14
+ end
15
+
16
+ assert_equal "test_logger", Sidekiq::MemoryLogger.configuration.logger
17
+ assert_equal "test_callback", Sidekiq::MemoryLogger.configuration.callback
18
+ end
19
+
20
+ def test_configuration_object
21
+ config = Sidekiq::MemoryLogger.configuration
22
+ assert_instance_of Sidekiq::MemoryLogger::Configuration, config
23
+ end
24
+
25
+ def test_configuration_is_singleton
26
+ config1 = Sidekiq::MemoryLogger.configuration
27
+ config2 = Sidekiq::MemoryLogger.configuration
28
+ assert_same config1, config2
29
+ end
30
+
31
+ def test_direct_configuration_access
32
+ Sidekiq::MemoryLogger.configuration.logger = "direct_logger"
33
+ assert_equal "direct_logger", Sidekiq::MemoryLogger.configuration.logger
34
+
35
+ callback = ->(job, queue, memory) { "test" }
36
+ Sidekiq::MemoryLogger.configuration.callback = callback
37
+ assert_equal callback, Sidekiq::MemoryLogger.configuration.callback
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class TestSidekiqMemoryLogger < Minitest::Test
6
+ def test_that_it_has_a_version_number
7
+ refute_nil ::Sidekiq::MemoryLogger::VERSION
8
+ end
9
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class TestSidekiqMemoryLoggerMiddleware < Minitest::Test
6
+ def setup
7
+ @job = {"class" => "TestJob", "args" => [123, "test_arg"]}
8
+ @queue = "test_queue"
9
+ end
10
+
11
+ def test_middleware_calls_callback_when_configured
12
+ callback_calls = []
13
+ config = Sidekiq::MemoryLogger::Configuration.new
14
+ config.callback = ->(job_class, queue, memory_diff, objects_diff, args) do
15
+ callback_calls << [job_class, queue, memory_diff, objects_diff, args]
16
+ end
17
+
18
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
19
+ middleware.call(nil, @job, @queue) { sleep 0.01 }
20
+
21
+ assert_equal 1, callback_calls.length
22
+ job_class, queue, memory_diff, _objects_diff, args = callback_calls.first
23
+ assert_equal "TestJob", job_class
24
+ assert_equal "test_queue", queue
25
+ assert_kind_of Float, memory_diff
26
+ assert_equal [123, "test_arg"], args
27
+ end
28
+
29
+ def test_middleware_logs_with_default_callback
30
+ log_output = StringIO.new
31
+ test_logger = Logger.new(log_output)
32
+ config = Sidekiq::MemoryLogger::Configuration.new
33
+ config.logger = test_logger
34
+ # Reset to default callback (which logs)
35
+ config.callback = config.send(:default_callback)
36
+
37
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
38
+ middleware.call(nil, @job, @queue) { sleep 0.01 }
39
+
40
+ log_content = log_output.string
41
+ assert_includes log_content, "[MemoryLogger] job=TestJob queue=test_queue memory_mb="
42
+ assert_includes log_content, "objects="
43
+ end
44
+
45
+ def test_middleware_handles_exceptions
46
+ callback_calls = []
47
+ config = Sidekiq::MemoryLogger::Configuration.new
48
+ config.callback = ->(job_class, queue, memory_diff, objects_diff, args) do
49
+ callback_calls << [job_class, queue, memory_diff, objects_diff, args]
50
+ end
51
+
52
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
53
+
54
+ assert_raises(RuntimeError) do
55
+ middleware.call(nil, @job, @queue) { raise "test error" }
56
+ end
57
+
58
+ assert_equal 1, callback_calls.length
59
+ end
60
+
61
+ def test_middleware_handles_callback_exceptions
62
+ log_output = StringIO.new
63
+ test_logger = Logger.new(log_output)
64
+ config = Sidekiq::MemoryLogger::Configuration.new
65
+ config.logger = test_logger
66
+ config.callback = ->(job_class, queue, memory_diff, objects_diff, args) do
67
+ raise StandardError, "callback error"
68
+ end
69
+
70
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
71
+
72
+ # Middleware should not raise, but should log the error
73
+ middleware.call(nil, @job, @queue) { sleep 0.01 }
74
+
75
+ log_content = log_output.string
76
+ assert_includes log_content, "Sidekiq memory logger callback failed: callback error"
77
+ end
78
+
79
+ def test_middleware_passes_job_arguments_to_callback
80
+ callback_args = nil
81
+ job_with_company_id = {
82
+ "class" => "ProcessCompanyDataJob",
83
+ "args" => [42, "Acme Corp", {"priority" => "high"}]
84
+ }
85
+
86
+ config = Sidekiq::MemoryLogger::Configuration.new
87
+ config.callback = ->(job_class, queue, memory_diff, objects_diff, args) do
88
+ callback_args = args
89
+ end
90
+
91
+ # Create middleware with custom configuration
92
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
93
+ middleware.call(nil, job_with_company_id, @queue) { sleep 0.01 }
94
+
95
+ assert_equal [42, "Acme Corp", {"priority" => "high"}], callback_args
96
+ end
97
+
98
+ def test_middleware_works_with_empty_job_args
99
+ callback_args = nil
100
+ job_without_args = {
101
+ "class" => "CleanupJob",
102
+ "args" => []
103
+ }
104
+
105
+ config = Sidekiq::MemoryLogger::Configuration.new
106
+ config.callback = ->(job_class, queue, memory_diff, objects_diff, args) do
107
+ callback_args = args
108
+ end
109
+
110
+ # Create middleware with custom configuration
111
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
112
+ middleware.call(nil, job_without_args, @queue) { sleep 0.01 }
113
+
114
+ assert_equal [], callback_args
115
+ end
116
+
117
+ def test_middleware_works_with_nil_job_args
118
+ callback_args = :not_set
119
+ job_with_nil_args = {
120
+ "class" => "SpecialJob"
121
+ # Note: no "args" key at all
122
+ }
123
+
124
+ config = Sidekiq::MemoryLogger::Configuration.new
125
+ config.callback = ->(job_class, queue, memory_diff, objects_diff, args) do
126
+ callback_args = args
127
+ end
128
+
129
+ # Create middleware with custom configuration
130
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
131
+ middleware.call(nil, job_with_nil_args, @queue) { sleep 0.01 }
132
+
133
+ assert_nil callback_args
134
+ end
135
+
136
+ def test_middleware_skips_queues_not_in_config
137
+ callback_calls = []
138
+ config = Sidekiq::MemoryLogger::Configuration.new
139
+ config.queues = ["important", "critical"]
140
+ config.callback = ->(job_class, queue, memory_diff, objects_diff, args) do
141
+ callback_calls << [job_class, queue, memory_diff, objects_diff, args]
142
+ end
143
+
144
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
145
+ middleware.call(nil, @job, "unimportant_queue") { sleep 0.01 }
146
+
147
+ assert_equal 0, callback_calls.length
148
+ end
149
+
150
+ def test_middleware_processes_queues_in_config
151
+ callback_calls = []
152
+ config = Sidekiq::MemoryLogger::Configuration.new
153
+ config.queues = ["test_queue", "critical"]
154
+ config.callback = ->(job_class, queue, memory_diff, objects_diff, args) do
155
+ callback_calls << [job_class, queue, memory_diff, objects_diff, args]
156
+ end
157
+
158
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
159
+ middleware.call(nil, @job, "test_queue") { sleep 0.01 }
160
+
161
+ assert_equal 1, callback_calls.length
162
+ job_class, queue, _memory_diff, _objects_diff, _args = callback_calls.first
163
+ assert_equal "TestJob", job_class
164
+ assert_equal "test_queue", queue
165
+ end
166
+
167
+ def test_middleware_processes_all_queues_when_empty_config
168
+ callback_calls = []
169
+ config = Sidekiq::MemoryLogger::Configuration.new
170
+ config.queues = []
171
+ config.callback = ->(job_class, queue, memory_diff, objects_diff, args) do
172
+ callback_calls << [job_class, queue, memory_diff, objects_diff, args]
173
+ end
174
+
175
+ middleware = Sidekiq::MemoryLogger::Middleware.new(config)
176
+ middleware.call(nil, @job, "any_queue") { sleep 0.01 }
177
+
178
+ assert_equal 1, callback_calls.length
179
+ job_class, queue, _memory_diff, _objects_diff, _args = callback_calls.first
180
+ assert_equal "TestJob", job_class
181
+ assert_equal "any_queue", queue
182
+ end
183
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "rails"
5
+
6
+ class TestSidekiqMemoryLoggerRails < Minitest::Test
7
+ def test_rails_is_available_and_responds_to_logger
8
+ # Verify Rails constant exists and has logger method
9
+ assert defined?(Rails), "Rails should be defined"
10
+ assert Rails.respond_to?(:logger), "Rails should respond to logger"
11
+ end
12
+
13
+ def test_configuration_uses_rails_logger_when_rails_logger_available
14
+ # Create a mock Rails with a logger
15
+ original_logger_method = Rails.method(:logger) if Rails.respond_to?(:logger)
16
+ test_logger = Logger.new(StringIO.new)
17
+
18
+ Rails.define_singleton_method(:logger) { test_logger }
19
+
20
+ # Create new configuration
21
+ config = Sidekiq::MemoryLogger::Configuration.new
22
+
23
+ # Should use our test Rails.logger
24
+ assert_equal test_logger, config.logger
25
+ ensure
26
+ # Restore original logger method
27
+ if original_logger_method
28
+ Rails.define_singleton_method(:logger, &original_logger_method)
29
+ end
30
+ end
31
+
32
+ def test_configuration_falls_back_when_rails_logger_nil
33
+ # Ensure Rails.logger returns nil (default state)
34
+ Rails.define_singleton_method(:logger) { nil }
35
+
36
+ # Create new configuration
37
+ config = Sidekiq::MemoryLogger::Configuration.new
38
+
39
+ # Should fall back to stdout logger since Rails.logger is nil
40
+ assert_instance_of Logger, config.logger
41
+ assert_equal $stdout, config.logger.instance_variable_get(:@logdev).dev
42
+ end
43
+
44
+ def test_configuration_falls_back_when_rails_not_available
45
+ # Temporarily hide Rails constant
46
+ rails_backup = Object.send(:remove_const, :Rails) if defined?(Rails)
47
+
48
+ # Create new configuration instance
49
+ config = Sidekiq::MemoryLogger::Configuration.new
50
+
51
+ # Should fall back to stdout logger
52
+ assert_instance_of Logger, config.logger
53
+ assert_equal $stdout, config.logger.instance_variable_get(:@logdev).dev
54
+ ensure
55
+ # Restore Rails constant
56
+ Object.const_set(:Rails, rails_backup) if rails_backup
57
+ end
58
+
59
+ def test_configuration_falls_back_when_rails_logger_unavailable
60
+ # Create mock Rails without logger method
61
+ rails_backup = Object.send(:remove_const, :Rails) if defined?(Rails)
62
+ rails_mock = Class.new
63
+ Object.const_set(:Rails, rails_mock)
64
+
65
+ # Create new configuration instance
66
+ config = Sidekiq::MemoryLogger::Configuration.new
67
+
68
+ # Should fall back to stdout logger since Rails doesn't respond to logger
69
+ assert_instance_of Logger, config.logger
70
+ assert_equal $stdout, config.logger.instance_variable_get(:@logdev).dev
71
+ ensure
72
+ # Restore Rails constant
73
+ Object.send(:remove_const, :Rails)
74
+ Object.const_set(:Rails, rails_backup) if rails_backup
75
+ end
76
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+ require "sidekiq/memory_logger"
5
+
6
+ require "minitest/autorun"
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-memory_logger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Nate Berkopec
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: get_process_mem
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sidekiq
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ description: A Sidekiq server middleware that tracks RSS memory usage for each job
41
+ and provides configurable logging and reporting options
42
+ email:
43
+ - nate.berkopec@speedshop.co
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".custom_rubocop.yml"
49
+ - ".github/workflows/benchmark.yml"
50
+ - ".github/workflows/test.yml"
51
+ - ".standard.yml"
52
+ - LICENSE.txt
53
+ - README.md
54
+ - Rakefile
55
+ - lib/rubocop/cop/sidekiq_memory_logger.rb
56
+ - lib/rubocop/cop/sidekiq_memory_logger/no_configure_in_tests.rb
57
+ - lib/sidekiq-memory_logger.rb
58
+ - lib/sidekiq/memory_logger.rb
59
+ - lib/sidekiq/memory_logger/version.rb
60
+ - test/sidekiq/memory_logger/configuration_test.rb
61
+ - test/sidekiq/memory_logger/configure_method_test.rb
62
+ - test/sidekiq/memory_logger/memory_logger_test.rb
63
+ - test/sidekiq/memory_logger/middleware_test.rb
64
+ - test/sidekiq/memory_logger/rails_test.rb
65
+ - test/test_helper.rb
66
+ homepage: https://github.com/speedshop/sidekiq-memory_logger
67
+ licenses:
68
+ - MIT
69
+ metadata:
70
+ allowed_push_host: https://rubygems.org
71
+ homepage_uri: https://github.com/speedshop/sidekiq-memory_logger
72
+ source_code_uri: https://github.com/speedshop/sidekiq-memory_logger
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 2.7.0
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 3.6.9
88
+ specification_version: 4
89
+ summary: Sidekiq server middleware for logging memory usage per job
90
+ test_files: []