Class: Stoplight::Infrastructure::Redis::Storage::WindowMetrics
- Defined in:
- lib/stoplight/infrastructure/redis/storage/window_metrics.rb,
sig/_private/stoplight/infrastructure/redis/storage/window_metrics.rbs
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 Attribute Summary collapse
-
#clock ⇒ Domain::_Clock
readonly
Returns the value of attribute clock.
-
#config ⇒ Domain::Config
readonly
Returns the value of attribute config.
-
#key_space ⇒ KeySpace
readonly
Returns the value of attribute key_space.
-
#metrics_key ⇒ String
readonly
Returns the value of attribute metrics_key.
-
#redis ⇒ redis
readonly
Returns the value of attribute redis.
-
#scripting ⇒ Scripting
readonly
Returns the value of attribute scripting.
Instance Method Summary collapse
-
#bucket_key(metric:, time:) ⇒ String
Generates a Redis key for a specific metric and time.
-
#bucket_size ⇒ Integer
1 hour.
- #bucket_ttl ⇒ Integer
-
#buckets_for_window(metric:, window_end:) ⇒ Object
private
Retrieves the list of Redis bucket keys required to cover a specific time window.
- #clear ⇒ Object
- #errors_key(time:) ⇒ String
- #failure_bucket_keys(window_end) ⇒ Array[String]
-
#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.
- #success_bucket_keys(window_end) ⇒ Array[String]
- #successes_key(time:) ⇒ String
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 Attribute Details
#clock ⇒ Domain::_Clock (readonly)
Returns the value of attribute clock.
157 158 159 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 157 def clock @clock end |
#config ⇒ Domain::Config (readonly)
Returns the value of attribute config.
6 7 8 |
# File 'sig/_private/stoplight/infrastructure/redis/storage/window_metrics.rbs', line 6 def config @config end |
#key_space ⇒ KeySpace (readonly)
Returns the value of attribute key_space.
158 159 160 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 158 def key_space @key_space end |
#metrics_key ⇒ String (readonly)
Returns the value of attribute metrics_key.
156 157 158 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 156 def metrics_key @metrics_key end |
#redis ⇒ redis (readonly)
Returns the value of attribute redis.
154 155 156 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 154 def redis @redis end |
#scripting ⇒ Scripting (readonly)
Returns the value of attribute scripting.
155 156 157 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 155 def scripting @scripting 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 |
#bucket_size ⇒ Integer
1 hour
160 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 160 def bucket_size = 3600 # 1 hour |
#bucket_ttl ⇒ Integer
161 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 161 def bucket_ttl = @window_size + bucket_size |
#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 |
#errors_key(time:) ⇒ String
165 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 165 def errors_key(time:) = bucket_key(metric: :failure, time:) |
#failure_bucket_keys(window_end) ⇒ Array[String]
167 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 167 def failure_bucket_keys(window_end) = buckets_for_window(metric: :failure, window_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 |
#success_bucket_keys(window_end) ⇒ Array[String]
169 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 169 def success_bucket_keys(window_end) = buckets_for_window(metric: :success, window_end:) |
#successes_key(time:) ⇒ String
163 |
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 163 def successes_key(time:) = bucket_key(metric: :success, time:) |