smplkit 3.0.94 → 3.0.96
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/lib/smplkit/_generated/audit/lib/smplkit_audit_client/api/categories_api.rb +2 -2
- data/lib/smplkit/_generated/audit/lib/smplkit_audit_client/api/event_types_api.rb +2 -2
- data/lib/smplkit/_generated/audit/lib/smplkit_audit_client/api/events_api.rb +4 -4
- data/lib/smplkit/_generated/audit/lib/smplkit_audit_client/api/resource_types_api.rb +2 -2
- data/lib/smplkit/_generated/audit/lib/smplkit_audit_client/models/event_search_request.rb +1 -1
- data/lib/smplkit/_generated/audit/spec/api/categories_api_spec.rb +1 -1
- data/lib/smplkit/_generated/audit/spec/api/event_types_api_spec.rb +1 -1
- data/lib/smplkit/_generated/audit/spec/api/events_api_spec.rb +2 -2
- data/lib/smplkit/_generated/audit/spec/api/resource_types_api_spec.rb +1 -1
- data/lib/smplkit/account/client.rb +121 -0
- data/lib/smplkit/account/models.rb +53 -0
- data/lib/smplkit/api_support.rb +83 -0
- data/lib/smplkit/audit/client.rb +9 -10
- data/lib/smplkit/{management/audit.rb → audit/forwarders.rb} +73 -76
- data/lib/smplkit/audit/models.rb +40 -1
- data/lib/smplkit/buffers.rb +235 -0
- data/lib/smplkit/client.rb +126 -67
- data/lib/smplkit/config/client.rb +617 -182
- data/lib/smplkit/config_resolution.rb +11 -5
- data/lib/smplkit/errors.rb +8 -0
- data/lib/smplkit/flags/client.rb +472 -114
- data/lib/smplkit/flags/types.rb +6 -7
- data/lib/smplkit/{management/jobs.rb → jobs/client.rb} +148 -89
- data/lib/smplkit/logging/client.rb +647 -192
- data/lib/smplkit/logging/helpers.rb +1 -0
- data/lib/smplkit/logging/models.rb +92 -1
- data/lib/smplkit/logging/sources.rb +1 -1
- data/lib/smplkit/platform/client.rb +472 -0
- data/lib/smplkit/platform/models.rb +182 -0
- data/lib/smplkit/{management → platform}/types.rb +7 -4
- data/lib/smplkit/transport.rb +99 -0
- data/lib/smplkit.rb +18 -6
- metadata +11 -7
- data/lib/smplkit/management/buffer.rb +0 -198
- data/lib/smplkit/management/client.rb +0 -1074
- data/lib/smplkit/management/models.rb +0 -178
|
@@ -1,40 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# SIEM forwarder CRUD for the Smpl Audit client.
|
|
4
|
+
#
|
|
5
|
+
# Forwarders are part of the single unified audit surface — there is no
|
|
6
|
+
# runtime/management split for audit (see +Smplkit::Audit::AuditClient+). This
|
|
7
|
+
# file holds the forwarder CRUD sub-client that the unified +AuditClient+
|
|
8
|
+
# exposes as +.forwarders+:
|
|
9
|
+
#
|
|
10
|
+
# * +ForwardersClient+ — +forwarders.new/get/list/save/delete+
|
|
11
|
+
#
|
|
12
|
+
# The forwarder model classes (+Forwarder+, +ForwarderEnvironment+, …) live in
|
|
13
|
+
# +lib/smplkit/audit/models.rb+.
|
|
3
14
|
module Smplkit
|
|
4
|
-
module
|
|
5
|
-
#
|
|
15
|
+
module Audit
|
|
16
|
+
# Surface for +client.audit.forwarders.*+ — manage the customer's
|
|
17
|
+
# configured SIEM forwarders.
|
|
6
18
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
attr_reader :forwarders
|
|
13
|
-
|
|
14
|
-
def initialize(api_client)
|
|
15
|
-
@forwarders = ForwardersNamespace.new(
|
|
16
|
-
SmplkitGeneratedClient::Audit::ForwardersApi.new(api_client)
|
|
17
|
-
)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# +mgmt.audit.forwarders.*+ — manage the customer's configured SIEM
|
|
22
|
-
# forwarders.
|
|
23
|
-
#
|
|
24
|
-
# The active-record entry point is {#new_forwarder}: instantiate a
|
|
25
|
-
# draft, mutate fields, then call {Smplkit::Audit::Forwarder#save}.
|
|
26
|
-
# The namespace exposes {#list}, {#get}, and {#delete} directly; the
|
|
27
|
-
# +_create_forwarder+ / +_update_forwarder+ helpers are private and
|
|
28
|
-
# invoked by {Smplkit::Audit::Forwarder#save}.
|
|
29
|
-
class ForwardersNamespace
|
|
19
|
+
# The active-record entry point is {#new}: instantiate a draft, mutate
|
|
20
|
+
# fields, then call {Smplkit::Audit::Forwarder#save}. The client exposes
|
|
21
|
+
# {#list}, {#get}, and {#delete} directly; the +_create_forwarder+ /
|
|
22
|
+
# +_update_forwarder+ helpers are invoked by {Smplkit::Audit::Forwarder#save}.
|
|
23
|
+
class ForwardersClient
|
|
30
24
|
def initialize(api)
|
|
31
25
|
@api = api
|
|
32
26
|
end
|
|
33
27
|
|
|
34
|
-
# Construct an unsaved {Smplkit::Audit::Forwarder} bound to this
|
|
35
|
-
#
|
|
28
|
+
# Construct an unsaved {Smplkit::Audit::Forwarder} bound to this client.
|
|
29
|
+
# Call +#save+ on the returned instance to persist.
|
|
36
30
|
#
|
|
37
|
-
# @param
|
|
31
|
+
# @param id [String] Caller-supplied unique identifier (the forwarder's
|
|
32
|
+
# key). Unique within the account; immutable. The audit service returns
|
|
33
|
+
# 409 if another live forwarder already uses this id.
|
|
34
|
+
# @param name [String] Display name. Defaults to +id+ when not supplied.
|
|
38
35
|
# @param forwarder_type [String] One of {Smplkit::Audit::ForwarderType::VALUES}.
|
|
39
36
|
# @param configuration [Smplkit::Audit::HttpConfiguration] Destination
|
|
40
37
|
# request configuration. Headers carry credentials and are encrypted at
|
|
@@ -56,11 +53,9 @@ module Smplkit
|
|
|
56
53
|
# @param filter [Hash, nil] Optional JSON Logic filter; events that don't
|
|
57
54
|
# match are recorded as +filtered_out+ deliveries.
|
|
58
55
|
# @param transform [Object, nil] Optional template applied to each event
|
|
59
|
-
# before delivery.
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# +TransformType::JSONATA+, +transform+ must be a +String+ (the JSONata
|
|
63
|
-
# expression).
|
|
56
|
+
# before delivery. Must be paired with a non-nil +transform_type+; when
|
|
57
|
+
# +transform_type+ is +TransformType::JSONATA+, +transform+ must be a
|
|
58
|
+
# +String+ (the JSONata expression).
|
|
64
59
|
# @param transform_type [String, nil] Engine that evaluates +transform+ —
|
|
65
60
|
# one of {Smplkit::Audit::TransformType::VALUES}. Must be paired with a
|
|
66
61
|
# non-nil +transform+.
|
|
@@ -68,12 +63,12 @@ module Smplkit
|
|
|
68
63
|
# nil or both set, or when +transform_type+ is +JSONATA+ and +transform+
|
|
69
64
|
# is not a +String+.
|
|
70
65
|
# @return [Smplkit::Audit::Forwarder]
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
def new(id, forwarder_type:, configuration:, name: nil,
|
|
67
|
+
environments: nil, description: nil,
|
|
68
|
+
forward_smplkit_events: false,
|
|
69
|
+
filter: nil, transform: nil, transform_type: nil)
|
|
70
|
+
Forwarder.send(:validate_transform_pair!, transform, transform_type)
|
|
71
|
+
Forwarder.new(
|
|
77
72
|
self,
|
|
78
73
|
id: id,
|
|
79
74
|
name: name || id,
|
|
@@ -91,33 +86,31 @@ module Smplkit
|
|
|
91
86
|
# List forwarders for the authenticated account.
|
|
92
87
|
#
|
|
93
88
|
# Offset paginated per ADR-014: pass +page_number+ (1-based) and
|
|
94
|
-
# +page_size+ (default 1000, max 1000). Pass +meta_total: true+ to
|
|
95
|
-
#
|
|
96
|
-
#
|
|
89
|
+
# +page_size+ (default 1000, max 1000). Pass +meta_total: true+ to populate
|
|
90
|
+
# +total+ and +total_pages+ in the returned +pagination+ block (costs an
|
|
91
|
+
# extra COUNT query server-side).
|
|
97
92
|
#
|
|
98
93
|
# @return [ForwarderListPage]
|
|
99
94
|
def list(forwarder_type: nil, page_number: nil, page_size: nil, meta_total: nil)
|
|
100
95
|
opts = {}
|
|
101
|
-
opts[:filter_forwarder_type] =
|
|
96
|
+
opts[:filter_forwarder_type] = ForwarderType.coerce(forwarder_type) if forwarder_type
|
|
102
97
|
opts[:page_number] = page_number if page_number
|
|
103
98
|
opts[:page_size] = page_size if page_size
|
|
104
99
|
opts[:meta_total] = meta_total unless meta_total.nil?
|
|
105
100
|
|
|
106
|
-
resp =
|
|
107
|
-
forwarders = (resp.data || []).map
|
|
108
|
-
|
|
109
|
-
end
|
|
110
|
-
ForwarderListPage.new(forwarders, Smplkit::Audit.extract_pagination(resp.meta))
|
|
101
|
+
resp = Audit.call_api { @api.list_forwarders(opts) }
|
|
102
|
+
forwarders = (resp.data || []).map { |r| Forwarder.from_resource(r, client: self) }
|
|
103
|
+
ForwarderListPage.new(forwarders, Audit.extract_pagination(resp.meta))
|
|
111
104
|
end
|
|
112
105
|
|
|
113
|
-
# Fetch a single forwarder by id. The returned instance is bound to
|
|
114
|
-
#
|
|
106
|
+
# Fetch a single forwarder by id. The returned instance is bound to this
|
|
107
|
+
# client, so +forwarder.save+ and +forwarder.delete+ work.
|
|
115
108
|
#
|
|
116
109
|
# @param forwarder_id [String]
|
|
117
110
|
# @return [Smplkit::Audit::Forwarder]
|
|
118
111
|
def get(forwarder_id)
|
|
119
|
-
resp =
|
|
120
|
-
|
|
112
|
+
resp = Audit.call_api { @api.get_forwarder(forwarder_id) }
|
|
113
|
+
Forwarder.from_resource(resp.data, client: self)
|
|
121
114
|
end
|
|
122
115
|
|
|
123
116
|
# Soft-delete a forwarder.
|
|
@@ -125,7 +118,7 @@ module Smplkit
|
|
|
125
118
|
# @param forwarder_id [String]
|
|
126
119
|
# @return [nil]
|
|
127
120
|
def delete(forwarder_id)
|
|
128
|
-
|
|
121
|
+
Audit.call_api { @api.delete_forwarder(forwarder_id) }
|
|
129
122
|
nil
|
|
130
123
|
end
|
|
131
124
|
|
|
@@ -136,22 +129,21 @@ module Smplkit
|
|
|
136
129
|
raise ArgumentError, "Forwarder.id is required on create (caller-supplied key)"
|
|
137
130
|
end
|
|
138
131
|
|
|
139
|
-
resp =
|
|
140
|
-
|
|
132
|
+
resp = Audit.call_api { @api.create_forwarder(build_create_body(forwarder)) }
|
|
133
|
+
Forwarder.from_resource(resp.data, client: self)
|
|
141
134
|
end
|
|
142
135
|
|
|
143
|
-
# @api private — Full-replace PUT for an existing forwarder. Called
|
|
144
|
-
#
|
|
136
|
+
# @api private — Full-replace PUT for an existing forwarder. Called by
|
|
137
|
+
# {Smplkit::Audit::Forwarder#save} on instances with +created_at+.
|
|
145
138
|
#
|
|
146
|
-
# Header values must be re-supplied as plaintext; the GET path
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
# round-trip them.
|
|
139
|
+
# Header values must be re-supplied as plaintext; the GET path redacts
|
|
140
|
+
# them, so a PUT body containing +"<redacted>"+ would persist that literal.
|
|
141
|
+
# Track real header values client-side and round-trip them.
|
|
150
142
|
def _update_forwarder(forwarder)
|
|
151
143
|
raise ArgumentError, "cannot update a Forwarder with no id" if forwarder.id.nil?
|
|
152
144
|
|
|
153
|
-
resp =
|
|
154
|
-
|
|
145
|
+
resp = Audit.call_api { @api.update_forwarder(forwarder.id, build_body(forwarder)) }
|
|
146
|
+
Forwarder.from_resource(resp.data, client: self)
|
|
155
147
|
end
|
|
156
148
|
|
|
157
149
|
private
|
|
@@ -160,16 +152,15 @@ module Smplkit
|
|
|
160
152
|
#
|
|
161
153
|
# Accepts either {Smplkit::Audit::ForwarderEnvironment} values or plain
|
|
162
154
|
# hashes (+{ enabled: true, configuration: HttpConfiguration.new(...) }+)
|
|
163
|
-
# so callers can use the lightweight hash form without importing the
|
|
164
|
-
# model.
|
|
155
|
+
# so callers can use the lightweight hash form without importing the model.
|
|
165
156
|
def normalize_environments(environments)
|
|
166
157
|
return {} if environments.nil? || environments.empty?
|
|
167
158
|
|
|
168
159
|
environments.each_with_object({}) do |(env_key, value), out|
|
|
169
|
-
out[env_key.to_s] = if value.is_a?(
|
|
160
|
+
out[env_key.to_s] = if value.is_a?(ForwarderEnvironment)
|
|
170
161
|
value
|
|
171
162
|
else
|
|
172
|
-
|
|
163
|
+
ForwarderEnvironment.new(
|
|
173
164
|
enabled: value[:enabled] || value["enabled"] || false,
|
|
174
165
|
configuration: value[:configuration] || value["configuration"]
|
|
175
166
|
)
|
|
@@ -186,7 +177,7 @@ module Smplkit
|
|
|
186
177
|
(environments || {}).each_with_object({}) do |(env_key, env), out|
|
|
187
178
|
out[env_key.to_s] = SmplkitGeneratedClient::Audit::ForwarderEnvironment.new(
|
|
188
179
|
enabled: env.enabled,
|
|
189
|
-
configuration: env.configuration.nil? ? nil :
|
|
180
|
+
configuration: env.configuration.nil? ? nil : HttpConfiguration.to_wire(env.configuration)
|
|
190
181
|
)
|
|
191
182
|
end
|
|
192
183
|
end
|
|
@@ -197,20 +188,20 @@ module Smplkit
|
|
|
197
188
|
SmplkitGeneratedClient::Audit::Forwarder.new(
|
|
198
189
|
name: forwarder.name,
|
|
199
190
|
description: forwarder.description,
|
|
200
|
-
forwarder_type:
|
|
191
|
+
forwarder_type: ForwarderType.coerce(forwarder.forwarder_type),
|
|
201
192
|
forward_smplkit_events: forwarder.forward_smplkit_events,
|
|
202
193
|
environments: environments_to_wire(forwarder.environments),
|
|
203
194
|
filter: forwarder.filter,
|
|
204
|
-
transform_type:
|
|
195
|
+
transform_type: TransformType.coerce(forwarder.transform_type),
|
|
205
196
|
transform: forwarder.transform,
|
|
206
|
-
configuration:
|
|
197
|
+
configuration: HttpConfiguration.to_wire(forwarder.configuration)
|
|
207
198
|
)
|
|
208
199
|
end
|
|
209
200
|
|
|
210
201
|
def build_create_body(forwarder)
|
|
211
|
-
# Create uses the distinct ForwarderCreateRequest envelope; the
|
|
212
|
-
#
|
|
213
|
-
#
|
|
202
|
+
# Create uses the distinct ForwarderCreateRequest envelope; the audit
|
|
203
|
+
# service requires data.id (the customer-supplied key) on create and
|
|
204
|
+
# 409s on conflict.
|
|
214
205
|
resource = SmplkitGeneratedClient::Audit::ForwarderCreateResource.new(
|
|
215
206
|
id: forwarder.id.to_s,
|
|
216
207
|
type: "forwarder",
|
|
@@ -230,13 +221,19 @@ module Smplkit
|
|
|
230
221
|
end
|
|
231
222
|
end
|
|
232
223
|
|
|
233
|
-
# A single page returned from {
|
|
224
|
+
# A single page returned from {ForwardersClient#list}.
|
|
234
225
|
#
|
|
235
226
|
# @!attribute [rw] forwarders
|
|
236
227
|
# @return [Array<Smplkit::Audit::Forwarder>] Forwarders in this page.
|
|
237
228
|
# @!attribute [rw] pagination
|
|
238
229
|
# @return [Hash] +meta.pagination+ block (+:page+, +:size+, and — only when
|
|
239
230
|
# the caller passed +meta_total: true+ — +:total+ / +:total_pages+).
|
|
240
|
-
ForwarderListPage = Struct.new(:forwarders, :pagination)
|
|
231
|
+
ForwarderListPage = Struct.new(:forwarders, :pagination) do
|
|
232
|
+
include Enumerable
|
|
233
|
+
|
|
234
|
+
def each(&) = forwarders.each(&)
|
|
235
|
+
def length = forwarders.length
|
|
236
|
+
alias_method :size, :length
|
|
237
|
+
end
|
|
241
238
|
end
|
|
242
239
|
end
|
data/lib/smplkit/audit/models.rb
CHANGED
|
@@ -422,7 +422,7 @@ module Smplkit
|
|
|
422
422
|
# A SIEM streaming forwarder configured on the customer's account.
|
|
423
423
|
#
|
|
424
424
|
# Active-record style: instantiate via
|
|
425
|
-
# +
|
|
425
|
+
# +client.audit.forwarders.new(...)+, mutate fields directly,
|
|
426
426
|
# and call {#save} to persist or {#delete} to remove. Header values in
|
|
427
427
|
# +configuration.headers+ are returned redacted on reads — the GET path
|
|
428
428
|
# on the audit API replaces every header value with +"<redacted>"+.
|
|
@@ -558,6 +558,45 @@ module Smplkit
|
|
|
558
558
|
end
|
|
559
559
|
alias delete! delete
|
|
560
560
|
|
|
561
|
+
# Set this forwarder's destination configuration in memory.
|
|
562
|
+
#
|
|
563
|
+
# With +environment+ omitted, replaces the base {#configuration}. With
|
|
564
|
+
# +environment+ given, sets the per-environment override's configuration
|
|
565
|
+
# on {#environments}, creating the override entry if it doesn't exist yet
|
|
566
|
+
# (preserving any already-set +enabled+ on it). Call {#save} to persist.
|
|
567
|
+
def set_configuration(configuration, environment: nil)
|
|
568
|
+
if environment.nil?
|
|
569
|
+
@configuration = configuration
|
|
570
|
+
else
|
|
571
|
+
_environment_override(environment).configuration = configuration
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Set this forwarder's enablement in memory.
|
|
576
|
+
#
|
|
577
|
+
# With +environment+ omitted, sets the base {#enabled} (which the server
|
|
578
|
+
# pins false regardless — enablement is per-environment). With
|
|
579
|
+
# +environment+ given, sets the per-environment override's +enabled+ on
|
|
580
|
+
# {#environments}, creating the override entry if it doesn't exist yet
|
|
581
|
+
# (preserving any already-set +configuration+ on it). Call {#save} to
|
|
582
|
+
# persist.
|
|
583
|
+
def set_enabled(enabled, environment: nil)
|
|
584
|
+
if environment.nil?
|
|
585
|
+
@enabled = enabled
|
|
586
|
+
else
|
|
587
|
+
_environment_override(environment).enabled = enabled
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Return the override for +environment+, creating an empty one if absent.
|
|
592
|
+
#
|
|
593
|
+
# The per-environment mutators reach through here so an existing
|
|
594
|
+
# override's other field is preserved when only one of +enabled+ /
|
|
595
|
+
# +configuration+ is being set.
|
|
596
|
+
def _environment_override(environment)
|
|
597
|
+
@environments[environment] ||= ForwarderEnvironment.new
|
|
598
|
+
end
|
|
599
|
+
|
|
561
600
|
# @api private
|
|
562
601
|
def _apply(other)
|
|
563
602
|
@id = other.id
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
# Registration buffers backing the SDK's batched-discovery sub-clients.
|
|
6
|
+
#
|
|
7
|
+
# Four buffer types, each owned by the sub-client that drains it:
|
|
8
|
+
#
|
|
9
|
+
# - +ContextRegistrationBuffer+ -> +client.platform.contexts._buffer+
|
|
10
|
+
# - +FlagRegistrationBuffer+ -> +client.flags._buffer+
|
|
11
|
+
# - +ConfigRegistrationBuffer+ -> +client.config._buffer+
|
|
12
|
+
# - +LoggerRegistrationBuffer+ -> +client.logging.loggers._buffer+
|
|
13
|
+
#
|
|
14
|
+
# There is exactly one buffer + one bulk-flush implementation per resource.
|
|
15
|
+
module Smplkit
|
|
16
|
+
# When the deduplication LRU exceeds this size, the oldest entry is
|
|
17
|
+
# evicted. The next observation of an evicted entry will re-flush.
|
|
18
|
+
CONTEXT_REGISTRATION_LRU_SIZE = 10_000
|
|
19
|
+
|
|
20
|
+
# Pending-queue size that triggers an immediate background flush from
|
|
21
|
+
# inside +register+. The periodic timer on +Client+ covers the tail
|
|
22
|
+
# case for low-traffic services.
|
|
23
|
+
CONTEXT_BATCH_FLUSH_SIZE = 100
|
|
24
|
+
FLAG_BATCH_FLUSH_SIZE = 50
|
|
25
|
+
LOGGER_BATCH_FLUSH_SIZE = 50
|
|
26
|
+
CONFIG_BATCH_FLUSH_SIZE = 50
|
|
27
|
+
|
|
28
|
+
# Thread-safe batch buffer for context registration.
|
|
29
|
+
class ContextRegistrationBuffer
|
|
30
|
+
def initialize
|
|
31
|
+
@seen = {}
|
|
32
|
+
@pending = []
|
|
33
|
+
@lock = Mutex.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Queue any unseen contexts.
|
|
37
|
+
def observe(contexts)
|
|
38
|
+
@lock.synchronize do
|
|
39
|
+
contexts.each do |ctx|
|
|
40
|
+
cache_key = [ctx.type, ctx.key]
|
|
41
|
+
next if @seen.key?(cache_key)
|
|
42
|
+
|
|
43
|
+
@seen.shift if @seen.size >= CONTEXT_REGISTRATION_LRU_SIZE
|
|
44
|
+
@seen[cache_key] = ctx.attributes
|
|
45
|
+
@pending << { "type" => ctx.type, "key" => ctx.key, "attributes" => ctx.attributes.dup }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Return and clear the current pending batch.
|
|
51
|
+
def drain
|
|
52
|
+
@lock.synchronize do
|
|
53
|
+
batch = @pending
|
|
54
|
+
@pending = []
|
|
55
|
+
batch
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def pending_count
|
|
60
|
+
@lock.synchronize { @pending.length }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Thread-safe batch buffer for flag declarations.
|
|
65
|
+
#
|
|
66
|
+
# Use +peek+ + +commit(ids)+ for the send path so a failed POST leaves
|
|
67
|
+
# declarations queued for the next attempt; the legacy +drain+ is
|
|
68
|
+
# unconditional and used only by tests.
|
|
69
|
+
class FlagRegistrationBuffer
|
|
70
|
+
def initialize
|
|
71
|
+
@seen = {}
|
|
72
|
+
@pending = []
|
|
73
|
+
@lock = Mutex.new
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def add(declaration)
|
|
77
|
+
@lock.synchronize do
|
|
78
|
+
next if @seen.key?(declaration.id)
|
|
79
|
+
|
|
80
|
+
@seen[declaration.id] = true
|
|
81
|
+
item = { "id" => declaration.id, "type" => declaration.type, "default" => declaration.default }
|
|
82
|
+
item["service"] = declaration.service if declaration.service
|
|
83
|
+
item["environment"] = declaration.environment if declaration.environment
|
|
84
|
+
@pending << item
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def peek
|
|
89
|
+
@lock.synchronize { @pending.dup }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def commit(ids)
|
|
93
|
+
return if ids.nil? || ids.empty?
|
|
94
|
+
|
|
95
|
+
committed = ids.to_set
|
|
96
|
+
@lock.synchronize { @pending.reject! { |item| committed.include?(item["id"]) } }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def drain
|
|
100
|
+
@lock.synchronize do
|
|
101
|
+
batch = @pending
|
|
102
|
+
@pending = []
|
|
103
|
+
batch
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def pending_count
|
|
108
|
+
@lock.synchronize { @pending.length }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Thread-safe batch buffer for config declarations.
|
|
113
|
+
#
|
|
114
|
+
# Configs differ from flags/loggers because each entry carries a nested
|
|
115
|
+
# +items+ dict that grows incrementally as the customer's code touches more
|
|
116
|
+
# typed getters on a declared handle. The buffer therefore stores per-config
|
|
117
|
+
# metadata permanently (so post-flush deltas can be re-attributed to the
|
|
118
|
+
# right service/environment) and dedups items per +(config_id, item_key)+ so
|
|
119
|
+
# we never re-send an item that the server has already accepted.
|
|
120
|
+
#
|
|
121
|
+
# Call sites:
|
|
122
|
+
#
|
|
123
|
+
# - +declare+ once per +client.config.bind(id, ...)+.
|
|
124
|
+
# - +add_item+ for every introspected leaf field, or anything else the
|
|
125
|
+
# runtime client observes. Repeated calls with the same
|
|
126
|
+
# +(config_id, item_key)+ after a successful flush are no-ops.
|
|
127
|
+
# - +drain+ returns the pending payload list, clears the pending buffer, and
|
|
128
|
+
# records what was sent.
|
|
129
|
+
#
|
|
130
|
+
# The buffer never drops metadata — only +pending+ is cleared on flush. If
|
|
131
|
+
# the customer's code declares new items via typed getters after a flush, a
|
|
132
|
+
# fresh pending entry is created using the stored metadata so the server can
|
|
133
|
+
# route the delta to the right source row.
|
|
134
|
+
class ConfigRegistrationBuffer
|
|
135
|
+
def initialize
|
|
136
|
+
@pending = {} # config_id -> { id:, items: {}, ...meta }
|
|
137
|
+
@meta = {} # config_id -> { service:, environment:, parent:, name:, description: }
|
|
138
|
+
@sent_items = {} # "#{config_id}::#{item_key}" -> true
|
|
139
|
+
@lock = Mutex.new
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Register a configuration. Idempotent within a process.
|
|
143
|
+
def declare(config_id, service:, environment:, parent: nil, name: nil, description: nil)
|
|
144
|
+
@lock.synchronize do
|
|
145
|
+
next if @meta.key?(config_id)
|
|
146
|
+
|
|
147
|
+
@meta[config_id] = {
|
|
148
|
+
service: service, environment: environment,
|
|
149
|
+
parent: parent, name: name, description: description
|
|
150
|
+
}
|
|
151
|
+
@pending[config_id] = build_entry(config_id)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Queue an item declaration if not already sent.
|
|
156
|
+
#
|
|
157
|
+
# Must be preceded by +declare+ for the same +config_id+; otherwise the
|
|
158
|
+
# call is dropped (no implicit declaration).
|
|
159
|
+
def add_item(config_id, item_key, item_type, default, description = nil)
|
|
160
|
+
@lock.synchronize do
|
|
161
|
+
next unless @meta.key?(config_id)
|
|
162
|
+
next if @sent_items.key?("#{config_id}::#{item_key}")
|
|
163
|
+
|
|
164
|
+
entry = (@pending[config_id] ||= build_entry(config_id))
|
|
165
|
+
next if entry["items"].key?(item_key)
|
|
166
|
+
|
|
167
|
+
item = { "value" => default, "type" => item_type }
|
|
168
|
+
item["description"] = description unless description.nil?
|
|
169
|
+
entry["items"][item_key] = item
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Return and clear the pending batch; record sent items.
|
|
174
|
+
def drain
|
|
175
|
+
@lock.synchronize do
|
|
176
|
+
entries = @pending.values
|
|
177
|
+
entries.each do |entry|
|
|
178
|
+
entry["items"].each_key { |item_key| @sent_items["#{entry["id"]}::#{item_key}"] = true }
|
|
179
|
+
end
|
|
180
|
+
@pending = {}
|
|
181
|
+
entries
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def pending_count
|
|
186
|
+
@lock.synchronize { @pending.size }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def build_entry(config_id)
|
|
192
|
+
meta = @meta[config_id]
|
|
193
|
+
entry = { "id" => config_id, "items" => {} }
|
|
194
|
+
%i[service environment parent name description].each do |k|
|
|
195
|
+
v = meta[k]
|
|
196
|
+
entry[k.to_s] = v unless v.nil?
|
|
197
|
+
end
|
|
198
|
+
entry
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Thread-safe batch buffer for logger discovery.
|
|
203
|
+
class LoggerRegistrationBuffer
|
|
204
|
+
def initialize
|
|
205
|
+
@seen = {}
|
|
206
|
+
@pending = []
|
|
207
|
+
@lock = Mutex.new
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def add(source)
|
|
211
|
+
@lock.synchronize do
|
|
212
|
+
next if @seen.key?(source.name)
|
|
213
|
+
|
|
214
|
+
@seen[source.name] = source.resolved_level
|
|
215
|
+
item = { "id" => source.name, "resolved_level" => source.resolved_level&.to_s }
|
|
216
|
+
item["level"] = source.level&.to_s if source.level
|
|
217
|
+
item["service"] = source.service if source.service
|
|
218
|
+
item["environment"] = source.environment if source.environment
|
|
219
|
+
@pending << item
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def drain
|
|
224
|
+
@lock.synchronize do
|
|
225
|
+
batch = @pending
|
|
226
|
+
@pending = []
|
|
227
|
+
batch
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def pending_count
|
|
232
|
+
@lock.synchronize { @pending.length }
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|