liteguard 0.6.20260405 → 0.7.20260603
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 +1 -0
- data/lib/liteguard/client.rb +202 -39
- data/lib/liteguard/scope.rb +6 -2
- data/lib/liteguard/types.rb +36 -4
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 674b74c3afa87054d07083210c6f894afad8bc53cb2b6ae8a4e8bd045b30c14c
|
|
4
|
+
data.tar.gz: 6cf6d9d1fceb2760404e65a0b26faa05ff3687318492d4e124f35eced843e9f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51eec265fceb91ea7994cf97d64878afb7312ce5bf72abe7732d584da009a8b7ce82efa163144110054254d01ad935cbe22a77ba3ca9f8a01ca5963f5d28d0ef
|
|
7
|
+
data.tar.gz: d998303a8c1bfa5156aabee64afe9d8b8e174c7da199004491b5cf5e94376a157644d459605fca2c2bbde7d87f9e1381993265f8d2fe0c636057acb60e4d4dc3
|
data/README.md
CHANGED
|
@@ -67,6 +67,7 @@ For server applications that want request-scoped context propagation:
|
|
|
67
67
|
## Notes
|
|
68
68
|
|
|
69
69
|
- Evaluation is local after the initial bundle fetch.
|
|
70
|
+
- Protected contexts use `properties`, `signature`, and optional `issued_at` / `expires_at` fields.
|
|
70
71
|
- Prefer explicit client and scope usage.
|
|
71
72
|
- Unadopted guards default open and emit no signals.
|
|
72
73
|
- 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.
|
data/lib/liteguard/client.rb
CHANGED
|
@@ -370,10 +370,8 @@ module Liteguard
|
|
|
370
370
|
props = props.merge(options[:properties]) if options[:properties]
|
|
371
371
|
|
|
372
372
|
detailed = Evaluation.evaluate_guard_detailed(guard, props)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
result = check_rate_limit(name.to_s, guard.rate_limit_per_minute, guard.rate_limit_properties, props)
|
|
376
|
-
end
|
|
373
|
+
applied_rate_limit = apply_rate_limit(guard, name.to_s, detailed.result, props, emit_signal: true)
|
|
374
|
+
result = applied_rate_limit[:result]
|
|
377
375
|
|
|
378
376
|
reason = detailed.matched_rule_index >= 0 ? "matched_rule" : "default_value"
|
|
379
377
|
matched_idx = detailed.matched_rule_index >= 0 ? detailed.matched_rule_index : nil
|
|
@@ -381,7 +379,8 @@ module Liteguard
|
|
|
381
379
|
buffer_signal(
|
|
382
380
|
name.to_s, result, props,
|
|
383
381
|
kind: "guard_check",
|
|
384
|
-
measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil
|
|
382
|
+
measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil,
|
|
383
|
+
rate_limit_decisions: applied_rate_limit[:rate_limit_decisions]
|
|
385
384
|
)
|
|
386
385
|
|
|
387
386
|
GuardDecision.new(
|
|
@@ -475,14 +474,8 @@ module Liteguard
|
|
|
475
474
|
props = props.merge(options[:properties])
|
|
476
475
|
end
|
|
477
476
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
result = if emit_signal
|
|
481
|
-
check_rate_limit(name, guard.rate_limit_per_minute, guard.rate_limit_properties, props)
|
|
482
|
-
else
|
|
483
|
-
would_pass_rate_limit(name, guard.rate_limit_per_minute, guard.rate_limit_properties, props)
|
|
484
|
-
end
|
|
485
|
-
end
|
|
477
|
+
applied_rate_limit = apply_rate_limit(guard, name, Evaluation.evaluate_guard(guard, props), props, emit_signal: emit_signal)
|
|
478
|
+
result = applied_rate_limit[:result]
|
|
486
479
|
|
|
487
480
|
signal = nil
|
|
488
481
|
if emit_signal
|
|
@@ -491,7 +484,8 @@ module Liteguard
|
|
|
491
484
|
result,
|
|
492
485
|
props,
|
|
493
486
|
kind: "guard_check",
|
|
494
|
-
measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil
|
|
487
|
+
measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil,
|
|
488
|
+
rate_limit_decisions: applied_rate_limit[:rate_limit_decisions]
|
|
495
489
|
)
|
|
496
490
|
end
|
|
497
491
|
|
|
@@ -533,6 +527,12 @@ module Liteguard
|
|
|
533
527
|
projectClientToken: @project_client_token,
|
|
534
528
|
environment: @environment,
|
|
535
529
|
signals: batch.map do |signal|
|
|
530
|
+
rate_limit_decisions_payload = if signal.rate_limit_decisions&.any?
|
|
531
|
+
{ rateLimitDecisions: signal.rate_limit_decisions.map { |decision| rate_limit_decision_payload(decision) } }
|
|
532
|
+
else
|
|
533
|
+
{}
|
|
534
|
+
end
|
|
535
|
+
|
|
536
536
|
{
|
|
537
537
|
guardName: signal.guard_name,
|
|
538
538
|
result: signal.result,
|
|
@@ -545,7 +545,8 @@ module Liteguard
|
|
|
545
545
|
kind: signal.kind,
|
|
546
546
|
droppedSignalsSinceLast: signal.dropped_signals_since_last,
|
|
547
547
|
**(signal.parent_signal_id ? { parentSignalId: signal.parent_signal_id } : {}),
|
|
548
|
-
**(signal.measurement ? { measurement: signal_measurement_payload(signal.measurement) } : {})
|
|
548
|
+
**(signal.measurement ? { measurement: signal_measurement_payload(signal.measurement) } : {}),
|
|
549
|
+
**rate_limit_decisions_payload
|
|
549
550
|
}
|
|
550
551
|
end
|
|
551
552
|
)
|
|
@@ -598,7 +599,7 @@ module Liteguard
|
|
|
598
599
|
# @param measurement [SignalPerformance, nil] optional measurement payload
|
|
599
600
|
# @param parent_signal_id_override [String, nil] explicit parent signal ID
|
|
600
601
|
# @return [Signal] buffered signal instance
|
|
601
|
-
def buffer_signal(guard_name, result, props, kind:, measurement: nil, parent_signal_id_override: nil)
|
|
602
|
+
def buffer_signal(guard_name, result, props, kind:, measurement: nil, parent_signal_id_override: nil, rate_limit_decisions: nil)
|
|
602
603
|
metadata = next_signal_metadata(parent_signal_id_override)
|
|
603
604
|
signal = Signal.new(
|
|
604
605
|
guard_name: guard_name,
|
|
@@ -613,7 +614,8 @@ module Liteguard
|
|
|
613
614
|
callsite_id: capture_callsite_id,
|
|
614
615
|
kind: kind,
|
|
615
616
|
dropped_signals_since_last: take_dropped_signals,
|
|
616
|
-
measurement: measurement
|
|
617
|
+
measurement: measurement,
|
|
618
|
+
rate_limit_decisions: Array(rate_limit_decisions).dup
|
|
617
619
|
)
|
|
618
620
|
should_flush = false
|
|
619
621
|
@monitor.synchronize do
|
|
@@ -772,10 +774,13 @@ module Liteguard
|
|
|
772
774
|
return PUBLIC_BUNDLE_KEY if protected_context.nil?
|
|
773
775
|
|
|
774
776
|
keys = protected_context.properties.keys.sort
|
|
775
|
-
parts = [protected_context.signature, ""]
|
|
777
|
+
parts = [protected_context.signature, "properties"]
|
|
776
778
|
keys.each do |key|
|
|
777
779
|
parts << "#{key}=#{protected_context.properties[key]}"
|
|
778
780
|
end
|
|
781
|
+
parts << ""
|
|
782
|
+
parts << "issuedAt=#{protected_context.issued_at || ''}"
|
|
783
|
+
parts << "expiresAt=#{protected_context.expires_at || ''}"
|
|
779
784
|
parts.join("\x00")
|
|
780
785
|
end
|
|
781
786
|
|
|
@@ -806,6 +811,8 @@ module Liteguard
|
|
|
806
811
|
properties: protected_context.properties,
|
|
807
812
|
signature: protected_context.signature
|
|
808
813
|
}
|
|
814
|
+
payload[:protectedContext][:issuedAt] = protected_context.issued_at unless protected_context.issued_at.nil?
|
|
815
|
+
payload[:protectedContext][:expiresAt] = protected_context.expires_at unless protected_context.expires_at.nil?
|
|
809
816
|
end
|
|
810
817
|
|
|
811
818
|
uri = URI("#{@backend_url}/api/v1/guards")
|
|
@@ -942,12 +949,31 @@ module Liteguard
|
|
|
942
949
|
rules: (raw["rules"] || []).map { |rule| parse_rule(rule) },
|
|
943
950
|
default_value: !!raw["defaultValue"],
|
|
944
951
|
adopted: !!raw["adopted"],
|
|
945
|
-
|
|
946
|
-
|
|
952
|
+
rate_limit: parse_rate_limit(raw["rateLimit"]),
|
|
953
|
+
dry_run_rate_limit: parse_dry_run_rate_limit(raw["dryRunRateLimit"]),
|
|
947
954
|
disable_measurement: raw.key?("disableMeasurement") ? raw["disableMeasurement"] : nil
|
|
948
955
|
)
|
|
949
956
|
end
|
|
950
957
|
|
|
958
|
+
def parse_rate_limit(raw)
|
|
959
|
+
return nil unless raw.is_a?(Hash)
|
|
960
|
+
|
|
961
|
+
GuardRateLimitConfig.new(
|
|
962
|
+
requests_per_minute: (raw["requestsPerMinute"] || 0).to_i,
|
|
963
|
+
partition_properties: Array(raw["partitionProperties"])
|
|
964
|
+
)
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
def parse_dry_run_rate_limit(raw)
|
|
968
|
+
return nil unless raw.is_a?(Hash)
|
|
969
|
+
|
|
970
|
+
GuardDryRunRateLimit.new(
|
|
971
|
+
dry_run_id: raw["dryRunId"].to_s,
|
|
972
|
+
requests_per_minute: (raw["requestsPerMinute"] || 0).to_i,
|
|
973
|
+
partition_properties: Array(raw["partitionProperties"])
|
|
974
|
+
)
|
|
975
|
+
end
|
|
976
|
+
|
|
951
977
|
# Parse a rule payload returned by the backend.
|
|
952
978
|
#
|
|
953
979
|
# @param raw [Hash] decoded rule payload
|
|
@@ -970,19 +996,118 @@ module Liteguard
|
|
|
970
996
|
# the bucket key
|
|
971
997
|
# @param props [Hash] evaluation properties
|
|
972
998
|
# @return [Boolean] `true` when the evaluation is within the limit
|
|
973
|
-
def
|
|
999
|
+
def evaluate_rate_limit(slot_key, limit_per_minute, rate_limit_properties, props, consume:)
|
|
974
1000
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
975
|
-
key = rate_limit_bucket_key(
|
|
1001
|
+
key = rate_limit_bucket_key(slot_key, rate_limit_properties, props)
|
|
976
1002
|
@monitor.synchronize do
|
|
977
1003
|
entry = @rate_limit_state[key] || { window_start: now, count: 0 }
|
|
978
1004
|
entry = { window_start: now, count: 0 } if now - entry[:window_start] >= 60.0
|
|
979
|
-
|
|
1005
|
+
count_in_window = entry[:count] + 1
|
|
1006
|
+
allowed = entry[:count] < limit_per_minute
|
|
1007
|
+
if consume
|
|
1008
|
+
entry = { window_start: entry[:window_start], count: count_in_window } if allowed
|
|
980
1009
|
@rate_limit_state[key] = entry
|
|
981
|
-
return false
|
|
982
1010
|
end
|
|
983
|
-
|
|
1011
|
+
{ allowed: allowed, count_in_window: count_in_window }
|
|
984
1012
|
end
|
|
985
|
-
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def consume_rate_limit(name, evaluation_target, dry_run_id, limit_per_minute, rate_limit_properties, props)
|
|
1016
|
+
evaluate_rate_limit(
|
|
1017
|
+
rate_limit_slot_key(name, evaluation_target, dry_run_id),
|
|
1018
|
+
limit_per_minute,
|
|
1019
|
+
rate_limit_properties,
|
|
1020
|
+
props,
|
|
1021
|
+
consume: true
|
|
1022
|
+
)
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
def peek_rate_limit(name, evaluation_target, dry_run_id, limit_per_minute, rate_limit_properties, props)
|
|
1026
|
+
evaluate_rate_limit(
|
|
1027
|
+
rate_limit_slot_key(name, evaluation_target, dry_run_id),
|
|
1028
|
+
limit_per_minute,
|
|
1029
|
+
rate_limit_properties,
|
|
1030
|
+
props,
|
|
1031
|
+
consume: false
|
|
1032
|
+
)
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def rate_limit_slot_key(name, evaluation_target, dry_run_id)
|
|
1036
|
+
return "#{name}\x00active" if evaluation_target == :active
|
|
1037
|
+
|
|
1038
|
+
"#{name}\x00dry_run=#{dry_run_id}"
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
def partition_values(rate_limit_properties, props)
|
|
1042
|
+
Array(rate_limit_properties).each_with_object({}) do |property, values|
|
|
1043
|
+
values[property] = props[property] if props.key?(property)
|
|
1044
|
+
end
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
def build_rate_limit_decision(evaluation_target, dry_run_id, outcome, requests_per_minute, rate_limit_properties, props, count_in_window)
|
|
1048
|
+
RateLimitDecision.new(
|
|
1049
|
+
evaluation_target: evaluation_target,
|
|
1050
|
+
dry_run_id: dry_run_id,
|
|
1051
|
+
outcome: outcome,
|
|
1052
|
+
requests_per_minute: requests_per_minute,
|
|
1053
|
+
partition_properties: Array(rate_limit_properties).dup,
|
|
1054
|
+
partition_values: partition_values(rate_limit_properties, props),
|
|
1055
|
+
count_in_window: count_in_window
|
|
1056
|
+
)
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
def apply_rate_limit(guard, name, initial_result, props, emit_signal:)
|
|
1060
|
+
rate_limit_decisions = []
|
|
1061
|
+
return { result: initial_result, rate_limit_decisions: rate_limit_decisions } unless initial_result
|
|
1062
|
+
|
|
1063
|
+
result = initial_result
|
|
1064
|
+
rate_limit = guard.rate_limit
|
|
1065
|
+
if rate_limit && rate_limit.requests_per_minute.to_i > 0
|
|
1066
|
+
evaluation = if emit_signal
|
|
1067
|
+
consume_rate_limit(name, :active, nil, rate_limit.requests_per_minute, rate_limit.partition_properties, props)
|
|
1068
|
+
else
|
|
1069
|
+
peek_rate_limit(name, :active, nil, rate_limit.requests_per_minute, rate_limit.partition_properties, props)
|
|
1070
|
+
end
|
|
1071
|
+
result = evaluation[:allowed]
|
|
1072
|
+
if emit_signal
|
|
1073
|
+
rate_limit_decisions << build_rate_limit_decision(
|
|
1074
|
+
:active,
|
|
1075
|
+
nil,
|
|
1076
|
+
evaluation[:allowed] ? :within_limit : :limited,
|
|
1077
|
+
rate_limit.requests_per_minute,
|
|
1078
|
+
rate_limit.partition_properties,
|
|
1079
|
+
props,
|
|
1080
|
+
evaluation[:count_in_window]
|
|
1081
|
+
)
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
dry_run_rate_limit = guard.dry_run_rate_limit
|
|
1086
|
+
if emit_signal && dry_run_rate_limit && dry_run_rate_limit.requests_per_minute.to_i > 0
|
|
1087
|
+
evaluation = consume_rate_limit(
|
|
1088
|
+
name,
|
|
1089
|
+
:dry_run,
|
|
1090
|
+
dry_run_rate_limit.dry_run_id,
|
|
1091
|
+
dry_run_rate_limit.requests_per_minute,
|
|
1092
|
+
dry_run_rate_limit.partition_properties,
|
|
1093
|
+
props
|
|
1094
|
+
)
|
|
1095
|
+
rate_limit_decisions << build_rate_limit_decision(
|
|
1096
|
+
:dry_run,
|
|
1097
|
+
dry_run_rate_limit.dry_run_id,
|
|
1098
|
+
evaluation[:allowed] ? :within_limit : :would_limit,
|
|
1099
|
+
dry_run_rate_limit.requests_per_minute,
|
|
1100
|
+
dry_run_rate_limit.partition_properties,
|
|
1101
|
+
props,
|
|
1102
|
+
evaluation[:count_in_window]
|
|
1103
|
+
)
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
{ result: result, rate_limit_decisions: rate_limit_decisions }
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
def check_rate_limit(name, limit_per_minute, rate_limit_properties, props)
|
|
1110
|
+
consume_rate_limit(name, :active, nil, limit_per_minute, rate_limit_properties, props)[:allowed]
|
|
986
1111
|
end
|
|
987
1112
|
|
|
988
1113
|
# Check whether an evaluation would pass rate limiting without consuming a
|
|
@@ -995,15 +1120,7 @@ module Liteguard
|
|
|
995
1120
|
# @param props [Hash] evaluation properties
|
|
996
1121
|
# @return [Boolean] `true` when the evaluation would pass the limit
|
|
997
1122
|
def would_pass_rate_limit(name, limit_per_minute, rate_limit_properties, props)
|
|
998
|
-
|
|
999
|
-
key = rate_limit_bucket_key(name, rate_limit_properties, props)
|
|
1000
|
-
@monitor.synchronize do
|
|
1001
|
-
entry = @rate_limit_state[key]
|
|
1002
|
-
return true if entry.nil?
|
|
1003
|
-
return true if now - entry[:window_start] >= 60.0
|
|
1004
|
-
|
|
1005
|
-
entry[:count] < limit_per_minute
|
|
1006
|
-
end
|
|
1123
|
+
peek_rate_limit(name, :active, nil, limit_per_minute, rate_limit_properties, props)[:allowed]
|
|
1007
1124
|
end
|
|
1008
1125
|
|
|
1009
1126
|
# Build the cache key used for rate-limit buckets.
|
|
@@ -1066,19 +1183,51 @@ module Liteguard
|
|
|
1066
1183
|
properties.each_with_object({}) { |(key, value), acc| acc[key.to_s] = value }
|
|
1067
1184
|
end
|
|
1068
1185
|
|
|
1186
|
+
# Normalize protected-context property keys and values to strings.
|
|
1187
|
+
#
|
|
1188
|
+
# @param properties [Hash, nil] raw protected property hash
|
|
1189
|
+
# @return [Hash] normalized protected property hash
|
|
1190
|
+
def normalize_protected_context_properties(properties)
|
|
1191
|
+
return {} if properties.nil?
|
|
1192
|
+
|
|
1193
|
+
properties.each_with_object({}) { |(key, value), acc| acc[key.to_s] = value.to_s }
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
# Normalize an optional integer field from symbol or string keyed hashes.
|
|
1197
|
+
#
|
|
1198
|
+
# @param raw [Hash] source payload
|
|
1199
|
+
# @param snake_key [Symbol] snake_case key
|
|
1200
|
+
# @param camel_key [String] camelCase key
|
|
1201
|
+
# @return [Integer, nil] normalized integer value
|
|
1202
|
+
def normalize_optional_integer(raw, snake_key, camel_key)
|
|
1203
|
+
value = raw[snake_key] || raw[camel_key]
|
|
1204
|
+
return nil if value.nil?
|
|
1205
|
+
|
|
1206
|
+
Integer(value)
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1069
1209
|
# Normalize a protected-context payload into a `ProtectedContext` object.
|
|
1070
1210
|
#
|
|
1071
1211
|
# @param protected_context [ProtectedContext, Hash] raw protected context
|
|
1072
1212
|
# @return [ProtectedContext] normalized protected context
|
|
1073
1213
|
def normalize_protected_context(protected_context)
|
|
1074
1214
|
raw = if protected_context.is_a?(ProtectedContext)
|
|
1075
|
-
{
|
|
1215
|
+
{
|
|
1216
|
+
properties: protected_context.properties,
|
|
1217
|
+
signature: protected_context.signature,
|
|
1218
|
+
issued_at: protected_context.issued_at,
|
|
1219
|
+
expires_at: protected_context.expires_at
|
|
1220
|
+
}
|
|
1076
1221
|
else
|
|
1077
1222
|
protected_context
|
|
1078
1223
|
end
|
|
1079
1224
|
ProtectedContext.new(
|
|
1080
|
-
properties:
|
|
1081
|
-
|
|
1225
|
+
properties: normalize_protected_context_properties(
|
|
1226
|
+
raw[:properties] || raw["properties"] || {}
|
|
1227
|
+
),
|
|
1228
|
+
signature: (raw[:signature] || raw["signature"]).to_s,
|
|
1229
|
+
issued_at: normalize_optional_integer(raw, :issued_at, "issuedAt"),
|
|
1230
|
+
expires_at: normalize_optional_integer(raw, :expires_at, "expiresAt")
|
|
1082
1231
|
)
|
|
1083
1232
|
end
|
|
1084
1233
|
|
|
@@ -1091,7 +1240,9 @@ module Liteguard
|
|
|
1091
1240
|
|
|
1092
1241
|
ProtectedContext.new(
|
|
1093
1242
|
properties: protected_context.properties.dup,
|
|
1094
|
-
signature: protected_context.signature.dup
|
|
1243
|
+
signature: protected_context.signature.dup,
|
|
1244
|
+
issued_at: protected_context.issued_at,
|
|
1245
|
+
expires_at: protected_context.expires_at
|
|
1095
1246
|
)
|
|
1096
1247
|
end
|
|
1097
1248
|
|
|
@@ -1216,6 +1367,18 @@ module Liteguard
|
|
|
1216
1367
|
payload
|
|
1217
1368
|
end
|
|
1218
1369
|
|
|
1370
|
+
def rate_limit_decision_payload(decision)
|
|
1371
|
+
{
|
|
1372
|
+
evaluationTarget: decision.evaluation_target.to_s,
|
|
1373
|
+
**(decision.dry_run_id ? { dryRunId: decision.dry_run_id } : {}),
|
|
1374
|
+
outcome: decision.outcome.to_s,
|
|
1375
|
+
requestsPerMinute: decision.requests_per_minute,
|
|
1376
|
+
partitionProperties: decision.partition_properties,
|
|
1377
|
+
partitionValues: decision.partition_values,
|
|
1378
|
+
countInWindow: decision.count_in_window,
|
|
1379
|
+
}
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1219
1382
|
# Track an unadopted guard so it can be reported with observation metadata.
|
|
1220
1383
|
#
|
|
1221
1384
|
# @param name [String] guard name to record
|
data/lib/liteguard/scope.rb
CHANGED
|
@@ -21,7 +21,9 @@ module Liteguard
|
|
|
21
21
|
@bundle_key = bundle_key
|
|
22
22
|
@protected_context = protected_context ? ProtectedContext.new(
|
|
23
23
|
properties: protected_context.properties.dup,
|
|
24
|
-
signature: protected_context.signature.dup
|
|
24
|
+
signature: protected_context.signature.dup,
|
|
25
|
+
issued_at: protected_context.issued_at,
|
|
26
|
+
expires_at: protected_context.expires_at
|
|
25
27
|
) : nil
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -160,7 +162,9 @@ module Liteguard
|
|
|
160
162
|
|
|
161
163
|
ProtectedContext.new(
|
|
162
164
|
properties: @protected_context.properties.dup,
|
|
163
|
-
signature: @protected_context.signature.dup
|
|
165
|
+
signature: @protected_context.signature.dup,
|
|
166
|
+
issued_at: @protected_context.issued_at,
|
|
167
|
+
expires_at: @protected_context.expires_at
|
|
164
168
|
)
|
|
165
169
|
end
|
|
166
170
|
|
data/lib/liteguard/types.rb
CHANGED
|
@@ -8,6 +8,10 @@ module Liteguard
|
|
|
8
8
|
|
|
9
9
|
GUARD_DECISION_REASONS = %i[matched_rule default_value unadopted fallback].freeze
|
|
10
10
|
|
|
11
|
+
RATE_LIMIT_EVALUATION_TARGETS = %i[active dry_run].freeze
|
|
12
|
+
|
|
13
|
+
RATE_LIMIT_DECISION_OUTCOMES = %i[within_limit limited would_limit].freeze
|
|
14
|
+
|
|
11
15
|
# A single rule condition within a Guard.
|
|
12
16
|
Rule = Data.define(
|
|
13
17
|
:property_name,
|
|
@@ -17,14 +21,27 @@ module Liteguard
|
|
|
17
21
|
:enabled
|
|
18
22
|
)
|
|
19
23
|
|
|
24
|
+
# GuardRateLimitConfig (mirrors proto message).
|
|
25
|
+
GuardRateLimitConfig = Data.define(
|
|
26
|
+
:requests_per_minute,
|
|
27
|
+
:partition_properties
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# GuardDryRunRateLimit (mirrors proto message).
|
|
31
|
+
GuardDryRunRateLimit = Data.define(
|
|
32
|
+
:dry_run_id,
|
|
33
|
+
:requests_per_minute,
|
|
34
|
+
:partition_properties
|
|
35
|
+
)
|
|
36
|
+
|
|
20
37
|
# A named feature guard with an ordered rule set.
|
|
21
38
|
Guard = Data.define(
|
|
22
39
|
:name,
|
|
23
40
|
:rules,
|
|
24
41
|
:default_value,
|
|
25
42
|
:adopted,
|
|
26
|
-
:
|
|
27
|
-
:
|
|
43
|
+
:rate_limit,
|
|
44
|
+
:dry_run_rate_limit,
|
|
28
45
|
:disable_measurement
|
|
29
46
|
)
|
|
30
47
|
|
|
@@ -44,7 +61,9 @@ module Liteguard
|
|
|
44
61
|
# ProtectedContext (mirrors proto message).
|
|
45
62
|
ProtectedContext = Data.define(
|
|
46
63
|
:properties,
|
|
47
|
-
:signature
|
|
64
|
+
:signature,
|
|
65
|
+
:issued_at,
|
|
66
|
+
:expires_at
|
|
48
67
|
)
|
|
49
68
|
|
|
50
69
|
# GetGuardsRequest (mirrors proto message).
|
|
@@ -57,6 +76,7 @@ module Liteguard
|
|
|
57
76
|
# Parsed response from the guards endpoint.
|
|
58
77
|
GuardsResponse = Data.define(
|
|
59
78
|
:guards,
|
|
79
|
+
:omitted_protected_property_names,
|
|
60
80
|
:refresh_rate_seconds,
|
|
61
81
|
:etag
|
|
62
82
|
)
|
|
@@ -97,6 +117,17 @@ module Liteguard
|
|
|
97
117
|
:guard_execution
|
|
98
118
|
)
|
|
99
119
|
|
|
120
|
+
# RateLimitDecision (mirrors proto message).
|
|
121
|
+
RateLimitDecision = Data.define(
|
|
122
|
+
:evaluation_target,
|
|
123
|
+
:dry_run_id,
|
|
124
|
+
:outcome,
|
|
125
|
+
:requests_per_minute,
|
|
126
|
+
:partition_properties,
|
|
127
|
+
:partition_values,
|
|
128
|
+
:count_in_window
|
|
129
|
+
)
|
|
130
|
+
|
|
100
131
|
# A buffered guard-check record for upload to the data plane.
|
|
101
132
|
Signal = Data.define(
|
|
102
133
|
:guard_name,
|
|
@@ -111,7 +142,8 @@ module Liteguard
|
|
|
111
142
|
:callsite_id,
|
|
112
143
|
:kind,
|
|
113
144
|
:dropped_signals_since_last,
|
|
114
|
-
:measurement
|
|
145
|
+
:measurement,
|
|
146
|
+
:rate_limit_decisions
|
|
115
147
|
)
|
|
116
148
|
|
|
117
149
|
# UnadoptedGuardObservation (mirrors proto message).
|
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.7.20260603
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Liteguard
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04
|
|
11
|
+
date: 2026-06-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -16,28 +16,28 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
19
|
+
version: 3.13.2
|
|
20
20
|
type: :development
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
26
|
+
version: 3.13.2
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: webmock
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
33
|
+
version: 3.26.2
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
40
|
+
version: 3.26.2
|
|
41
41
|
description: Liteguard gives you feature guards, observability, and CVE auto-disable
|
|
42
42
|
in one gem. Guards are evaluated entirely in-process against rules fetched once
|
|
43
43
|
at startup. Every open? check is sub-millisecond with no network round-trip.
|