Class: Stoplight::Infrastructure::Redis::Storage::WindowMetrics
- Defined in:
- lib/stoplight/infrastructure/redis/storage/window_metrics.rb
Overview
Distributed storage for time-windowed light metrics using Redis.
This class implements sliding window metrics using Redis sorted sets (ZSETs) for efficient time-range queries. Events are bucketed by hour to bound memory usage and enable automatic expiration via Redis TTLs.
Storage Structure
Events are stored in hourly buckets as ZSETs:
stoplight:{version}:{system}:{light}:window_metrics:success:1696154400
stoplight:{version}:{system}:{light}:window_metrics:failure:1696154400
Each ZSET member is a unique event ID with its timestamp as the score, enabling O(log N) range queries via ZCOUNT.
Metadata (consecutive counters, last error) is stored in a hash:
stoplight:{version}:{system}:{light}:window_metrics
Bucket Strategy
Fixed 1-hour buckets provide a balance between:
-
Query efficiency: At most ~25 buckets for a 24-hour window
-
Memory efficiency: Natural expiration without manual cleanup
-
Precision: Sub-bucket accuracy via ZSET scores
Atomicity
All operations use Lua scripts to ensure atomicity:
-
record_success: Increments counter and updates metadata in one round-trip
-
record_failure: Same, plus stores serialized error details
-
metrics_snapshot: Aggregates across buckets atomically
Instance Method Summary collapse
-
#bucket_key(metric:, time:) ⇒ String
Generates a Redis key for a specific metric and time.
-
#buckets_for_window(metric:, window_end:) ⇒ Object
private
Retrieves the list of Redis bucket keys required to cover a specific time window.
- #clear ⇒ Object
-
#initialize(redis:, scripting:, config:, clock:, key_space:) ⇒ WindowMetrics
constructor
A new instance of WindowMetrics.
-
#metrics_snapshot ⇒ Stoplight::Domain::Metrics
Get metrics for the current light.
-
#record_failure(exception) ⇒ void
Records failed circuit breaker execution.
-
#record_success ⇒ void
Records successful circuit breaker execution.
Methods inherited from Metrics
#deserialize_failure, #serialize_exception
Constructor Details
#initialize(redis:, scripting:, config:, clock:, key_space:) ⇒ WindowMetrics
Returns a new instance of WindowMetrics.
40 41 42 43 44 45 46 47 48 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 40 def initialize(redis:, scripting:, config:, clock:, key_space:) @clock = clock @scripting = scripting @redis = redis @config = config @key_space = key_space @metrics_key = key_space.key(:window_metrics) @window_size = T.must(config.window_size).to_i end |
Instance Method Details
#bucket_key(metric:, time:) ⇒ String
Generates a Redis key for a specific metric and time.
127 128 129 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 127 def bucket_key(metric:, time:) key_space.key(:window_metrics, metric, (time.to_i / bucket_size) * bucket_size) end |
#buckets_for_window(metric:, window_end:) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Retrieves the list of Redis bucket keys required to cover a specific time window.
137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 137 def buckets_for_window(metric:, window_end:) window_end_ts = window_end.to_i window_start_ts = window_end_ts - @window_size # Find bucket timestamps that contain any part of the window start_bucket = (window_start_ts / bucket_size) * bucket_size # End bucket is the last bucket that contains data within our window end_bucket = ((window_end_ts - 1) / bucket_size) * bucket_size (start_bucket..end_bucket).step(bucket_size).map do |bucket_start| bucket_key(metric: metric, time: bucket_start) end end |
#clear ⇒ Object
112 113 114 115 116 117 118 119 120 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 112 def clear window_end_ts = clock.current_time.to_f failure_keys = failure_bucket_keys(window_end_ts) success_keys = success_bucket_keys(window_end_ts) redis.with do |client| client.del(metrics_key, *failure_keys, *success_keys) end end |
#metrics_snapshot ⇒ Stoplight::Domain::Metrics
Get metrics for the current light
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 53 def metrics_snapshot window_end_ts = clock.current_time.to_f window_start_ts = window_end_ts - @window_size failure_keys = failure_bucket_keys(window_end_ts) success_keys = success_bucket_keys(window_end_ts) successes, errors, last_success_at, last_error_json, consecutive_errors, consecutive_successes = scripting.call( :"window_metrics/metrics_snapshot", args: [ failure_keys.count, window_start_ts, window_end_ts, "last_success_at", "last_error_json", "consecutive_errors", "consecutive_successes" ], keys: [ metrics_key, *success_keys, *failure_keys ] ) Domain::MetricsSnapshot.new( successes:, errors:, consecutive_errors: [consecutive_errors.to_i, errors].min, consecutive_successes: [consecutive_successes.to_i, successes].min, last_error: deserialize_failure(last_error_json), last_success_at: (clock.at(last_success_at.to_f) if last_success_at) ) end |
#record_failure(exception) ⇒ void
This method returns an undefined value.
Records failed circuit breaker execution
102 103 104 105 106 107 108 109 110 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 102 def record_failure(exception) = clock.current_time.to_f scripting.call( :"window_metrics/record_failure", args: [, SecureRandom.hex(12), serialize_exception(exception, timestamp:), bucket_ttl, metrics_ttl], keys: [metrics_key, errors_key(time: )] ) end |
#record_success ⇒ void
This method returns an undefined value.
Records successful circuit breaker execution
85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 85 def record_success = clock.current_time.to_f scripting.call( :"window_metrics/record_success", args: [, SecureRandom.hex(12), bucket_ttl, metrics_ttl], keys: [ metrics_key, successes_key(time: ) ] ) end |