Class: Stoplight::Infrastructure::Redis::Storage::WindowMetrics

Inherits:
Metrics
  • Object
show all
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

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.

Parameters:

  • metric (Symbol)

    The metric type (e.g., “errors”).

  • time (Time, Numeric)

    The time for which to generate the key.

Returns:

  • (String)

    The generated Redis key.



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.

Parameters:

  • metric

    The metric type (e.g., “errors”).

  • window_end

    The end time of the window (can be a Time object or a numeric timestamp).

Returns:

  • A list of Redis keys for the buckets that cover the 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

#clearObject



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_snapshotStoplight::Domain::Metrics

Get metrics for the current light

Returns:

  • (Stoplight::Domain::Metrics)


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

Parameters:

  • exception (StandardError)


102
103
104
105
106
107
108
109
110
# File 'lib/stoplight/infrastructure/redis/storage/window_metrics.rb', line 102

def record_failure(exception)
  timestamp = clock.current_time.to_f

  scripting.call(
    :"window_metrics/record_failure",
    args: [timestamp, SecureRandom.hex(12), serialize_exception(exception, timestamp:), bucket_ttl, metrics_ttl],
    keys: [metrics_key, errors_key(time: timestamp)]
  )
end

#record_successvoid

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
  timestamp = clock.current_time.to_f

  scripting.call(
    :"window_metrics/record_success",
    args: [timestamp, SecureRandom.hex(12), bucket_ttl, metrics_ttl],
    keys: [
      metrics_key,
      successes_key(time: timestamp)
    ]
  )
end