Skip to content

Instantly share code, notes, and snippets.

@stympy
Last active April 3, 2023 13:54
Show Gist options
  • Save stympy/763f0e75e5058a3c8e262d4c6ef8959e to your computer and use it in GitHub Desktop.
Save stympy/763f0e75e5058a3c8e262d4c6ef8959e to your computer and use it in GitHub Desktop.
Rack middleware to send request metrics to CloudWatch Metrics
require "aws-sdk-cloudwatch"
require "descriptive_statistics/safe"
class CloudWatchMetricsMiddleware
def initialize(app, opts = {})
@app = app
@client = Aws::CloudWatch::Client.new(region: opts[:region] || "us-east-1")
@counts = Queue.new
@timings = Queue.new
@mutex = Mutex.new
@sleep_time = opts[:sleep_time] || 60
@namespace = opts[:namespace] || "MyApp"
@timer_thread = Thread.new { start_timer }
end
def call(env)
start_time = Time.now
response = @app.call(env)
duration_ms = ((Time.now - start_time) * 1000).to_i
# Get status code from response
status_code = response[0]
# Queue metrics to be sent to CloudWatch
@mutex.synchronize do
@counts.push({status_code: status_code, count: 1})
@timings.push({status_code: status_code, duration_ms: duration_ms})
end
response
end
def start_timer
loop do
sleep(@sleep_time)
send_metrics_to_cloudwatch
end
end
def send_metrics_to_cloudwatch
data_to_send = []
counts_by_status_code = Hash.new(0)
timings_by_status_code = Hash.new { |h, k| h[k] = [] }
@mutex.synchronize do
until @counts.empty?
count = @counts.pop
counts_by_status_code[count[:status_code]] += count[:count]
end
until @timings.empty?
timing = @timings.pop
timings_by_status_code[timing[:status_code]] << timing[:duration_ms]
end
counts_by_status_code.each do |status_code, count|
data_to_send << {
metric_name: "RequestCount",
dimensions: [{name: "StatusCode", value: status_code.to_s}],
value: count,
unit: "Count"
}
end
timings_by_status_code.each do |status_code, timings|
data_to_send << {
metric_name: "ResponseTime",
dimensions: [{name: "StatusCode", value: status_code.to_s}],
statistic_values: {
sample_count: timings.size,
sum: timings.sum,
minimum: timings.min,
maximum: timings.max
},
unit: "Milliseconds"
}
data_to_send << {
metric_name: "ResponseTime p90",
dimensions: [{name: "StatusCode", value: status_code.to_s}],
value: DescriptiveStatistics.percentile(90, timings),
unit: "Milliseconds"
}
end
end
return if data_to_send.empty?
data_to_send.each_slice(100) do |slice|
@client.put_metric_data(namespace: @namespace, metric_data: slice)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment