star_gates 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.dictate.toml +19 -0
- data/.pre-commit-config.yaml +20 -0
- data/CHANGELOG.md +52 -3
- data/LICENSE.txt +23 -16
- data/README.md +175 -16
- data/Rakefile +14 -4
- data/lib/star_gates/adapters/base_adapter.rb +65 -0
- data/lib/star_gates/adapters/memory_adapter.rb +93 -0
- data/lib/star_gates/adapters/redis_adapter.rb +119 -0
- data/lib/star_gates/application_star_gate.rb +398 -0
- data/lib/star_gates/circuit.rb +171 -0
- data/lib/star_gates/configuration.rb +172 -0
- data/lib/star_gates/dsl.rb +71 -2
- data/lib/star_gates/errors.rb +8 -0
- data/lib/star_gates/event_store/event_store.rb +79 -0
- data/lib/star_gates/event_store/memory_adapter.rb +78 -0
- data/lib/star_gates/events/base_event.rb +46 -0
- data/lib/star_gates/events/feature_events.rb +97 -0
- data/lib/star_gates/feature.rb +446 -0
- data/lib/star_gates/flags.rb +153 -0
- data/lib/star_gates/gate.rb +100 -0
- data/lib/star_gates/hexagonal/feature_aggregate.rb +250 -0
- data/lib/star_gates/hexagonal/feature_repository.rb +67 -0
- data/lib/star_gates/hexagonal/feature_service.rb +121 -0
- data/lib/star_gates/jsonrpc_rails_adapter.rb +251 -0
- data/lib/star_gates/projections/feature_projection.rb +224 -0
- data/lib/star_gates/registry.rb +58 -0
- data/lib/star_gates/telemetry/opentelemetry_adapter.rb +254 -0
- data/lib/star_gates/telemetry/prometheus_adapter.rb +184 -0
- data/lib/star_gates/telemetry/telemetry_adapter.rb +174 -0
- data/lib/star_gates/version.rb +1 -1
- data/lib/star_gates.rb +308 -8
- metadata +95 -14
- data/sig/star_gates.rbs +0 -4
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StarGates
|
|
4
|
+
module Projections
|
|
5
|
+
class FeatureProjection
|
|
6
|
+
def initialize(adapter)
|
|
7
|
+
@adapter = adapter
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Handle events to build read model
|
|
12
|
+
def handle_event(event)
|
|
13
|
+
case event
|
|
14
|
+
when Events::FeatureCreated
|
|
15
|
+
handle_feature_created(event)
|
|
16
|
+
when Events::FeatureStateChanged
|
|
17
|
+
handle_state_changed(event)
|
|
18
|
+
when Events::FeatureConfigurationChanged
|
|
19
|
+
handle_configuration_changed(event)
|
|
20
|
+
when Events::FeatureExecuted
|
|
21
|
+
handle_execution_recorded(event)
|
|
22
|
+
when Events::FeatureEvaluated
|
|
23
|
+
handle_evaluation_recorded(event)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Query methods
|
|
28
|
+
def find_feature(feature_id)
|
|
29
|
+
@adapter.fetch("feature:#{feature_id}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def find_features_by_state(state)
|
|
33
|
+
state_key = "features_by_state:#{state}"
|
|
34
|
+
feature_ids = @adapter.fetch(state_key) || []
|
|
35
|
+
feature_ids.map { |id| find_feature(id) }.compact
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def all_features
|
|
39
|
+
feature_ids = @adapter.fetch('all_features') || []
|
|
40
|
+
feature_ids.map { |id| find_feature(id) }.compact
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def feature_stats
|
|
44
|
+
{
|
|
45
|
+
total_features: all_features.size,
|
|
46
|
+
by_state: features_by_state_count,
|
|
47
|
+
recent_transitions: recent_transitions_count,
|
|
48
|
+
total_executions: total_executions_count
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def recent_transitions(limit: 10)
|
|
53
|
+
transitions = @adapter.fetch('recent_transitions') || []
|
|
54
|
+
transitions.last(limit).reverse
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def feature_exists?(feature_id)
|
|
58
|
+
@adapter.exists?("feature:#{feature_id}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Clear projection (for testing)
|
|
62
|
+
def clear!
|
|
63
|
+
return unless @adapter.respond_to?(:clear!)
|
|
64
|
+
|
|
65
|
+
@adapter.clear!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def handle_feature_created(event)
|
|
71
|
+
@mutex.synchronize do
|
|
72
|
+
feature_data = {
|
|
73
|
+
id: event.aggregate_id,
|
|
74
|
+
state: :disabled,
|
|
75
|
+
rollout_percentage: event.data[:rollout_percentage],
|
|
76
|
+
group_matcher: event.data[:group_matcher],
|
|
77
|
+
metadata: event.data[:metadata],
|
|
78
|
+
created_at: event.occurred_at,
|
|
79
|
+
updated_at: event.occurred_at,
|
|
80
|
+
transition_count: 0,
|
|
81
|
+
execution_count: 0,
|
|
82
|
+
evaluation_count: 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Store feature
|
|
86
|
+
@adapter.store("feature:#{event.aggregate_id}", feature_data)
|
|
87
|
+
|
|
88
|
+
# Update indexes
|
|
89
|
+
update_all_features_index(event.aggregate_id, :add)
|
|
90
|
+
update_state_index(event.aggregate_id, :disabled, :add)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_state_changed(event)
|
|
95
|
+
@mutex.synchronize do
|
|
96
|
+
feature_data = @adapter.fetch("feature:#{event.aggregate_id}")
|
|
97
|
+
return unless feature_data
|
|
98
|
+
|
|
99
|
+
old_state = feature_data[:state]
|
|
100
|
+
new_state = event.to_state
|
|
101
|
+
|
|
102
|
+
# Update feature data
|
|
103
|
+
feature_data[:state] = new_state
|
|
104
|
+
feature_data[:updated_at] = event.occurred_at
|
|
105
|
+
feature_data[:transition_count] += 1
|
|
106
|
+
|
|
107
|
+
@adapter.store("feature:#{event.aggregate_id}", feature_data)
|
|
108
|
+
|
|
109
|
+
# Update state indexes
|
|
110
|
+
update_state_index(event.aggregate_id, old_state, :remove)
|
|
111
|
+
update_state_index(event.aggregate_id, new_state, :add)
|
|
112
|
+
|
|
113
|
+
# Update recent transitions
|
|
114
|
+
update_recent_transitions(event)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def handle_configuration_changed(event)
|
|
119
|
+
@mutex.synchronize do
|
|
120
|
+
feature_data = @adapter.fetch("feature:#{event.aggregate_id}")
|
|
121
|
+
return unless feature_data
|
|
122
|
+
|
|
123
|
+
feature_data[:updated_at] = event.occurred_at
|
|
124
|
+
@adapter.store("feature:#{event.aggregate_id}", feature_data)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def handle_execution_recorded(event)
|
|
129
|
+
@mutex.synchronize do
|
|
130
|
+
feature_data = @adapter.fetch("feature:#{event.aggregate_id}")
|
|
131
|
+
return unless feature_data
|
|
132
|
+
|
|
133
|
+
feature_data[:execution_count] += 1
|
|
134
|
+
feature_data[:updated_at] = event.occurred_at
|
|
135
|
+
@adapter.store("feature:#{event.aggregate_id}", feature_data)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def handle_evaluation_recorded(event)
|
|
140
|
+
@mutex.synchronize do
|
|
141
|
+
feature_data = @adapter.fetch("feature:#{event.aggregate_id}")
|
|
142
|
+
return unless feature_data
|
|
143
|
+
|
|
144
|
+
feature_data[:evaluation_count] += 1
|
|
145
|
+
feature_data[:updated_at] = event.occurred_at
|
|
146
|
+
@adapter.store("feature:#{event.aggregate_id}", feature_data)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def update_all_features_index(feature_id, operation)
|
|
151
|
+
key = 'all_features'
|
|
152
|
+
feature_ids = @adapter.fetch(key) || []
|
|
153
|
+
|
|
154
|
+
case operation
|
|
155
|
+
when :add
|
|
156
|
+
feature_ids << feature_id unless feature_ids.include?(feature_id)
|
|
157
|
+
when :remove
|
|
158
|
+
feature_ids.delete(feature_id)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
@adapter.store(key, feature_ids)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def update_state_index(feature_id, state, operation)
|
|
165
|
+
key = "features_by_state:#{state}"
|
|
166
|
+
feature_ids = @adapter.fetch(key) || []
|
|
167
|
+
|
|
168
|
+
case operation
|
|
169
|
+
when :add
|
|
170
|
+
feature_ids << feature_id unless feature_ids.include?(feature_id)
|
|
171
|
+
when :remove
|
|
172
|
+
feature_ids.delete(feature_id)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
@adapter.store(key, feature_ids)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def update_recent_transitions(event)
|
|
179
|
+
transitions = @adapter.fetch('recent_transitions') || []
|
|
180
|
+
|
|
181
|
+
transition_data = {
|
|
182
|
+
feature_id: event.aggregate_id,
|
|
183
|
+
from_state: event.from_state,
|
|
184
|
+
to_state: event.to_state,
|
|
185
|
+
event_name: event.event_name,
|
|
186
|
+
context: event.context,
|
|
187
|
+
occurred_at: event.occurred_at
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
transitions << transition_data
|
|
191
|
+
|
|
192
|
+
# Keep only last 100 transitions
|
|
193
|
+
transitions = transitions.last(100) if transitions.size > 100
|
|
194
|
+
|
|
195
|
+
@adapter.store('recent_transitions', transitions)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def features_by_state_count
|
|
199
|
+
states = %i[disabled canary enabled]
|
|
200
|
+
counts = {}
|
|
201
|
+
|
|
202
|
+
states.each do |state|
|
|
203
|
+
feature_ids = @adapter.fetch("features_by_state:#{state}") || []
|
|
204
|
+
counts[state] = feature_ids.size
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
counts
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def recent_transitions_count(days: 7)
|
|
211
|
+
transitions = @adapter.fetch('recent_transitions') || []
|
|
212
|
+
cutoff = Time.now - (days * 24 * 60 * 60)
|
|
213
|
+
|
|
214
|
+
transitions.count do |transition|
|
|
215
|
+
Time.parse(transition[:occurred_at].to_s) > cutoff
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def total_executions_count
|
|
220
|
+
all_features.sum { |feature| feature[:execution_count] || 0 }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
|
|
5
|
+
module StarGates
|
|
6
|
+
class Registry
|
|
7
|
+
def initialize
|
|
8
|
+
# Use Concurrent::Hash for thread-safe operations (already in dependencies)
|
|
9
|
+
@gates = Concurrent::Hash.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def register(name, gate)
|
|
13
|
+
# Concurrent::Hash handles thread-safety automatically
|
|
14
|
+
@gates[name.to_sym] = gate
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def [](name)
|
|
18
|
+
@gates[name.to_sym]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fetch(name, default = nil)
|
|
22
|
+
@gates.fetch(name.to_sym, default)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def include?(name)
|
|
26
|
+
@gates.key?(name.to_sym)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def keys
|
|
30
|
+
@gates.keys
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def values
|
|
34
|
+
@gates.values
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def each(&)
|
|
38
|
+
@gates.each(&)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def clear
|
|
42
|
+
# Concurrent::Hash#clear is already thread-safe
|
|
43
|
+
@gates.clear
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def size
|
|
47
|
+
@gates.size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def empty?
|
|
51
|
+
@gates.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
@gates.dup
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'telemetry_adapter'
|
|
4
|
+
|
|
5
|
+
module StarGates
|
|
6
|
+
module Telemetry
|
|
7
|
+
class OpentelemetryAdapter < TelemetryAdapter
|
|
8
|
+
def initialize(config = {})
|
|
9
|
+
super
|
|
10
|
+
@tracer = config[:tracer] || default_tracer
|
|
11
|
+
@meter = config[:meter] || default_meter
|
|
12
|
+
@service_name = config[:service_name] || 'star_gates'
|
|
13
|
+
@service_version = config[:service_version] || StarGates::VERSION
|
|
14
|
+
setup_instruments
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# OpenTelemetry-specific methods
|
|
18
|
+
def trace_feature_evaluation(feature_id, user_context, &block)
|
|
19
|
+
return block.call unless enabled?
|
|
20
|
+
|
|
21
|
+
@tracer.in_span('feature.evaluate', attributes: {
|
|
22
|
+
'feature.id' => feature_id,
|
|
23
|
+
'feature.user_type' => extract_user_type(user_context),
|
|
24
|
+
'service.name' => @service_name,
|
|
25
|
+
'service.version' => @service_version
|
|
26
|
+
}) do |span|
|
|
27
|
+
start_time = Time.now
|
|
28
|
+
result = block.call
|
|
29
|
+
duration = ((Time.now - start_time) * 1000).round(2)
|
|
30
|
+
|
|
31
|
+
span.set_attribute('feature.eligible', result[:eligible]) if result.is_a?(Hash)
|
|
32
|
+
span.set_attribute('feature.evaluation.duration_ms', duration)
|
|
33
|
+
span.status = OpenTelemetry::Trace::Status.ok
|
|
34
|
+
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def trace_feature_execution(feature_id, state, &block)
|
|
40
|
+
return block.call unless enabled?
|
|
41
|
+
|
|
42
|
+
@tracer.in_span('feature.execute', attributes: {
|
|
43
|
+
'feature.id' => feature_id,
|
|
44
|
+
'feature.state' => state,
|
|
45
|
+
'service.name' => @service_name,
|
|
46
|
+
'service.version' => @service_version
|
|
47
|
+
}) do |span|
|
|
48
|
+
start_time = Time.now
|
|
49
|
+
begin
|
|
50
|
+
result = block.call
|
|
51
|
+
duration = ((Time.now - start_time) * 1000).round(2)
|
|
52
|
+
|
|
53
|
+
span.set_attribute('feature.execution.duration_ms', duration)
|
|
54
|
+
span.set_attribute('feature.execution.success', true)
|
|
55
|
+
span.status = OpenTelemetry::Trace::Status.ok
|
|
56
|
+
|
|
57
|
+
result
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
duration = ((Time.now - start_time) * 1000).round(2)
|
|
60
|
+
span.set_attribute('feature.execution.duration_ms', duration)
|
|
61
|
+
span.set_attribute('feature.execution.success', false)
|
|
62
|
+
span.set_attribute('feature.execution.error', e.message)
|
|
63
|
+
span.status = OpenTelemetry::Trace::Status.error(e.message)
|
|
64
|
+
raise
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def trace_state_transition(feature_id, from_state, to_state, event_name, &block)
|
|
70
|
+
return block.call unless enabled?
|
|
71
|
+
|
|
72
|
+
@tracer.in_span('feature.state_transition', attributes: {
|
|
73
|
+
'feature.id' => feature_id,
|
|
74
|
+
'feature.transition.from' => from_state,
|
|
75
|
+
'feature.transition.to' => to_state,
|
|
76
|
+
'feature.transition.event' => event_name,
|
|
77
|
+
'service.name' => @service_name,
|
|
78
|
+
'service.version' => @service_version
|
|
79
|
+
}) do |span|
|
|
80
|
+
result = block.call
|
|
81
|
+
span.status = OpenTelemetry::Trace::Status.ok
|
|
82
|
+
result
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
protected
|
|
87
|
+
|
|
88
|
+
def send_metric(name, value, tags, _timestamp)
|
|
89
|
+
# Convert tags to OpenTelemetry attributes
|
|
90
|
+
attributes = tags.transform_keys(&:to_s)
|
|
91
|
+
attributes['service.name'] = @service_name
|
|
92
|
+
attributes['service.version'] = @service_version
|
|
93
|
+
|
|
94
|
+
case name
|
|
95
|
+
when /\.count$/
|
|
96
|
+
instrument_name = name.gsub(/\.count$/, '').gsub('.', '_')
|
|
97
|
+
counter = @instruments["#{instrument_name}_total"] ||=
|
|
98
|
+
@meter.create_counter("star_gates_#{instrument_name}_total",
|
|
99
|
+
unit: '1',
|
|
100
|
+
description: "Total count of #{instrument_name}")
|
|
101
|
+
counter.add(value, attributes: attributes)
|
|
102
|
+
|
|
103
|
+
when /\.gauge$/
|
|
104
|
+
instrument_name = name.gsub(/\.gauge$/, '').gsub('.', '_')
|
|
105
|
+
@instruments[instrument_name] ||=
|
|
106
|
+
@meter.create_observable_gauge("star_gates_#{instrument_name}",
|
|
107
|
+
unit: '1',
|
|
108
|
+
description: "Current value of #{instrument_name}")
|
|
109
|
+
# NOTE: OpenTelemetry gauges are observable, so we store the value
|
|
110
|
+
@gauge_values ||= {}
|
|
111
|
+
@gauge_values["#{instrument_name}_#{attributes.hash}"] = { value: value, attributes: attributes }
|
|
112
|
+
|
|
113
|
+
when /\.histogram$/, /\.timing$/
|
|
114
|
+
instrument_name = name.gsub(/\.(histogram|timing)$/, '').gsub('.', '_')
|
|
115
|
+
histogram = @instruments["#{instrument_name}_duration"] ||=
|
|
116
|
+
@meter.create_histogram("star_gates_#{instrument_name}_duration",
|
|
117
|
+
unit: 'ms',
|
|
118
|
+
description: "Duration histogram for #{instrument_name}")
|
|
119
|
+
histogram.record(value, attributes: attributes)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def healthy?
|
|
124
|
+
super && @tracer.respond_to?(:in_span) && @meter.respond_to?(:create_counter)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def default_tracer
|
|
130
|
+
unless defined?(OpenTelemetry)
|
|
131
|
+
raise ArgumentError, "OpenTelemetry gem not available. Install with: gem 'opentelemetry-sdk'"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
OpenTelemetry.tracer_provider.tracer(@service_name, @service_version)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def default_meter
|
|
138
|
+
unless defined?(OpenTelemetry)
|
|
139
|
+
raise ArgumentError, "OpenTelemetry gem not available. Install with: gem 'opentelemetry-sdk'"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
OpenTelemetry.meter_provider.meter(@service_name, @service_version)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def setup_instruments
|
|
146
|
+
@instruments = {}
|
|
147
|
+
@gauge_values = {}
|
|
148
|
+
|
|
149
|
+
# Pre-create common instruments
|
|
150
|
+
@instruments['feature_evaluations_total'] = @meter.create_counter(
|
|
151
|
+
'star_gates_feature_evaluations_total',
|
|
152
|
+
unit: '1',
|
|
153
|
+
description: 'Total number of feature evaluations'
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@instruments['feature_executions_total'] = @meter.create_counter(
|
|
157
|
+
'star_gates_feature_executions_total',
|
|
158
|
+
unit: '1',
|
|
159
|
+
description: 'Total number of feature executions'
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@instruments['state_transitions_total'] = @meter.create_counter(
|
|
163
|
+
'star_gates_state_transitions_total',
|
|
164
|
+
unit: '1',
|
|
165
|
+
description: 'Total number of state transitions'
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@instruments['evaluation_duration'] = @meter.create_histogram(
|
|
169
|
+
'star_gates_evaluation_duration',
|
|
170
|
+
unit: 'ms',
|
|
171
|
+
description: 'Feature evaluation duration'
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@instruments['execution_duration'] = @meter.create_histogram(
|
|
175
|
+
'star_gates_execution_duration',
|
|
176
|
+
unit: 'ms',
|
|
177
|
+
description: 'Feature execution duration'
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Setup observable gauge for system health
|
|
181
|
+
@meter.create_observable_gauge(
|
|
182
|
+
'star_gates_system_health',
|
|
183
|
+
unit: '1',
|
|
184
|
+
description: 'System health status'
|
|
185
|
+
) do |observer|
|
|
186
|
+
@gauge_values.each do |key, data|
|
|
187
|
+
next unless key.include?('system_health')
|
|
188
|
+
|
|
189
|
+
observer.observe(data[:value], attributes: data[:attributes])
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Override specific methods to use pre-created instruments
|
|
195
|
+
def record_feature_evaluation(feature_id, eligible, user_context, execution_time_ms = nil)
|
|
196
|
+
return unless enabled?
|
|
197
|
+
|
|
198
|
+
attributes = {
|
|
199
|
+
'feature.id' => feature_id.to_s,
|
|
200
|
+
'feature.eligible' => eligible.to_s,
|
|
201
|
+
'feature.user_type' => extract_user_type(user_context),
|
|
202
|
+
'service.name' => @service_name,
|
|
203
|
+
'service.version' => @service_version
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@instruments['feature_evaluations_total'].add(1, attributes: attributes)
|
|
207
|
+
|
|
208
|
+
return unless execution_time_ms
|
|
209
|
+
|
|
210
|
+
@instruments['evaluation_duration'].record(execution_time_ms, attributes: {
|
|
211
|
+
'feature.id' => feature_id.to_s,
|
|
212
|
+
'feature.eligible' => eligible.to_s,
|
|
213
|
+
'service.name' => @service_name,
|
|
214
|
+
'service.version' => @service_version
|
|
215
|
+
})
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def record_feature_execution(feature_id, state, success, execution_time_ms, _context = {})
|
|
219
|
+
return unless enabled?
|
|
220
|
+
|
|
221
|
+
attributes = {
|
|
222
|
+
'feature.id' => feature_id.to_s,
|
|
223
|
+
'feature.state' => state.to_s,
|
|
224
|
+
'feature.success' => success.to_s,
|
|
225
|
+
'service.name' => @service_name,
|
|
226
|
+
'service.version' => @service_version
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@instruments['feature_executions_total'].add(1, attributes: attributes)
|
|
230
|
+
@instruments['execution_duration'].record(execution_time_ms, attributes: {
|
|
231
|
+
'feature.id' => feature_id.to_s,
|
|
232
|
+
'feature.state' => state.to_s,
|
|
233
|
+
'service.name' => @service_name,
|
|
234
|
+
'service.version' => @service_version
|
|
235
|
+
})
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def record_state_transition(feature_id, from_state, to_state, event_name)
|
|
239
|
+
return unless enabled?
|
|
240
|
+
|
|
241
|
+
attributes = {
|
|
242
|
+
'feature.id' => feature_id.to_s,
|
|
243
|
+
'feature.transition.from' => from_state.to_s,
|
|
244
|
+
'feature.transition.to' => to_state.to_s,
|
|
245
|
+
'feature.transition.event' => event_name.to_s,
|
|
246
|
+
'service.name' => @service_name,
|
|
247
|
+
'service.version' => @service_version
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@instruments['state_transitions_total'].add(1, attributes: attributes)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|