diff --git a/CHANGELOG.md b/CHANGELOG.md index 78378c5c..3857312e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - [OpenAPI] Add `openapi:validate` Rake task for OpenAPI tags validation (#163). - [Logger] Add `config.log_redact_keys=` for redacting structured log context. +- [Telemetry] Add `Rage::Telemetry::Capacity` to provide read-only access to metrics describing the server's current load and resource utilization. ### Fixed diff --git a/lib/rage/telemetry/capacity.rb b/lib/rage/telemetry/capacity.rb new file mode 100644 index 00000000..9b1cb12e --- /dev/null +++ b/lib/rage/telemetry/capacity.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Rage::Telemetry + ## + # The `Rage::Telemetry::Capacity` module provides read-only access to + # metrics describing the server's current load and resource utilization. + # Example: how much work is queued up, and how close the server is to its limits. + # + # Unlike spans, capacity metrics aren't tied to a specific operation or + # event, they reflect the server state at the moment they are read, and + # are meant to be sampled periodically rather than triggered by handlers. + # + # Combine with {Rage::Telemetry.every} to report a metric on a fixed + # interval: + # + # Rage::Telemetry.every(1000) do + # MyMetrics.gauge("server.queued_connections", Rage::Telemetry::Capacity.queued_connections) + # end + # + # # Available Metrics + # + # | ---------- Method -------|--------Description-------- | + # | `.queued_connections` | The number of established connections currently waiting in the kernel's accept queue, across the server listening sockets | + # + # @see Rage::Telemetry.every + # + module Capacity + class << self + # Returns the number of established connections currently waiting in the + # kernel accept queue for the server listening sockets. This is the + # count of clients that have already completed the TCP handshake but + # haven't yet been picked up by the application via `accept()`. + # + # A value that stays close to the configured backlog limit is a sign the + # server isn't accepting connections fast enough to keep up with incoming + # traffic. + # + # @return [Integer] the accept-queue depth + def queued_connections + accept_queue_depth = Iodine::Perf.queued_connections + raise NotImplementedError if accept_queue_depth.nil? + + accept_queue_depth + end + end + end +end diff --git a/lib/rage/telemetry/telemetry.rb b/lib/rage/telemetry/telemetry.rb index 45061223..728112c5 100644 --- a/lib/rage/telemetry/telemetry.rb +++ b/lib/rage/telemetry/telemetry.rb @@ -134,4 +134,5 @@ def success? require_relative "tracer" require_relative "handler" +require_relative "capacity" Dir["#{__dir__}/spans/*.rb"].each { |span| require_relative span } diff --git a/rage.gemspec b/rage.gemspec index 6c4da2be..8d3d676b 100644 --- a/rage.gemspec +++ b/rage.gemspec @@ -29,7 +29,7 @@ Gem::Specification.new do |spec| spec.add_dependency "thor", "~> 1.0" spec.add_dependency "rack", "< 4" - spec.add_dependency "rage-iodine", "~> 5.5" + spec.add_dependency "rage-iodine", "~> 5.6" spec.add_dependency "zeitwerk", "~> 2.6" spec.add_dependency "rack-test", "~> 2.1" spec.add_dependency "rake", ">= 12.0" diff --git a/spec/telemetry/capacity_spec.rb b/spec/telemetry/capacity_spec.rb new file mode 100644 index 00000000..497ac8c2 --- /dev/null +++ b/spec/telemetry/capacity_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +RSpec.describe Rage::Telemetry::Capacity do + describe ".queued_connections" do + before do + Fiber.set_scheduler(Rage::FiberScheduler.new) + end + + after do + Fiber.set_scheduler(nil) + end + + it "reports the accept-queue depth of the server's listeners" do + require "socket" + + s = TCPServer.new("127.0.0.1", 0) + port = s.addr[1] + s.close + result = nil + + Iodine.workers = 1 + Iodine.on_state(:on_start) do + Iodine.listen(port: port, handler: -> { [200, {}, ["ok"]] }) + socks = 5.times.map { TCPSocket.new("127.0.0.1", port) } + Iodine.run { result = described_class.queued_connections } + socks.each(&:close) + Iodine.run { Iodine.stop } + end + Iodine.start + + expect(result).to eq(5) + end + + it "is callable from within the reactor via .every" do + require "socket" + + s = TCPServer.new("127.0.0.1", 0) + port = s.addr[1] + s.close + result = nil + + Iodine.workers = 1 + Iodine.on_state(:on_start) do + Iodine.listen(port: port, handler: -> { [200, {}, ["ok"]] }) + Rage::Telemetry.every(5) do + Iodine.run { result = described_class.queued_connections } + Iodine.stop + end + Iodine.run_after(200) { Iodine.stop } + end + Iodine.start + + expect(result).to be_a(Integer) + end + end +end