liteguard 0.4.20260317 → 0.6.20260405
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/README.md +13 -0
- data/lib/liteguard/client.rb +103 -60
- data/lib/liteguard/decision.rb +34 -0
- data/lib/liteguard/evaluation.rb +15 -0
- data/lib/liteguard/scope.rb +24 -0
- data/lib/liteguard/types.rb +14 -2
- data/lib/liteguard.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64fb509c369cbd43217407216706597424220bd7143e03d460b3ad4428b4ff7a
|
|
4
|
+
data.tar.gz: b53c0c549c66353d3cb6f9aea811e5dffeb9e3b53f74f49a3cb315a17aa513e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3488d12c50f5c9d3a6e48029ed450e2062f71efd24d06ebf217a88ee4742e0e3317c0731e4a2b13e38eebb33291bdc3b7745e8d5607451734c05a3378d58f383
|
|
7
|
+
data.tar.gz: e5a64bcf4c70b0692a88841666e1eb1358859073ac2d1d51fcae2a672ecf9e5dd6895b1524e1827e5a61995b6fbb4186621cf451ada5bf8cdd4410193ed7979e
|
data/README.md
CHANGED
|
@@ -47,16 +47,29 @@ client.shutdown
|
|
|
47
47
|
- `client.start` fetches the initial bundle and starts refresh and flush workers.
|
|
48
48
|
- `client.create_scope(**properties)` creates an immutable scope.
|
|
49
49
|
- `scope.open?(name, **options)` evaluates locally.
|
|
50
|
+
- `scope.evaluate(name, **options)` returns a `GuardDecision` with the full evaluation result.
|
|
50
51
|
- `scope.execute_if_open(name, **options) { ... }` measures guarded work.
|
|
52
|
+
- `scope.start_execution` creates an execution correlation handle.
|
|
51
53
|
- `scope.bind_protected_context(**protected_context)` derives a protected scope.
|
|
54
|
+
- `scope.properties` returns the current property bag for transport.
|
|
52
55
|
- `client.flush` flushes buffered telemetry.
|
|
53
56
|
- `client.shutdown` flushes and stops background work.
|
|
54
57
|
|
|
58
|
+
## Convenience helpers
|
|
59
|
+
|
|
60
|
+
For server applications that want request-scoped context propagation:
|
|
61
|
+
|
|
62
|
+
- `client.with_scope(scope) { ... }` runs a block with `scope` as the active scope, isolated via `Thread.current`.
|
|
63
|
+
- `client.active_scope` returns the scope bound to the current thread.
|
|
64
|
+
- `client.with_properties(properties) { ... }` derives and activates a scope with additional properties.
|
|
65
|
+
- `client.with_protected_context(protected_context) { ... }` derives and activates a protected scope.
|
|
66
|
+
|
|
55
67
|
## Notes
|
|
56
68
|
|
|
57
69
|
- Evaluation is local after the initial bundle fetch.
|
|
58
70
|
- Prefer explicit client and scope usage.
|
|
59
71
|
- Unadopted guards default open and emit no signals.
|
|
72
|
+
- The convenience helpers above also serve as infrastructure for auto-instrumentation of third-party dependencies. See the [Liteguard CLI](https://github.com/liteguard/liteguard/tree/main/cli) for build-time instrumentation.
|
|
60
73
|
|
|
61
74
|
## Development
|
|
62
75
|
|
data/lib/liteguard/client.rb
CHANGED
|
@@ -10,8 +10,8 @@ module Liteguard
|
|
|
10
10
|
# This class is the primary Ruby SDK entrypoint.
|
|
11
11
|
class Client
|
|
12
12
|
DEFAULT_BACKEND_URL = "https://api.liteguard.io"
|
|
13
|
-
DEFAULT_REFRESH_RATE =
|
|
14
|
-
DEFAULT_FLUSH_RATE =
|
|
13
|
+
DEFAULT_REFRESH_RATE = 60
|
|
14
|
+
DEFAULT_FLUSH_RATE = 60
|
|
15
15
|
DEFAULT_FLUSH_SIZE = 500
|
|
16
16
|
DEFAULT_HTTP_TIMEOUT = 4
|
|
17
17
|
DEFAULT_FLUSH_BUFFER_MULTIPLIER = 4
|
|
@@ -138,6 +138,25 @@ module Liteguard
|
|
|
138
138
|
end
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
# Start a new execution correlation context and return a handle.
|
|
142
|
+
#
|
|
143
|
+
# Returns a lightweight {Execution} handle that groups telemetry signals
|
|
144
|
+
# under a shared execution ID. Call {Execution#end_execution} when done.
|
|
145
|
+
# For block-based usage, prefer {#with_execution} instead.
|
|
146
|
+
#
|
|
147
|
+
# @return [Execution] a correlation handle
|
|
148
|
+
def start_execution
|
|
149
|
+
exec_id = next_signal_id
|
|
150
|
+
previous = Thread.current[:liteguard_execution_state]
|
|
151
|
+
Thread.current[:liteguard_execution_state] = {
|
|
152
|
+
execution_id: exec_id,
|
|
153
|
+
sequence_number: 0,
|
|
154
|
+
last_signal_id: nil,
|
|
155
|
+
}
|
|
156
|
+
cleanup = -> { Thread.current[:liteguard_execution_state] = previous }
|
|
157
|
+
Execution.new(exec_id, cleanup: cleanup)
|
|
158
|
+
end
|
|
159
|
+
|
|
141
160
|
# ---------------------------------------------------------------------
|
|
142
161
|
# Scope API
|
|
143
162
|
# ---------------------------------------------------------------------
|
|
@@ -152,6 +171,13 @@ module Liteguard
|
|
|
152
171
|
|
|
153
172
|
# Return the active scope for the current thread.
|
|
154
173
|
#
|
|
174
|
+
# This method serves two roles:
|
|
175
|
+
# 1. Developer convenience for retrieving the current request scope.
|
|
176
|
+
# 2. Required infrastructure for auto-instrumentation. Instrumented
|
|
177
|
+
# third-party code calls this method to discover the evaluation
|
|
178
|
+
# context established by application-level middleware via
|
|
179
|
+
# {#with_scope}.
|
|
180
|
+
#
|
|
155
181
|
# @return [Scope] the active scope, or the default scope when none is bound
|
|
156
182
|
def active_scope
|
|
157
183
|
scope = Thread.current[@active_scope_key]
|
|
@@ -162,6 +188,13 @@ module Liteguard
|
|
|
162
188
|
|
|
163
189
|
# Bind a scope for the duration of a block.
|
|
164
190
|
#
|
|
191
|
+
# This method serves two roles:
|
|
192
|
+
# 1. Developer convenience for scoping guard evaluation to a request.
|
|
193
|
+
# 2. Required infrastructure for auto-instrumentation. When application
|
|
194
|
+
# middleware activates a scope with this method, instrumented
|
|
195
|
+
# third-party code running inside the block can discover the scope
|
|
196
|
+
# via {#active_scope}.
|
|
197
|
+
#
|
|
165
198
|
# @param scope [Scope, nil] scope to bind, or `nil` to reuse the current
|
|
166
199
|
# scope
|
|
167
200
|
# @yield Runs with the resolved scope bound as active
|
|
@@ -209,64 +242,6 @@ module Liteguard
|
|
|
209
242
|
with_scope(active_scope.bind_protected_context(protected_context)) { yield }
|
|
210
243
|
end
|
|
211
244
|
|
|
212
|
-
# Replace the active scope with one that includes merged properties.
|
|
213
|
-
#
|
|
214
|
-
# @param properties [Hash] properties to merge into the active scope
|
|
215
|
-
# @return [Scope] the derived active scope
|
|
216
|
-
def add_properties(properties)
|
|
217
|
-
replace_current_scope(active_scope.with_properties(properties))
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Replace the active scope with one that omits the named properties.
|
|
221
|
-
#
|
|
222
|
-
# @param names [Array<String, Symbol>] property names to remove
|
|
223
|
-
# @return [Scope] the derived active scope
|
|
224
|
-
def clear_properties(names)
|
|
225
|
-
replace_current_scope(active_scope.clear_properties(Array(names).map(&:to_s)))
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Replace the active scope with an empty property scope.
|
|
229
|
-
#
|
|
230
|
-
# @return [Scope] the derived active scope
|
|
231
|
-
def reset_properties
|
|
232
|
-
replace_current_scope(active_scope.reset_properties)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Replace the active scope with a protected-context-derived scope.
|
|
236
|
-
#
|
|
237
|
-
# @param protected_context [ProtectedContext, Hash] signed protected context
|
|
238
|
-
# @return [Scope] the derived active scope
|
|
239
|
-
def bind_protected_context(protected_context)
|
|
240
|
-
replace_current_scope(active_scope.bind_protected_context(protected_context))
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# Replace the active scope with one using the public bundle.
|
|
244
|
-
#
|
|
245
|
-
# @return [Scope] the derived active scope
|
|
246
|
-
def clear_protected_context
|
|
247
|
-
replace_current_scope(active_scope.clear_protected_context)
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# Return the current thread's evaluation properties.
|
|
251
|
-
#
|
|
252
|
-
# @return [Hash] active scope properties
|
|
253
|
-
def context
|
|
254
|
-
active_scope.properties
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Replace the current thread's active scope.
|
|
258
|
-
#
|
|
259
|
-
# Mutation helpers are intentionally thread-local so request-scoped data
|
|
260
|
-
# cannot leak through the process-wide default scope.
|
|
261
|
-
#
|
|
262
|
-
# @param scope [Scope] scope to install
|
|
263
|
-
# @return [Scope] the resolved scope
|
|
264
|
-
def replace_current_scope(scope)
|
|
265
|
-
resolved = resolve_scope(scope)
|
|
266
|
-
Thread.current[@active_scope_key] = resolved
|
|
267
|
-
resolved
|
|
268
|
-
end
|
|
269
|
-
|
|
270
245
|
# Resolve a scope argument and verify that it belongs to this client.
|
|
271
246
|
#
|
|
272
247
|
# @param scope [Scope, nil] candidate scope
|
|
@@ -347,6 +322,74 @@ module Liteguard
|
|
|
347
322
|
evaluate_guard_in_scope(scope, name.to_s, options, emit_signal: false)[:result]
|
|
348
323
|
end
|
|
349
324
|
|
|
325
|
+
# Evaluate a guard and return a {GuardDecision} with full reasoning.
|
|
326
|
+
#
|
|
327
|
+
# @param name [String] guard name to evaluate
|
|
328
|
+
# @param options [Hash, nil] optional per-call overrides
|
|
329
|
+
# @return [GuardDecision] the evaluation decision
|
|
330
|
+
def evaluate(name, options = nil, **legacy_options)
|
|
331
|
+
evaluate_in_scope(active_scope, name, options, **legacy_options)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Evaluate a guard in the provided scope and return a {GuardDecision}.
|
|
335
|
+
#
|
|
336
|
+
# @param scope [Scope] scope to evaluate against
|
|
337
|
+
# @param name [String] guard name to evaluate
|
|
338
|
+
# @param options [Hash, nil] optional per-call overrides
|
|
339
|
+
# @return [GuardDecision] the evaluation decision
|
|
340
|
+
def evaluate_in_scope(scope, name, options = nil, **legacy_options)
|
|
341
|
+
options = normalize_is_open_options(options, legacy_options)
|
|
342
|
+
resolved_scope = resolve_scope(scope)
|
|
343
|
+
bundle = bundle_for_scope(resolved_scope)
|
|
344
|
+
effective_fallback = options[:fallback].nil? ? @fallback : options[:fallback]
|
|
345
|
+
|
|
346
|
+
unless bundle.ready
|
|
347
|
+
return GuardDecision.new(
|
|
348
|
+
name: name.to_s, is_open: effective_fallback, adopted: false,
|
|
349
|
+
reason: "fallback", matched_rule_index: nil, properties: {}
|
|
350
|
+
)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
guard = bundle.guards[name.to_s]
|
|
354
|
+
if guard.nil?
|
|
355
|
+
record_unadopted_guard(name.to_s)
|
|
356
|
+
return GuardDecision.new(
|
|
357
|
+
name: name.to_s, is_open: true, adopted: false,
|
|
358
|
+
reason: "unadopted", matched_rule_index: nil, properties: {}
|
|
359
|
+
)
|
|
360
|
+
end
|
|
361
|
+
unless guard.adopted
|
|
362
|
+
record_unadopted_guard(name.to_s)
|
|
363
|
+
return GuardDecision.new(
|
|
364
|
+
name: name.to_s, is_open: true, adopted: false,
|
|
365
|
+
reason: "unadopted", matched_rule_index: nil, properties: {}
|
|
366
|
+
)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
props = resolved_scope.properties
|
|
370
|
+
props = props.merge(options[:properties]) if options[:properties]
|
|
371
|
+
|
|
372
|
+
detailed = Evaluation.evaluate_guard_detailed(guard, props)
|
|
373
|
+
result = detailed.result
|
|
374
|
+
if result && guard.rate_limit_per_minute.to_i > 0
|
|
375
|
+
result = check_rate_limit(name.to_s, guard.rate_limit_per_minute, guard.rate_limit_properties, props)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
reason = detailed.matched_rule_index >= 0 ? "matched_rule" : "default_value"
|
|
379
|
+
matched_idx = detailed.matched_rule_index >= 0 ? detailed.matched_rule_index : nil
|
|
380
|
+
|
|
381
|
+
buffer_signal(
|
|
382
|
+
name.to_s, result, props,
|
|
383
|
+
kind: "guard_check",
|
|
384
|
+
measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
GuardDecision.new(
|
|
388
|
+
name: name.to_s, is_open: result, adopted: guard.adopted,
|
|
389
|
+
reason: reason, matched_rule_index: matched_idx, properties: props.dup
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
|
|
350
393
|
# Evaluate a guard and execute the block only when it resolves open.
|
|
351
394
|
#
|
|
352
395
|
# @param name [String] guard name to evaluate
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liteguard
|
|
4
|
+
# Internal result from {Evaluation.evaluate_guard_detailed}.
|
|
5
|
+
DetailedEvalResult = Data.define(:result, :matched_rule_index)
|
|
6
|
+
|
|
7
|
+
# A lightweight execution correlation handle.
|
|
8
|
+
#
|
|
9
|
+
# Groups telemetry signals under a shared execution ID. Created by
|
|
10
|
+
# {Scope#start_execution} or {Client#start_execution}.
|
|
11
|
+
# Call {#end_execution} when the logical execution boundary is complete.
|
|
12
|
+
class Execution
|
|
13
|
+
# @return [String] the unique identifier for this execution
|
|
14
|
+
attr_reader :execution_id
|
|
15
|
+
|
|
16
|
+
# @api private
|
|
17
|
+
def initialize(execution_id, cleanup: nil)
|
|
18
|
+
@execution_id = execution_id
|
|
19
|
+
@cleanup = cleanup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# End this execution.
|
|
23
|
+
#
|
|
24
|
+
# After calling +end_execution+, subsequent guard checks will no longer be
|
|
25
|
+
# correlated with this execution.
|
|
26
|
+
# @return [void]
|
|
27
|
+
def end_execution
|
|
28
|
+
if @cleanup
|
|
29
|
+
@cleanup.call
|
|
30
|
+
@cleanup = nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/liteguard/evaluation.rb
CHANGED
|
@@ -18,6 +18,21 @@ module Liteguard
|
|
|
18
18
|
guard.default_value
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Evaluate a guard and return both the result and the matched rule index.
|
|
22
|
+
#
|
|
23
|
+
# @param guard [Guard] guard definition to evaluate
|
|
24
|
+
# @param properties [Hash{String => Object}] normalized evaluation properties
|
|
25
|
+
# @return [DetailedEvalResult] result with the matched rule index (-1 when default)
|
|
26
|
+
def self.evaluate_guard_detailed(guard, properties)
|
|
27
|
+
guard.rules.each_with_index do |rule, i|
|
|
28
|
+
next unless rule.enabled
|
|
29
|
+
if matches_rule?(rule, properties)
|
|
30
|
+
return DetailedEvalResult.new(result: rule.result, matched_rule_index: i)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
DetailedEvalResult.new(result: guard.default_value, matched_rule_index: -1)
|
|
34
|
+
end
|
|
35
|
+
|
|
21
36
|
# Determine whether a single rule matches the provided properties.
|
|
22
37
|
#
|
|
23
38
|
# @param rule [Rule] rule to evaluate
|
data/lib/liteguard/scope.rb
CHANGED
|
@@ -105,6 +105,30 @@ module Liteguard
|
|
|
105
105
|
@client.execute_if_open_in_scope(self, name, options, **legacy_options, &block)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
# Evaluate a guard and return a {GuardDecision} with full reasoning.
|
|
109
|
+
#
|
|
110
|
+
# Like {#is_open} but returns the full decision context including the
|
|
111
|
+
# reason, matched rule index, and the merged properties used. A
|
|
112
|
+
# +guard_check+ telemetry signal is buffered.
|
|
113
|
+
#
|
|
114
|
+
# @param name [String] guard name to evaluate
|
|
115
|
+
# @param options [Hash, nil] optional per-call overrides
|
|
116
|
+
# @return [GuardDecision] the evaluation decision
|
|
117
|
+
def evaluate(name, options = nil, **legacy_options)
|
|
118
|
+
@client.evaluate_in_scope(self, name, options, **legacy_options)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Start a new execution correlation context.
|
|
122
|
+
#
|
|
123
|
+
# Returns a lightweight {Execution} handle that groups telemetry signals
|
|
124
|
+
# under a shared execution ID. Call {Execution#end_execution} when the
|
|
125
|
+
# logical execution boundary is complete.
|
|
126
|
+
#
|
|
127
|
+
# @return [Execution] a correlation handle
|
|
128
|
+
def start_execution
|
|
129
|
+
@client.start_execution
|
|
130
|
+
end
|
|
131
|
+
|
|
108
132
|
# Bind this scope as the active scope for the duration of a block.
|
|
109
133
|
#
|
|
110
134
|
# @yield Runs with this scope bound as active
|
data/lib/liteguard/types.rb
CHANGED
|
@@ -6,6 +6,8 @@ module Liteguard
|
|
|
6
6
|
|
|
7
7
|
OPERATORS = %i[equals not_equals in not_in regex gt gte lt lte].freeze
|
|
8
8
|
|
|
9
|
+
GUARD_DECISION_REASONS = %i[matched_rule default_value unadopted fallback].freeze
|
|
10
|
+
|
|
9
11
|
# A single rule condition within a Guard.
|
|
10
12
|
Rule = Data.define(
|
|
11
13
|
:property_name,
|
|
@@ -26,6 +28,16 @@ module Liteguard
|
|
|
26
28
|
:disable_measurement
|
|
27
29
|
)
|
|
28
30
|
|
|
31
|
+
# The detailed result of evaluating a guard.
|
|
32
|
+
GuardDecision = Data.define(
|
|
33
|
+
:name,
|
|
34
|
+
:is_open,
|
|
35
|
+
:adopted,
|
|
36
|
+
:reason,
|
|
37
|
+
:matched_rule_index,
|
|
38
|
+
:properties
|
|
39
|
+
)
|
|
40
|
+
|
|
29
41
|
# Keys accepted by the options hash passed to {Liteguard::Client#is_open}.
|
|
30
42
|
CHECK_OPTION_KEYS = %i[properties fallback disable_measurement].freeze
|
|
31
43
|
|
|
@@ -128,8 +140,8 @@ module Liteguard
|
|
|
128
140
|
project_client_token: nil,
|
|
129
141
|
environment: nil,
|
|
130
142
|
fallback: false,
|
|
131
|
-
refresh_rate_seconds:
|
|
132
|
-
flush_rate_seconds:
|
|
143
|
+
refresh_rate_seconds: 60,
|
|
144
|
+
flush_rate_seconds: 60,
|
|
133
145
|
flush_size: 500,
|
|
134
146
|
backend_url: "https://api.liteguard.io",
|
|
135
147
|
quiet: true,
|
data/lib/liteguard.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: liteguard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.20260405
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Liteguard
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-04-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- README.md
|
|
51
51
|
- lib/liteguard.rb
|
|
52
52
|
- lib/liteguard/client.rb
|
|
53
|
+
- lib/liteguard/decision.rb
|
|
53
54
|
- lib/liteguard/evaluation.rb
|
|
54
55
|
- lib/liteguard/scope.rb
|
|
55
56
|
- lib/liteguard/types.rb
|