smplkit 3.0.95 → 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/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
|
@@ -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
|
data/lib/smplkit/client.rb
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "concurrent"
|
|
4
4
|
|
|
5
|
-
require "smplkit/audit/client"
|
|
6
|
-
|
|
7
5
|
module Smplkit
|
|
8
6
|
# Synchronous entry point for the smplkit SDK.
|
|
9
7
|
#
|
|
@@ -28,9 +26,12 @@ module Smplkit
|
|
|
28
26
|
# internal SDK-owned threads; public methods block the calling thread and
|
|
29
27
|
# return values directly.
|
|
30
28
|
class Client
|
|
29
|
+
# Periodic flush of all sub-client registration buffers (contexts, flags,
|
|
30
|
+
# loggers). Threshold flushes still fire immediately when buffers fill up;
|
|
31
|
+
# this timer is the liveness guarantee for the tail.
|
|
31
32
|
PERIODIC_FLUSH_INTERVAL = 60.0
|
|
32
33
|
|
|
33
|
-
attr_reader :
|
|
34
|
+
attr_reader :platform, :account, :config, :flags, :logging, :audit, :jobs
|
|
34
35
|
|
|
35
36
|
# Construct, yield to the block, and close on exit.
|
|
36
37
|
def self.open(**kwargs)
|
|
@@ -42,7 +43,7 @@ module Smplkit
|
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
def initialize(api_key: nil, environment: nil, service: nil, profile: nil,
|
|
46
|
+
def initialize(api_key: nil, environment: nil, service: nil, profile: nil,
|
|
46
47
|
base_domain: nil, scheme: nil, debug: nil, telemetry: nil,
|
|
47
48
|
extra_headers: nil)
|
|
48
49
|
cfg = ConfigResolution.resolve_config(
|
|
@@ -56,6 +57,7 @@ module Smplkit
|
|
|
56
57
|
@service = cfg.service
|
|
57
58
|
@base_domain = cfg.base_domain
|
|
58
59
|
@scheme = cfg.scheme
|
|
60
|
+
@extra_headers = extra_headers
|
|
59
61
|
|
|
60
62
|
masked_key = cfg.api_key.length > 10 ? "#{cfg.api_key[0, 10]}..." : cfg.api_key
|
|
61
63
|
Smplkit.debug(
|
|
@@ -65,54 +67,86 @@ module Smplkit
|
|
|
65
67
|
"debug=#{cfg.debug} telemetry=#{cfg.telemetry}"
|
|
66
68
|
)
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@
|
|
70
|
+
# Build the per-service HTTP transports + the context-registration buffer.
|
|
71
|
+
# Side-effect-free: each transport connects lazily on first call.
|
|
72
|
+
# client.platform owns the buffer; client.config/flags/logging/jobs borrow
|
|
73
|
+
# their transports from here.
|
|
74
|
+
@transports = Transport.build_service_transports(Transport.to_transport_config(cfg, extra_headers))
|
|
73
75
|
|
|
74
|
-
app_url =
|
|
75
|
-
flags_url = ConfigResolution.service_url(cfg.scheme, "flags", cfg.base_domain)
|
|
76
|
-
logging_url = ConfigResolution.service_url(cfg.scheme, "logging", cfg.base_domain)
|
|
76
|
+
app_url = @transports.app_url
|
|
77
77
|
audit_url = ConfigResolution.service_url(cfg.scheme, "audit", cfg.base_domain)
|
|
78
|
+
|
|
79
|
+
# Alias the shared HTTP transports — single connection pool per service.
|
|
80
|
+
@http_client = @transports.config_http
|
|
81
|
+
@app_http = @transports.app_http
|
|
78
82
|
@app_base_url = app_url
|
|
79
83
|
|
|
84
|
+
# Metrics reporter
|
|
80
85
|
@metrics = if cfg.telemetry
|
|
81
|
-
MetricsReporter.new(http_client: @
|
|
82
|
-
environment: cfg.environment,
|
|
83
|
-
service: cfg.service)
|
|
86
|
+
MetricsReporter.new(http_client: @app_http, environment: cfg.environment, service: cfg.service)
|
|
84
87
|
end
|
|
85
88
|
|
|
86
89
|
@ws_manager = nil
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
# Platform's cross-cutting CRUD on one client; wired into this parent so
|
|
91
|
+
# it borrows the shared app transport, and owns the context-registration
|
|
92
|
+
# buffer. Built BEFORE flags so the contexts seam below is available.
|
|
93
|
+
@platform = Platform::PlatformClient.new(app_transport: @app_http)
|
|
94
|
+
# Account-level settings on one client; built from the app url + api key
|
|
95
|
+
# (the settings sub-client uses Faraday directly).
|
|
96
|
+
@account = Account::AccountClient.new(api_key: cfg.api_key, base_url: app_url, extra_headers: extra_headers)
|
|
97
|
+
# Config's full surface on one client; wired into this parent so it
|
|
98
|
+
# borrows the shared config transport and WebSocket.
|
|
99
|
+
@config = Config::ConfigClient.new(parent: self, transport: @transports.config_http, metrics: @metrics)
|
|
100
|
+
# Flags' full surface on one client; wired into this parent so it borrows
|
|
101
|
+
# the shared flags transport and WebSocket. ``contexts`` is the injection
|
|
102
|
+
# seam for evaluation-context registration, wired to
|
|
103
|
+
# ``client.platform.contexts``.
|
|
104
|
+
@flags = Flags::FlagsClient.new(
|
|
105
|
+
parent: self, transport: @transports.flags_http, contexts: @platform.contexts, metrics: @metrics
|
|
106
|
+
)
|
|
107
|
+
# Logging's full surface on one client; wired into this parent so it
|
|
108
|
+
# borrows the shared logging transport and WebSocket. The two management
|
|
109
|
+
# sub-clients live at client.logging.loggers / client.logging.log_groups.
|
|
110
|
+
@logging = Logging::LoggingClient.new(parent: self, transport: @transports.logging_http, metrics: @metrics)
|
|
111
|
+
# Audit's full surface on one client; this runtime instance carries the
|
|
112
|
+
# configured environment as ``X-Smplkit-Environment`` and owns its own
|
|
113
|
+
# transport (closed in ``close``).
|
|
114
|
+
@audit = Audit::AuditClient.new(
|
|
115
|
+
api_key: cfg.api_key, base_url: audit_url, environment: cfg.environment, extra_headers: extra_headers
|
|
116
|
+
)
|
|
117
|
+
# Jobs has no runtime/management split — reuse the shared jobs transport
|
|
118
|
+
# (single connection pool) so ``client.jobs`` is one-stop.
|
|
119
|
+
@jobs = Jobs::JobsClient.new(auth_client: @transports.jobs_http)
|
|
94
120
|
|
|
121
|
+
# Construction is side-effect-free: no background threads, no phone-home.
|
|
122
|
+
# The periodic registration-buffer flush and the service-context
|
|
123
|
+
# registration are deferred until the first config/flags/logging operation
|
|
124
|
+
# or set_context via ``_ensure_started`` — so an audit-only or jobs-only
|
|
125
|
+
# customer pays zero threads and zero network at construction.
|
|
95
126
|
@closed = false
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@
|
|
99
|
-
|
|
100
|
-
end
|
|
127
|
+
@started = false
|
|
128
|
+
@start_lock = Mutex.new
|
|
129
|
+
@flush_timer = nil
|
|
130
|
+
@init_thread = nil
|
|
101
131
|
end
|
|
102
132
|
|
|
103
|
-
#
|
|
133
|
+
# Optionally pre-warm the SDK and block until the live socket is up.
|
|
104
134
|
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
# After this returns, +flag.get+
|
|
108
|
-
# first-request connect tax) and
|
|
109
|
-
# server event from this point
|
|
135
|
+
# Eagerly connects config and flags — flushing discovery, pre-fetching all
|
|
136
|
+
# flags and configs into the local cache, opening the live-updates WebSocket
|
|
137
|
+
# — and waits for the handshake to complete. After this returns, +flag.get+
|
|
138
|
+
# / +client.config.subscribe+ hit cache (no first-request connect tax) and
|
|
139
|
+
# any +on_change+ listeners receive every server event from this point
|
|
140
|
+
# forward.
|
|
110
141
|
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
142
|
+
# Optional: config and flags connect lazily on first live use, so this is
|
|
143
|
+
# purely a pre-warm / WebSocket-ready barrier. Logging integration is *not*
|
|
144
|
+
# connected here — call +client.logging.install+ separately if you want it
|
|
145
|
+
# (it installs adapters and hooks into your application's logger, which
|
|
146
|
+
# should be opt-in).
|
|
113
147
|
def wait_until_ready(timeout: 10.0)
|
|
114
|
-
@flags.
|
|
115
|
-
@config.
|
|
148
|
+
@flags._ensure_connected
|
|
149
|
+
@config._ensure_connected
|
|
116
150
|
ws = _ensure_ws
|
|
117
151
|
deadline = monotonic_now + timeout
|
|
118
152
|
while ws.connection_status != "connected"
|
|
@@ -127,21 +161,26 @@ module Smplkit
|
|
|
127
161
|
|
|
128
162
|
# Stash +contexts+ as the current request's evaluation context.
|
|
129
163
|
#
|
|
164
|
+
# Typical use is from middleware — set the context once at request entry and
|
|
165
|
+
# every +flag.get+ (and other context-sensitive evaluations) inside that
|
|
166
|
+
# request automatically picks it up.
|
|
167
|
+
#
|
|
168
|
+
# Each unique +(type, key)+ is also queued for bulk registration on the
|
|
169
|
+
# management API (deduplicated; flushed in the background).
|
|
170
|
+
#
|
|
130
171
|
# Two usage shapes:
|
|
131
172
|
#
|
|
132
173
|
# # Fire-and-forget (typical middleware)
|
|
133
174
|
# client.set_context([Smplkit::Context.new("user", "u-123")])
|
|
134
175
|
#
|
|
135
|
-
# # Scoped block (impersonation or one-off override)
|
|
176
|
+
# # Scoped block (e.g. impersonation or one-off override)
|
|
136
177
|
# client.set_context([Smplkit::Context.new("user", "impersonated")]) do
|
|
137
178
|
# # ...
|
|
138
179
|
# end
|
|
139
180
|
# # original context restored here
|
|
140
|
-
#
|
|
141
|
-
# Each unique +(type, key)+ is also queued for bulk registration on the
|
|
142
|
-
# management API.
|
|
143
181
|
def set_context(contexts, &block)
|
|
144
|
-
|
|
182
|
+
_ensure_started
|
|
183
|
+
@platform.contexts.register(contexts) if contexts && !contexts.empty?
|
|
145
184
|
|
|
146
185
|
scope = Smplkit.set_request_context(contexts || [])
|
|
147
186
|
if block
|
|
@@ -151,19 +190,23 @@ module Smplkit
|
|
|
151
190
|
end
|
|
152
191
|
end
|
|
153
192
|
|
|
193
|
+
# Release all resources held by this client.
|
|
154
194
|
def close
|
|
155
195
|
Smplkit.debug("lifecycle", "Client.close called")
|
|
156
196
|
@closed = true
|
|
157
197
|
@flush_timer&.shutdown
|
|
198
|
+
@flush_timer = nil
|
|
158
199
|
final_flush
|
|
159
200
|
@metrics&.close
|
|
160
201
|
@logging._close
|
|
161
202
|
@flags._close
|
|
162
|
-
@config._close
|
|
163
203
|
@audit._close
|
|
164
204
|
@ws_manager&.stop
|
|
165
205
|
@ws_manager = nil
|
|
166
|
-
|
|
206
|
+
# Close the shared per-service HTTP transports (app/config/flags/logging/
|
|
207
|
+
# jobs). client.platform/account borrow the app transport and close
|
|
208
|
+
# nothing; client.audit owns and closed its own transport above.
|
|
209
|
+
@transports.close
|
|
167
210
|
end
|
|
168
211
|
|
|
169
212
|
# Internal accessors used by sub-clients --------------------------------
|
|
@@ -175,15 +218,29 @@ module Smplkit
|
|
|
175
218
|
def _metrics = @metrics
|
|
176
219
|
def _extra_headers = @extra_headers
|
|
177
220
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
221
|
+
# Start the deferred background machinery exactly once.
|
|
222
|
+
#
|
|
223
|
+
# Idempotent and thread-safe (lock + flag); a no-op after +close+. Triggered
|
|
224
|
+
# by the first config/flags/logging operation, +set_context+,
|
|
225
|
+
# +wait_until_ready+, or WebSocket open — never at construction.
|
|
226
|
+
def _ensure_started
|
|
227
|
+
@start_lock.synchronize do
|
|
228
|
+
return if @started || @closed
|
|
229
|
+
|
|
230
|
+
@started = true
|
|
183
231
|
end
|
|
232
|
+
schedule_periodic_flush
|
|
233
|
+
@init_thread = Thread.new { register_service_context }
|
|
184
234
|
end
|
|
185
235
|
|
|
186
|
-
def
|
|
236
|
+
def _ensure_ws
|
|
237
|
+
_ensure_started
|
|
238
|
+
if @ws_manager.nil?
|
|
239
|
+
@ws_manager = SharedWebSocket.new(app_base_url: @app_base_url, api_key: @api_key, metrics: @metrics)
|
|
240
|
+
@ws_manager.start
|
|
241
|
+
end
|
|
242
|
+
@ws_manager
|
|
243
|
+
end
|
|
187
244
|
|
|
188
245
|
private
|
|
189
246
|
|
|
@@ -198,38 +255,40 @@ module Smplkit
|
|
|
198
255
|
@flush_timer.execute
|
|
199
256
|
end
|
|
200
257
|
|
|
201
|
-
# Extracted as a private method so the timer body is reachable from
|
|
202
|
-
#
|
|
258
|
+
# Extracted as a private method so the timer body is reachable from tests
|
|
259
|
+
# without poking into Concurrent::TimerTask internals.
|
|
203
260
|
def run_periodic_flush
|
|
204
261
|
return if @closed
|
|
205
262
|
|
|
206
|
-
@
|
|
207
|
-
@
|
|
208
|
-
@
|
|
263
|
+
@platform.contexts.flush
|
|
264
|
+
@flags.flush
|
|
265
|
+
@logging.loggers.flush
|
|
266
|
+
@config.flush
|
|
209
267
|
rescue StandardError => e
|
|
210
268
|
Smplkit.debug("registration", "periodic flush failed: #{e.class}: #{e.message}")
|
|
211
269
|
end
|
|
212
270
|
|
|
213
271
|
def final_flush
|
|
214
|
-
[@
|
|
215
|
-
|
|
272
|
+
[@platform.contexts, @flags, @logging.loggers, @config].each do |target|
|
|
273
|
+
target.flush
|
|
216
274
|
rescue StandardError => e
|
|
217
275
|
Smplkit.debug("registration", "final flush failed: #{e.class}: #{e.message}")
|
|
218
276
|
end
|
|
219
277
|
end
|
|
220
278
|
|
|
221
|
-
def register_service_context
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
279
|
+
def register_service_context
|
|
280
|
+
# Register the environment and/or service as context instances. Only the
|
|
281
|
+
# values that are set are registered; if neither environment nor service
|
|
282
|
+
# was provided the POST is skipped entirely (an audit/jobs-only customer
|
|
283
|
+
# has nothing to register).
|
|
226
284
|
contexts = []
|
|
227
|
-
contexts << Smplkit::Context.new("environment",
|
|
228
|
-
contexts << Smplkit::Context.new("service",
|
|
229
|
-
|
|
230
|
-
|
|
285
|
+
contexts << Smplkit::Context.new("environment", @environment) if @environment
|
|
286
|
+
contexts << Smplkit::Context.new("service", @service, { "name" => @service }) if @service
|
|
287
|
+
return if contexts.empty?
|
|
288
|
+
|
|
289
|
+
@platform.contexts.register(contexts, flush: true)
|
|
231
290
|
rescue StandardError => e
|
|
232
|
-
Smplkit.debug("lifecycle", "register service context failed (app: #{
|
|
291
|
+
Smplkit.debug("lifecycle", "register service context failed (app: #{@app_base_url}): #{e.class}: #{e.message}")
|
|
233
292
|
end
|
|
234
293
|
end
|
|
235
294
|
end
|