Skip to content

Commit 1fddf03

Browse files
authored
Merge pull request #3 from speedshop/hash-based-report-signature
Refactor report() to use keyword arguments with predefined units
2 parents 92f38ff + bb0fa41 commit 1fddf03

File tree

10 files changed

+81
-73
lines changed

10 files changed

+81
-73
lines changed

lib/speedshop/cloudwatch/active_job.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ def report_job_metrics
1111
begin
1212
if enqueued_at
1313
queue_time = Time.now.to_f - enqueued_at.to_f
14-
dimensions = [{name: "JobClass", value: self.class.name}, {name: "QueueName", value: queue_name}]
15-
Cloudwatch.reporter.report("QueueLatency", queue_time, integration: :active_job, unit: "Seconds", dimensions: dimensions)
14+
Cloudwatch.reporter.report(metric: :QueueLatency, value: queue_time, dimensions: {JobClass: self.class.name, QueueName: queue_name})
1615
end
1716
rescue => e
1817
Speedshop::Cloudwatch.log_error("Failed to collect ActiveJob metrics: #{e.message}", e)

lib/speedshop/cloudwatch/configuration.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
module Speedshop
66
module Cloudwatch
77
class Configuration
8-
attr_accessor :interval, :client, :metrics, :namespaces, :logger, :sidekiq_queues, :dimensions
8+
attr_accessor :interval, :client, :metrics, :namespaces, :logger, :sidekiq_queues, :dimensions, :units
99

1010
def initialize
1111
@interval = 60
@@ -19,6 +19,16 @@ def initialize
1919
rack: [:RequestQueueTime],
2020
active_job: [:QueueLatency]
2121
}
22+
@units = {
23+
Workers: "Count", BootedWorkers: "Count", OldWorkers: "Count", Running: "Count",
24+
Backlog: "Count", PoolCapacity: "Count", MaxThreads: "Count",
25+
EnqueuedJobs: "Count", ProcessedJobs: "Count", FailedJobs: "Count",
26+
ScheduledJobs: "Count", RetryJobs: "Count", DeadJobs: "Count",
27+
Processes: "Count", Capacity: "Count", QueueSize: "Count",
28+
DefaultQueueLatency: "Seconds", QueueLatency: "Seconds",
29+
Utilization: "Percent",
30+
RequestQueueTime: "Milliseconds"
31+
}
2232
@namespaces = {puma: "Puma", sidekiq: "Sidekiq", rack: "Rack", active_job: "ActiveJob"}
2333
@sidekiq_queues = nil
2434
@dimensions = {}

lib/speedshop/cloudwatch/metric_reporter.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,29 @@ def stop!
5353
thread_to_join&.join
5454
end
5555

56-
def report(metric_name, value, integration:, unit: "None", dimensions: [])
57-
namespace = @config.namespaces[integration]
56+
def report(metric:, value:, dimensions: {}, namespace: nil)
57+
metric_name = metric.to_sym
58+
59+
integration = find_integration_for_metric(metric_name)
60+
return unless integration
61+
62+
ns = namespace || @config.namespaces[integration]
63+
unit = @config.units[metric_name] || "None"
5864

5965
if [:rack, :active_job].include?(integration)
6066
@mutex.synchronize { @registered_integrations << integration }
6167
end
6268

6369
return unless metric_allowed?(integration, metric_name)
6470

65-
all_dimensions = dimensions + custom_dimensions
71+
dimensions_array = dimensions.map { |k, v| {name: k.to_s, value: v.to_s} }
72+
all_dimensions = dimensions_array + custom_dimensions
6673

6774
@mutex.synchronize do
68-
@queue << {metric_name: metric_name, value: value, namespace: namespace, unit: unit,
75+
@queue << {metric_name: metric_name.to_s, value: value, namespace: ns, unit: unit,
6976
dimensions: all_dimensions, timestamp: Time.now}
7077
end
7178

72-
# Lazy-init of the reporter thread
7379
start! unless started?
7480
end
7581

@@ -134,6 +140,10 @@ def metric_allowed?(integration, metric_name)
134140
def custom_dimensions
135141
@config.dimensions.map { |name, value| {name: name.to_s, value: value.to_s} }
136142
end
143+
144+
def find_integration_for_metric(metric_name)
145+
@config.metrics.find { |int, metrics| metrics.include?(metric_name.to_sym) }&.first
146+
end
137147
end
138148
end
139149
end

lib/speedshop/cloudwatch/puma.rb

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ def collect_metrics
1818
if stats[:worker_status]
1919
%i[workers booted_workers old_workers].each do |m|
2020
# Submit to SnakeCase tyranny
21-
metric_name = m.to_s.split("_").map(&:capitalize).join
22-
@reporter.report(metric_name, stats[m] || 0, integration: :puma, unit: "Count")
21+
metric_name = m.to_s.split("_").map(&:capitalize).join.to_sym
22+
@reporter.report(metric: metric_name, value: stats[m] || 0)
2323
end
2424
end
2525

@@ -34,10 +34,9 @@ def worker_statuses(stats)
3434
end
3535

3636
def report_worker(stats, idx)
37-
dims = [{name: "WorkerIndex", value: idx.to_s}]
3837
%i[running backlog pool_capacity max_threads].each do |m|
39-
metric_name = m.to_s.split("_").map(&:capitalize).join
40-
@reporter.report(metric_name, stats[m] || 0, integration: :puma, unit: "Count", dimensions: dims)
38+
metric_name = m.to_s.split("_").map(&:capitalize).join.to_sym
39+
@reporter.report(metric: metric_name, value: stats[m] || 0, dimensions: {WorkerIndex: idx.to_s})
4140
end
4241
end
4342
end

lib/speedshop/cloudwatch/rack_middleware.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def call(env)
1212
# Header contains milliseconds since epoch (with optional "t=" prefix).
1313
if (header = env["HTTP_X_REQUEST_START"] || env["HTTP_X_QUEUE_START"])
1414
queue_time = (Time.now.to_f * 1000) - header.gsub("t=", "").to_f
15-
Speedshop::Cloudwatch.reporter.report("RequestQueueTime", queue_time, integration: :rack, unit: "Milliseconds")
15+
Speedshop::Cloudwatch.reporter.report(metric: :RequestQueueTime, value: queue_time)
1616
end
1717
rescue => e
1818
Speedshop::Cloudwatch.log_error("Failed to collect Rack metrics: #{e.message}", e)

lib/speedshop/cloudwatch/sidekiq.rb

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,34 +47,33 @@ def report_stats(stats)
4747
EnqueuedJobs: stats.enqueued, ProcessedJobs: stats.processed, FailedJobs: stats.failed,
4848
ScheduledJobs: stats.scheduled_size, RetryJobs: stats.retry_size, DeadJobs: stats.dead_size,
4949
Workers: stats.workers_size, Processes: stats.processes_size
50-
}.each { |m, v| @reporter.report(m.to_s, v, integration: :sidekiq, unit: "Count") }
51-
@reporter.report("DefaultQueueLatency", stats.default_queue_latency, integration: :sidekiq, unit: "Seconds")
50+
}.each { |m, v| @reporter.report(metric: m, value: v) }
51+
@reporter.report(metric: :DefaultQueueLatency, value: stats.default_queue_latency)
5252
end
5353

5454
def report_utilization(processes)
5555
capacity = processes.sum { |p| p["concurrency"] }
56-
@reporter.report("Capacity", capacity, integration: :sidekiq, unit: "Count")
56+
@reporter.report(metric: :Capacity, value: capacity)
5757

5858
utilization = avg_utilization(processes) * 100.0
59-
@reporter.report("Utilization", utilization, integration: :sidekiq, unit: "Percent") unless utilization.nan?
59+
@reporter.report(metric: :Utilization, value: utilization) unless utilization.nan?
6060

6161
processes.group_by { |p| p["tag"] }.each do |tag, procs|
6262
next unless tag
63-
dims = [{name: "Tag", value: tag}]
6463
capacity = procs.sum { |p| p["concurrency"] }
65-
@reporter.report("Capacity", capacity, integration: :sidekiq, unit: "Count", dimensions: dims)
64+
@reporter.report(metric: :Capacity, value: capacity, dimensions: {Tag: tag})
6665
util = avg_utilization(procs) * 100.0
67-
@reporter.report("Utilization", util, integration: :sidekiq, unit: "Percent", dimensions: dims) unless util.nan?
66+
@reporter.report(metric: :Utilization, value: util, dimensions: {Tag: tag}) unless util.nan?
6867
end
6968
end
7069

7170
def report_process_metrics(processes)
7271
processes.each do |p|
7372
next if p["concurrency"].zero?
7473
util = p["busy"] / p["concurrency"].to_f * 100.0
75-
dims = [{name: "Hostname", value: p["hostname"]}]
76-
dims << {name: "Tag", value: p["tag"]} if p["tag"] && !p["tag"].to_s.empty?
77-
@reporter.report("Utilization", util, integration: :sidekiq, unit: "Percent", dimensions: dims)
74+
dims = {Hostname: p["hostname"]}
75+
dims[:Tag] = p["tag"] if p["tag"] && !p["tag"].to_s.empty?
76+
@reporter.report(metric: :Utilization, value: util, dimensions: dims)
7877
end
7978
end
8079

@@ -86,9 +85,8 @@ def report_queue_metrics
8685
all_queues = ::Sidekiq::Queue.all
8786
queues = (configured.nil? || configured.empty?) ? all_queues : all_queues.select { |q| configured.include?(q.name) }
8887
queues.each do |q|
89-
dims = [{name: "QueueName", value: q.name}]
90-
@reporter.report("QueueLatency", q.latency, integration: :sidekiq, unit: "Seconds", dimensions: dims)
91-
@reporter.report("QueueSize", q.size, integration: :sidekiq, unit: "Count", dimensions: dims)
88+
@reporter.report(metric: :QueueLatency, value: q.latency, dimensions: {QueueName: q.name})
89+
@reporter.report(metric: :QueueSize, value: q.size, dimensions: {QueueName: q.name})
9290
end
9391
end
9492

test/active_job_test.rb

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,29 +41,20 @@ def test_reports_queue_time_when_job_executes
4141
job = TestJob.new("test_arg")
4242
job.enqueued_at = Time.now.to_f - 2.5
4343

44-
reported_metric = nil
45-
reported_value = nil
46-
reported_integration = nil
47-
reported_dimensions = nil
44+
reported_kwargs = nil
4845

4946
reporter = Speedshop::Cloudwatch.reporter
50-
reporter.stub :report, ->(metric, value, integration:, unit: nil, dimensions: nil) {
51-
reported_metric = metric
52-
reported_value = value
53-
reported_integration = integration
54-
reported_dimensions = dimensions
47+
reporter.stub :report, ->(**kwargs) {
48+
reported_kwargs = kwargs
5549
} do
5650
job.perform_now
5751
end
5852

59-
assert_equal "QueueLatency", reported_metric
60-
assert_operator reported_value, :>=, 2.5
61-
assert_equal :active_job, reported_integration
62-
assert_equal 2, reported_dimensions.size
63-
assert_equal "JobClass", reported_dimensions[0][:name]
64-
assert_equal "TestJob", reported_dimensions[0][:value]
65-
assert_equal "QueueName", reported_dimensions[1][:name]
66-
assert_equal "default", reported_dimensions[1][:value]
53+
assert_equal :QueueLatency, reported_kwargs[:metric]
54+
assert_operator reported_kwargs[:value], :>=, 2.5
55+
assert_equal 2, reported_kwargs[:dimensions].size
56+
assert_equal "TestJob", reported_kwargs[:dimensions][:JobClass]
57+
assert_equal "default", reported_kwargs[:dimensions][:QueueName]
6758
end
6859

6960
def test_does_not_report_when_enqueued_at_is_nil
@@ -98,15 +89,15 @@ def test_uses_configured_namespace
9889
job = TestJob.new("test_arg")
9990
job.enqueued_at = Time.now.to_f - 1.0
10091

101-
reported_integration = nil
92+
reported_kwargs = nil
10293
reporter = Speedshop::Cloudwatch.reporter
103-
reporter.stub :report, ->(metric, value, integration:, **kwargs) {
104-
reported_integration = integration
94+
reporter.stub :report, ->(**kwargs) {
95+
reported_kwargs = kwargs
10596
} do
10697
job.perform_now
10798
end
10899

109-
assert_equal :active_job, reported_integration
100+
assert_equal :QueueLatency, reported_kwargs[:metric]
110101
end
111102

112103
def test_respects_active_job_metrics_whitelist

test/metric_reporter_test.rb

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ def teardown
1717
end
1818

1919
def test_queues_metrics
20-
@reporter.report("test_metric", 42, integration: :test)
21-
@reporter.report("another_metric", 100, integration: :test, unit: "Count")
20+
@reporter.report(metric: :test_metric, value: 42)
21+
@reporter.report(metric: :another_metric, value: 100)
2222
end
2323

2424
def test_can_start_and_stop
@@ -27,7 +27,7 @@ def test_can_start_and_stop
2727
end
2828

2929
def test_filters_unregistered_puma_metrics
30-
@reporter.report("Workers", 4, integration: :puma)
30+
@reporter.report(metric: :Workers, value: 4)
3131

3232
queue = @reporter.queue
3333
assert_empty queue
@@ -36,16 +36,16 @@ def test_filters_unregistered_puma_metrics
3636
def test_respects_puma_metrics_whitelist
3737
@reporter.register_collector(:puma) {}
3838
@config.metrics[:puma] = [:Workers]
39-
@reporter.report("Workers", 4, integration: :puma)
40-
@reporter.report("BootedWorkers", 4, integration: :puma)
39+
@reporter.report(metric: :Workers, value: 4)
40+
@reporter.report(metric: :BootedWorkers, value: 4)
4141

4242
queue = @reporter.queue
4343
assert_equal 1, queue.size
4444
assert_equal "Workers", queue.first[:metric_name]
4545
end
4646

4747
def test_filters_unregistered_sidekiq_metrics
48-
@reporter.report("EnqueuedJobs", 10, integration: :sidekiq)
48+
@reporter.report(metric: :EnqueuedJobs, value: 10)
4949

5050
queue = @reporter.queue
5151
assert_empty queue
@@ -54,9 +54,9 @@ def test_filters_unregistered_sidekiq_metrics
5454
def test_respects_sidekiq_metrics_whitelist
5555
@reporter.register_collector(:sidekiq) {}
5656
@config.metrics[:sidekiq] = [:EnqueuedJobs, :QueueLatency]
57-
@reporter.report("EnqueuedJobs", 10, integration: :sidekiq)
58-
@reporter.report("ProcessedJobs", 100, integration: :sidekiq)
59-
@reporter.report("QueueLatency", 5.2, integration: :sidekiq)
57+
@reporter.report(metric: :EnqueuedJobs, value: 10)
58+
@reporter.report(metric: :ProcessedJobs, value: 100)
59+
@reporter.report(metric: :QueueLatency, value: 5.2)
6060

6161
queue = @reporter.queue
6262
assert_equal 2, queue.size
@@ -68,9 +68,9 @@ def test_respects_sidekiq_metrics_whitelist
6868

6969
def test_allows_unknown_namespaces
7070
@config.namespaces[:custom] = "CustomNamespace"
71-
@config.metrics[:custom] = [:custom_metric]
71+
@config.metrics[:custom] = [:my_custom_metric]
7272
@reporter.register_collector(:custom) {}
73-
@reporter.report("custom_metric", 42, integration: :custom)
73+
@reporter.report(metric: :my_custom_metric, value: 42)
7474

7575
queue = @reporter.queue
7676
assert_equal 1, queue.size
@@ -133,7 +133,7 @@ def test_started_detects_dead_thread
133133
def test_adds_custom_dimensions_to_metrics
134134
@config.dimensions = {ServiceName: "myservice-api", Environment: "production"}
135135
@reporter.register_collector(:test) {}
136-
@reporter.report("test_metric", 42, integration: :test, dimensions: [{name: "Region", value: "us-east-1"}])
136+
@reporter.report(metric: :test_metric, value: 42, dimensions: {Region: "us-east-1"})
137137

138138
queue = @reporter.queue
139139
assert_equal 1, queue.size
@@ -154,7 +154,7 @@ def test_adds_custom_dimensions_to_metrics
154154

155155
def test_works_without_custom_dimensions
156156
@reporter.register_collector(:test) {}
157-
@reporter.report("test_metric", 42, integration: :test, dimensions: [{name: "Region", value: "us-east-1"}])
157+
@reporter.report(metric: :test_metric, value: 42, dimensions: {Region: "us-east-1"})
158158

159159
queue = @reporter.queue
160160
assert_equal 1, queue.size
@@ -167,7 +167,7 @@ def test_works_without_custom_dimensions
167167
def test_custom_dimensions_with_no_metric_dimensions
168168
@config.dimensions = {ServiceName: "myservice-api"}
169169
@reporter.register_collector(:test) {}
170-
@reporter.report("test_metric", 42, integration: :test)
170+
@reporter.report(metric: :test_metric, value: 42)
171171

172172
queue = @reporter.queue
173173
assert_equal 1, queue.size
@@ -181,7 +181,7 @@ def test_lazy_startup_on_first_report
181181
@reporter.register_collector(:test) {}
182182
refute @reporter.started?
183183

184-
@reporter.report("test_metric", 42, integration: :test)
184+
@reporter.report(metric: :test_metric, value: 42)
185185

186186
assert @reporter.started?
187187
assert @reporter.thread.alive?
@@ -191,31 +191,31 @@ def test_lazy_startup_does_not_double_start
191191
@reporter.register_collector(:test) {}
192192
refute @reporter.started?
193193

194-
@reporter.report("metric1", 1, integration: :test)
194+
@reporter.report(metric: :metric1, value: 1)
195195
thread1 = @reporter.thread
196196

197-
@reporter.report("metric2", 2, integration: :test)
197+
@reporter.report(metric: :metric2, value: 2)
198198
thread2 = @reporter.thread
199199

200200
assert_same thread1, thread2
201201
end
202202

203203
def test_lazy_startup_restarts_after_stop
204204
@reporter.register_collector(:test) {}
205-
@reporter.report("metric1", 1, integration: :test)
205+
@reporter.report(metric: :metric1, value: 1)
206206
assert @reporter.started?
207207

208208
@reporter.stop!
209209
refute @reporter.started?
210210

211-
@reporter.report("metric2", 2, integration: :test)
211+
@reporter.report(metric: :metric2, value: 2)
212212
assert @reporter.started?
213213
end
214214

215215
def test_lazy_startup_with_unregistered_integration
216216
refute @reporter.started?
217217

218-
@reporter.report("Workers", 4, integration: :puma)
218+
@reporter.report(metric: :Workers, value: 4)
219219

220220
refute @reporter.started?
221221
assert_empty @reporter.queue

test/rack_middleware_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ def test_uses_configured_namespace
7171

7272
reporter = Speedshop::Cloudwatch.reporter
7373

74-
reported_integration = nil
75-
reporter.stub :report, ->(metric_name, value, integration:, **kwargs) { reported_integration = integration } do
74+
reported_kwargs = nil
75+
reporter.stub :report, ->(**kwargs) { reported_kwargs = kwargs } do
7676
call_middleware_with_header("HTTP_X_REQUEST_START")
7777
end
7878

79-
assert_equal :rack, reported_integration
79+
assert_equal :RequestQueueTime, reported_kwargs[:metric]
8080
end
8181

8282
def test_logs_error_when_collection_fails

test/support/doubles.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ def initialize
99
@metrics_collected = []
1010
end
1111

12-
def report(metric_name, value, **options)
13-
@metrics_collected << {name: metric_name, value: value, **options}
12+
def report(metric:, value:, dimensions: {}, namespace: nil)
13+
dims = dimensions.map { |k, v| {name: k.to_s, value: v.to_s} }
14+
@metrics_collected << {name: metric.to_s, value: value, dimensions: dims, namespace: namespace}
1415
end
1516

1617
def register_collector(integration, &block)

0 commit comments

Comments
 (0)