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
|
@@ -1,94 +1,393 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# The Smpl Logging client — one unified +LoggingClient+.
|
|
4
|
+
#
|
|
5
|
+
# Smpl Logging has two surfaces on a single client, mirroring how the config,
|
|
6
|
+
# flags, audit, and jobs clients expose their full surface from one class:
|
|
7
|
+
#
|
|
8
|
+
# * *Management surface* — works immediately, no +install+ required. Two
|
|
9
|
+
# sub-clients (the audit pattern):
|
|
10
|
+
#
|
|
11
|
+
# * +client.logging.loggers+ — logger CRUD + discovery: +new+ / +list+ / +get+
|
|
12
|
+
# / +delete+ plus +register+ / +flush+ / +flush_sync+ / +pending_count+.
|
|
13
|
+
# * +client.logging.log_groups+ — log-group CRUD: +new+ / +list+ / +get+ /
|
|
14
|
+
# +delete+.
|
|
15
|
+
#
|
|
16
|
+
# The fused client owns the logger-discovery buffer directly; the +loggers+
|
|
17
|
+
# sub-client shares that same buffer so discovery and explicit registration
|
|
18
|
+
# drain through one queue.
|
|
19
|
+
#
|
|
20
|
+
# * *Live surface* — directly on the client. +register_adapter+ is a PRE-install
|
|
21
|
+
# configuration call (allowed before +install+). +install+ opens the live
|
|
22
|
+
# connection (monkey-patches the app's logging framework, discovers loggers,
|
|
23
|
+
# fetches + applies levels, opens the shared WebSocket). +on_change+ /
|
|
24
|
+
# +refresh+ require +install+ first; calling them earlier raises
|
|
25
|
+
# +NotInstalledError+.
|
|
26
|
+
#
|
|
27
|
+
# The client supports two construction shapes:
|
|
28
|
+
#
|
|
29
|
+
# * *Wired* into +Smplkit::Client+ — borrows the parent's logging transport for
|
|
30
|
+
# both runtime fetch and CRUD and the parent's shared WebSocket for the live
|
|
31
|
+
# channel. This is the common path.
|
|
32
|
+
# * *Standalone* — +LoggingClient.new(api_key: ..., base_url: ..., ...)+ builds
|
|
33
|
+
# and owns its own logging transport and an app transport (the WebSocket
|
|
34
|
+
# gateway lives on the app service), and on +install+ opens and owns its own
|
|
35
|
+
# WebSocket. +close+ tears down only the owned transports and owned WebSocket.
|
|
5
36
|
module Smplkit
|
|
6
37
|
module Logging
|
|
7
|
-
|
|
38
|
+
NOT_INSTALLED_MESSAGE = "Smpl Logging live operations require install() first — this opens a live " \
|
|
39
|
+
"connection to your running service and hooks into your application's logging " \
|
|
40
|
+
"framework. Call client.logging.install() before on_change/refresh()."
|
|
41
|
+
|
|
42
|
+
# Build standalone logging + app transports and resolve the app base URL.
|
|
8
43
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
44
|
+
# +base_url+/+api_key+ are used directly when supplied (the path a top-level
|
|
45
|
+
# client takes after it has already resolved them); otherwise the management
|
|
46
|
+
# config resolver fills in whatever is missing (+~/.smplkit+ / env vars /
|
|
47
|
+
# defaults). The app transport is needed for the WebSocket gateway, which
|
|
48
|
+
# lives on the app service (like flags); the app base URL is returned so a
|
|
49
|
+
# standalone client can open its own WebSocket against the event gateway.
|
|
50
|
+
def self.logging_transport(api_key:, base_url:, profile:, base_domain:, scheme:, debug:, extra_headers:)
|
|
51
|
+
cfg = ConfigResolution.resolve_management_config(
|
|
52
|
+
profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme, debug: debug
|
|
53
|
+
)
|
|
54
|
+
resolved_key = api_key.nil? ? cfg.api_key : api_key
|
|
55
|
+
merged = {}
|
|
56
|
+
merged.merge!(cfg.extra_headers || {})
|
|
57
|
+
merged.merge!(extra_headers || {})
|
|
58
|
+
tcfg = ConfigResolution::ResolvedManagementConfig.new(
|
|
59
|
+
api_key: resolved_key, base_domain: cfg.base_domain, scheme: cfg.scheme,
|
|
60
|
+
debug: cfg.debug, extra_headers: merged
|
|
61
|
+
)
|
|
62
|
+
app_url = ConfigResolution.service_url(cfg.scheme, "app", cfg.base_domain)
|
|
63
|
+
logging_http = Transport.build_api_client(SmplkitGeneratedClient::Logging, "logging", tcfg, base_url: base_url)
|
|
64
|
+
app_http = Transport.build_api_client(SmplkitGeneratedClient::App, "app", tcfg)
|
|
65
|
+
[logging_http, app_http, app_url, resolved_key]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Discover and load the SDK's built-in logging adapters.
|
|
69
|
+
def self.auto_load_adapters
|
|
70
|
+
adapters = [Adapters::StdlibLoggerAdapter.new]
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
require "semantic_logger"
|
|
74
|
+
require_relative "adapters/semantic_logger_adapter"
|
|
75
|
+
adapters << Adapters::SemanticLoggerAdapter.new
|
|
76
|
+
rescue LoadError
|
|
77
|
+
Smplkit.debug("registration", "semantic_logger gem not installed; semantic-logger adapter skipped")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
adapters
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
LoggerChangeEvent = Struct.new(:name, :level, :source, keyword_init: true) do
|
|
84
|
+
def ==(other)
|
|
85
|
+
other.is_a?(LoggerChangeEvent) &&
|
|
86
|
+
name == other.name && level == other.level && source == other.source
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Surface for +client.logging.loggers.*+ (sync).
|
|
12
91
|
#
|
|
13
|
-
#
|
|
14
|
-
# and
|
|
15
|
-
#
|
|
16
|
-
|
|
92
|
+
# Logger CRUD plus the discovery buffer. The buffer is owned by the fused
|
|
93
|
+
# +LoggingClient+ and shared here so discovery (driven by
|
|
94
|
+
# +LoggingClient#install+) and explicit +register+ drain through one queue.
|
|
95
|
+
class LoggersClient
|
|
96
|
+
def initialize(http_client, buffer:)
|
|
97
|
+
@api = SmplkitGeneratedClient::Logging::LoggersApi.new(http_client)
|
|
98
|
+
@buffer = buffer
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Buffer logger sources for registration; optionally flush immediately.
|
|
102
|
+
def register(items, flush: false)
|
|
103
|
+
batch = items.is_a?(Array) ? items : [items]
|
|
104
|
+
batch.each do |src|
|
|
105
|
+
@buffer.add(LoggerSource.new(
|
|
106
|
+
name: Normalize.normalize_logger_name(src.name),
|
|
107
|
+
resolved_level: src.resolved_level, level: src.level,
|
|
108
|
+
service: src.service, environment: src.environment
|
|
109
|
+
))
|
|
110
|
+
end
|
|
111
|
+
if flush
|
|
112
|
+
self.flush
|
|
113
|
+
return
|
|
114
|
+
end
|
|
115
|
+
return unless @buffer.pending_count >= LOGGER_BATCH_FLUSH_SIZE
|
|
116
|
+
|
|
117
|
+
Thread.new { threshold_flush }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Drain the buffer and POST pending logger sources to the bulk endpoint.
|
|
121
|
+
def flush
|
|
122
|
+
batch = @buffer.drain
|
|
123
|
+
return if batch.empty?
|
|
124
|
+
|
|
125
|
+
items = batch.map do |entry|
|
|
126
|
+
SmplkitGeneratedClient::Logging::LoggerBulkItem.new(
|
|
127
|
+
id: entry["id"], resolved_level: entry["resolved_level"], level: entry["level"],
|
|
128
|
+
service: entry["service"], environment: entry["environment"]
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
body = SmplkitGeneratedClient::Logging::LoggerBulkRequest.new(loggers: items)
|
|
132
|
+
ApiSupport::ErrorMapping.call { @api.bulk_register_loggers(body) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Synchronous flush — alias of +flush+ for the periodic-flush path.
|
|
136
|
+
def flush_sync
|
|
137
|
+
flush
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Number of sources queued and awaiting flush.
|
|
141
|
+
def pending_count
|
|
142
|
+
@buffer.pending_count
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Return a new unsaved +SmplLogger+. Call +SmplLogger#save+ to persist.
|
|
146
|
+
def new(id, managed: true)
|
|
147
|
+
SmplLogger.new(self, id: id, name: id, resolved_level: nil, managed: managed)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# List loggers for the authenticated account.
|
|
151
|
+
def list(page_number: nil, page_size: nil)
|
|
152
|
+
opts = {}
|
|
153
|
+
opts[:page_number] = page_number unless page_number.nil?
|
|
154
|
+
opts[:page_size] = page_size unless page_size.nil?
|
|
155
|
+
response = ApiSupport::ErrorMapping.call { @api.list_loggers(opts) }
|
|
156
|
+
(response.data || []).map { |r| Helpers.logger_resource_to_model(self, ApiSupport::ResourceShim.from_model(r)) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Fetch the editable +SmplLogger+ resource by id.
|
|
160
|
+
def get(id)
|
|
161
|
+
response = ApiSupport::ErrorMapping.call { @api.get_logger(id) }
|
|
162
|
+
Helpers.logger_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Delete a logger by id.
|
|
166
|
+
def delete(id)
|
|
167
|
+
ApiSupport::ErrorMapping.call { @api.delete_logger(id) }
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def _update_logger(logger)
|
|
172
|
+
response = ApiSupport::ErrorMapping.call { @api.update_logger(logger.id || logger.name, logger_body(logger)) }
|
|
173
|
+
Helpers.logger_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Runtime entry — walks every page and returns an id-keyed Hash of
|
|
177
|
+
# resolution-cache entries (+level+, +group+, +managed+, +environments+).
|
|
178
|
+
def list_logger_entries
|
|
179
|
+
rows = ApiSupport::PaginatedFetch.collect { |opts| @api.list_loggers(opts) }
|
|
180
|
+
rows.to_h { |r| logger_entry_from_resource(ApiSupport::ResourceShim.from_model(r)) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Fetch one logger as a resolution-cache entry. Used by the +logger_changed+
|
|
184
|
+
# WS handler.
|
|
185
|
+
def get_logger_entry(id)
|
|
186
|
+
response = ApiSupport::ErrorMapping.call { @api.get_logger(id) }
|
|
187
|
+
logger_entry_from_resource(ApiSupport::ResourceShim.from_model(response.data))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def threshold_flush
|
|
193
|
+
flush
|
|
194
|
+
rescue StandardError => e
|
|
195
|
+
Smplkit.debug("registration", "logger registration flush failed: #{e.class}: #{e.message}")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def logger_entry_from_resource(resource)
|
|
199
|
+
attrs = resource["attributes"] || {}
|
|
200
|
+
[
|
|
201
|
+
resource["id"],
|
|
202
|
+
{
|
|
203
|
+
"level" => attrs["level"],
|
|
204
|
+
"group" => attrs["group"],
|
|
205
|
+
"managed" => attrs.key?("managed") ? attrs["managed"] : true,
|
|
206
|
+
"environments" => attrs["environments"] || {}
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def logger_body(logger)
|
|
212
|
+
# Logger server schema: name, level, group, managed, environments.
|
|
213
|
+
# +resolved_level+ is read-only, +service+/+environment+ are observed via
|
|
214
|
+
# bulk register, +description+ is wrapper-local. The PUT is a full
|
|
215
|
+
# replace, so the (possibly empty) environments map is always sent —
|
|
216
|
+
# omitting it would leave a cleared override in place server-side.
|
|
217
|
+
SmplkitGeneratedClient::Logging::LoggerResponse.new(
|
|
218
|
+
data: SmplkitGeneratedClient::Logging::LoggerResource.new(
|
|
219
|
+
type: "logger",
|
|
220
|
+
id: logger.id,
|
|
221
|
+
attributes: SmplkitGeneratedClient::Logging::Logger.new(
|
|
222
|
+
name: logger.name,
|
|
223
|
+
level: logger.level&.to_s,
|
|
224
|
+
group: logger.log_group_id,
|
|
225
|
+
managed: logger.managed,
|
|
226
|
+
environments: Logging.environments_to_wire(logger.environments)
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Surface for +client.logging.log_groups.*+ (sync).
|
|
234
|
+
class LogGroupsClient
|
|
235
|
+
def initialize(http_client)
|
|
236
|
+
@api = SmplkitGeneratedClient::Logging::LogGroupsApi.new(http_client)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Return a new unsaved +SmplLogGroup+. Call +SmplLogGroup#save+ to persist.
|
|
240
|
+
def new(id, name: nil, group: nil)
|
|
241
|
+
SmplLogGroup.new(
|
|
242
|
+
self, key: id, name: name.nil? ? Smplkit::Helpers.key_to_display_name(id) : name, parent_id: group
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# List log groups for the authenticated account.
|
|
247
|
+
def list(page_number: nil, page_size: nil)
|
|
248
|
+
opts = {}
|
|
249
|
+
opts[:page_number] = page_number unless page_number.nil?
|
|
250
|
+
opts[:page_size] = page_size unless page_size.nil?
|
|
251
|
+
response = ApiSupport::ErrorMapping.call { @api.list_log_groups(opts) }
|
|
252
|
+
(response.data || []).map do |r|
|
|
253
|
+
Helpers.log_group_resource_to_model(self, ApiSupport::ResourceShim.from_model(r))
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Fetch the editable +SmplLogGroup+ resource by id.
|
|
258
|
+
def get(id)
|
|
259
|
+
response = ApiSupport::ErrorMapping.call { @api.get_log_group(id) }
|
|
260
|
+
Helpers.log_group_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Delete a log group by id.
|
|
264
|
+
def delete(id)
|
|
265
|
+
ApiSupport::ErrorMapping.call { @api.delete_log_group(id) }
|
|
266
|
+
nil
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def _create_log_group(group)
|
|
270
|
+
response = ApiSupport::ErrorMapping.call { @api.create_log_group(log_group_body(group)) }
|
|
271
|
+
Helpers.log_group_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def _update_log_group(group)
|
|
275
|
+
response = ApiSupport::ErrorMapping.call { @api.update_log_group(group.key, log_group_body(group)) }
|
|
276
|
+
Helpers.log_group_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Runtime entry — walks every page and returns an id-keyed Hash of
|
|
280
|
+
# resolution-cache entries (+level+, +group+, +environments+). The +group+
|
|
281
|
+
# key carries the *parent group id* so the resolution algorithm can walk
|
|
282
|
+
# the chain with the same key shape it uses for loggers.
|
|
283
|
+
def list_group_entries
|
|
284
|
+
rows = ApiSupport::PaginatedFetch.collect { |opts| @api.list_log_groups(opts) }
|
|
285
|
+
rows.to_h { |r| group_entry_from_resource(ApiSupport::ResourceShim.from_model(r)) }
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def get_group_entry(key)
|
|
289
|
+
response = ApiSupport::ErrorMapping.call { @api.get_log_group(key) }
|
|
290
|
+
group_entry_from_resource(ApiSupport::ResourceShim.from_model(response.data))
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
private
|
|
294
|
+
|
|
295
|
+
def group_entry_from_resource(resource)
|
|
296
|
+
attrs = resource["attributes"] || {}
|
|
297
|
+
[
|
|
298
|
+
resource["id"],
|
|
299
|
+
{
|
|
300
|
+
"level" => attrs["level"],
|
|
301
|
+
"group" => attrs["parent_id"],
|
|
302
|
+
"environments" => attrs["environments"] || {}
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def log_group_body(group)
|
|
308
|
+
# LogGroup server schema: name, level, parent_id (no description).
|
|
309
|
+
SmplkitGeneratedClient::Logging::LogGroupResponse.new(
|
|
310
|
+
data: SmplkitGeneratedClient::Logging::LogGroupResource.new(
|
|
311
|
+
type: "log_group",
|
|
312
|
+
id: group.key,
|
|
313
|
+
attributes: SmplkitGeneratedClient::Logging::LogGroup.new(
|
|
314
|
+
name: group.name,
|
|
315
|
+
level: group.level&.to_s,
|
|
316
|
+
parent_id: group.parent_id
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# The Smpl Logging client (sync).
|
|
324
|
+
#
|
|
325
|
+
# One client exposes the full surface, reachable as +client.logging+
|
|
326
|
+
# (+Smplkit::Client+) or constructed directly:
|
|
327
|
+
#
|
|
328
|
+
# logging = Smplkit::LoggingClient.new(environment: "production", service: "my-svc")
|
|
329
|
+
# logging.loggers.new("sqlalchemy.engine").save
|
|
330
|
+
# logging.install
|
|
17
331
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# loggers' effective levels invokes the global listener N times (once
|
|
23
|
-
# per logger), each invocation also fires every matching key-scoped
|
|
24
|
-
# listener for that id. There are no batch / summary events and no
|
|
25
|
-
# deletion-flavored events — logger / group deletions only emit
|
|
26
|
-
# listener invocations for *dependents* whose computed effective level
|
|
27
|
-
# actually moved; the deleted key itself emits nothing.
|
|
332
|
+
# The management surface (+loggers+ / +log_groups+ sub-clients) works
|
|
333
|
+
# immediately. +register_adapter+ is a pre-install configuration call. The
|
|
334
|
+
# live surface (+install+ / +on_change+ / +refresh+) requires +install+
|
|
335
|
+
# first; calling +on_change+ / +refresh+ earlier raises +NotInstalledError+.
|
|
28
336
|
class LoggingClient
|
|
29
|
-
|
|
337
|
+
attr_reader :loggers, :log_groups
|
|
338
|
+
|
|
339
|
+
def initialize(api_key = nil, environment: nil, base_url: nil, profile: nil,
|
|
340
|
+
base_domain: nil, scheme: nil, debug: nil, extra_headers: nil,
|
|
341
|
+
parent: nil, transport: nil, metrics: nil)
|
|
30
342
|
@parent = parent
|
|
31
|
-
@manage = manage
|
|
32
343
|
@metrics = metrics
|
|
33
|
-
@
|
|
34
|
-
@
|
|
35
|
-
@
|
|
36
|
-
|
|
344
|
+
@environment = parent.nil? ? environment : parent._environment
|
|
345
|
+
@service = parent&._service
|
|
346
|
+
@standalone_api_key = nil
|
|
347
|
+
if transport.nil?
|
|
348
|
+
@logging_http, _app_http, @app_base_url, @standalone_api_key = Logging.logging_transport(
|
|
349
|
+
api_key: api_key, base_url: base_url, profile: profile,
|
|
350
|
+
base_domain: base_domain, scheme: scheme, debug: debug, extra_headers: extra_headers
|
|
351
|
+
)
|
|
352
|
+
else
|
|
353
|
+
@logging_http = transport
|
|
354
|
+
@app_base_url = nil
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Discovery buffer is owned by this client; the loggers sub-client shares
|
|
358
|
+
# it so discovery and explicit registration drain together.
|
|
359
|
+
@buffer = LoggerRegistrationBuffer.new
|
|
360
|
+
@loggers = LoggersClient.new(@logging_http, buffer: @buffer)
|
|
361
|
+
@log_groups = LogGroupsClient.new(@logging_http)
|
|
362
|
+
|
|
363
|
+
# Live-surface state.
|
|
364
|
+
@connected = false
|
|
365
|
+
@name_map = {} # original_name → normalized_id
|
|
366
|
+
@loggers_cache = {} # id → logger data
|
|
367
|
+
@groups_cache = {} # id → group data
|
|
37
368
|
@global_listeners = []
|
|
38
369
|
@key_listeners = Hash.new { |h, k| h[k] = [] }
|
|
370
|
+
@adapters = []
|
|
371
|
+
@explicit_adapters = false
|
|
372
|
+
@ws_manager = nil
|
|
373
|
+
@owns_ws = false
|
|
39
374
|
@lock = Mutex.new
|
|
40
|
-
# original_name → normalized_id for every adapter-discovered logger.
|
|
41
|
-
# We keep originals so adapter.apply_level receives whatever the
|
|
42
|
-
# framework's registry indexes by.
|
|
43
|
-
@name_map = {}
|
|
44
|
-
# normalized_id → resolution-cache entry.
|
|
45
|
-
@loggers_cache = {}
|
|
46
|
-
# group id → resolution-cache entry. Without this, any managed
|
|
47
|
-
# logger with +level=null+ that inherits from a group silently
|
|
48
|
-
# keeps whatever level its adapter had at startup.
|
|
49
|
-
@groups_cache = {}
|
|
50
|
-
# normalized_id → last-applied resolved level (string). Drives the
|
|
51
|
-
# lockstep between adapter.apply_level and listener notifications:
|
|
52
|
-
# we only push (and fire) when the freshly-resolved level differs
|
|
53
|
-
# from what's recorded here.
|
|
54
|
-
@resolved_levels = {}
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Install the logging integration.
|
|
58
|
-
#
|
|
59
|
-
# Auto-loads the +stdlib-logger+ adapter (always) and the
|
|
60
|
-
# +semantic-logger+ adapter (when the gem is available). Customer
|
|
61
|
-
# explicit registration via +register_adapter+ wins over auto-load.
|
|
62
|
-
def install
|
|
63
|
-
return self if @installed
|
|
64
|
-
|
|
65
|
-
auto_load_adapters if @adapters.empty?
|
|
66
|
-
|
|
67
|
-
@adapters.each do |adapter|
|
|
68
|
-
discovered = adapter.discover
|
|
69
|
-
discovered.each { |(name, _explicit, effective)| observe_logger(adapter, name, effective) }
|
|
70
|
-
adapter.install_hook { |name, _explicit, effective| observe_logger(adapter, name, effective) }
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
flush_initial_registration
|
|
74
|
-
fetch_and_apply(trigger: "install", source: "manual")
|
|
75
|
-
|
|
76
|
-
@ws_manager = @parent._ensure_ws
|
|
77
|
-
@ws_manager.on("logger_changed") { |data| handle_logger_changed(data) }
|
|
78
|
-
@ws_manager.on("logger_deleted") { |data| handle_logger_deleted(data) }
|
|
79
|
-
@ws_manager.on("group_changed") { |data| handle_group_changed(data) }
|
|
80
|
-
@ws_manager.on("group_deleted") { |data| handle_group_deleted(data) }
|
|
81
|
-
@ws_manager.on("loggers_changed") { |data| handle_loggers_changed(data) }
|
|
82
|
-
@installed = true
|
|
83
|
-
self
|
|
84
375
|
end
|
|
85
|
-
alias start install
|
|
86
376
|
|
|
377
|
+
# --- Adapter registration (pre-install, ungated) ---
|
|
378
|
+
|
|
379
|
+
# Register a logging adapter. Must be called before install().
|
|
380
|
+
#
|
|
381
|
+
# If called at least once, auto-loading is disabled — only explicitly
|
|
382
|
+
# registered adapters are used. This is a pre-install configuration call:
|
|
383
|
+
# it is intentionally NOT gated by +install+.
|
|
87
384
|
def register_adapter(adapter)
|
|
385
|
+
raise "Cannot register adapters after install()" if @connected
|
|
88
386
|
unless adapter.is_a?(Adapters::Base)
|
|
89
387
|
raise ArgumentError, "adapter must implement Smplkit::Logging::Adapters::Base"
|
|
90
388
|
end
|
|
91
389
|
|
|
390
|
+
@explicit_adapters = true
|
|
92
391
|
@adapters << adapter
|
|
93
392
|
self
|
|
94
393
|
end
|
|
@@ -97,197 +396,353 @@ module Smplkit
|
|
|
97
396
|
@adapters.dup
|
|
98
397
|
end
|
|
99
398
|
|
|
100
|
-
|
|
101
|
-
@manage.loggers.get(name)
|
|
102
|
-
end
|
|
399
|
+
# --- Live surface: install (gate) + transport / WebSocket helpers ---
|
|
103
400
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
401
|
+
# Hook smplkit into the application's logging machinery.
|
|
402
|
+
#
|
|
403
|
+
# Loads adapters, scans existing loggers, applies levels from the smplkit
|
|
404
|
+
# server, and wires WebSocket handlers for live updates. This IS the
|
|
405
|
+
# explicit consent gate — +on_change+ / +refresh+ require it first.
|
|
406
|
+
#
|
|
407
|
+
# Idempotent — safe to call multiple times.
|
|
408
|
+
def install
|
|
409
|
+
Smplkit.debug("lifecycle", "LoggingClient.install() called")
|
|
410
|
+
@parent&._ensure_started
|
|
411
|
+
return self if @connected
|
|
107
412
|
|
|
108
|
-
|
|
109
|
-
@
|
|
110
|
-
end
|
|
413
|
+
# 0. Load adapters
|
|
414
|
+
@adapters = Logging.auto_load_adapters if @adapters.empty?
|
|
111
415
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
416
|
+
# 1. Discover existing loggers from all adapters (keep discovery and hook
|
|
417
|
+
# installation as two passes — discover every adapter before any hook is
|
|
418
|
+
# live, mirroring the Python SDK).
|
|
419
|
+
# rubocop:disable Style/CombinableLoops
|
|
420
|
+
@adapters.each do |adapter|
|
|
421
|
+
existing = adapter.discover
|
|
422
|
+
existing.each do |name, explicit_level, effective_level|
|
|
423
|
+
@name_map[name] = Normalize.normalize_logger_name(name)
|
|
424
|
+
@loggers.register(loggersource_for(name, explicit_level, effective_level))
|
|
425
|
+
end
|
|
426
|
+
rescue StandardError => e
|
|
427
|
+
Smplkit.debug("logging", "adapter #{adapter.name} discover failed: #{e.class}: #{e.message}")
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# 2. Install continuous discovery hooks
|
|
431
|
+
@adapters.each do |adapter|
|
|
432
|
+
adapter.install_hook { |name, explicit, effective| on_new_logger(name, explicit, effective) }
|
|
433
|
+
rescue StandardError => e
|
|
434
|
+
Smplkit.debug("logging", "adapter #{adapter.name} install_hook failed: #{e.class}: #{e.message}")
|
|
435
|
+
end
|
|
436
|
+
# rubocop:enable Style/CombinableLoops
|
|
437
|
+
|
|
438
|
+
# 3. Flush initial batch
|
|
439
|
+
begin
|
|
440
|
+
@loggers.flush
|
|
441
|
+
rescue StandardError => e
|
|
442
|
+
Smplkit.debug("registration", "bulk logger registration failed: #{e.class}: #{e.message}")
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# 4-6. Fetch, resolve, apply
|
|
446
|
+
begin
|
|
447
|
+
fetch_and_apply(trigger: "install()")
|
|
448
|
+
rescue StandardError => e
|
|
449
|
+
Smplkit.debug("resolution",
|
|
450
|
+
"failed to fetch/apply logging levels during connect " \
|
|
451
|
+
"(logging: #{@logging_http&.config&.host}): #{e.class}: #{e.message}")
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# 7. Register WebSocket event handlers for real-time level updates
|
|
455
|
+
@ws_manager = ensure_ws
|
|
456
|
+
ws_handlers.each { |event, handler| @ws_manager.on(event, &handler) }
|
|
457
|
+
|
|
458
|
+
@connected = true
|
|
459
|
+
self
|
|
116
460
|
end
|
|
117
461
|
|
|
462
|
+
# --- Live surface: change listeners ---
|
|
463
|
+
|
|
464
|
+
# Register a change listener.
|
|
465
|
+
#
|
|
466
|
+
# client.logging.on_change { |event| ... } # global
|
|
467
|
+
# client.logging.on_change("sqlalchemy.engine") { |e| ... } # key-scoped
|
|
468
|
+
#
|
|
469
|
+
# Requires +install+ first; raises +NotInstalledError+ otherwise.
|
|
118
470
|
def on_change(name = nil, &block)
|
|
471
|
+
require_installed
|
|
119
472
|
raise ArgumentError, "on_change requires a block" unless block
|
|
120
473
|
|
|
121
474
|
if name.nil?
|
|
122
475
|
@global_listeners << block
|
|
123
476
|
else
|
|
124
|
-
@key_listeners[
|
|
477
|
+
@key_listeners[name] << block
|
|
125
478
|
end
|
|
126
479
|
block
|
|
127
480
|
end
|
|
128
481
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
482
|
+
# Re-fetch all loggers and groups and fire listener events for any deltas.
|
|
483
|
+
#
|
|
484
|
+
# Requires +install+ first; raises +NotInstalledError+ otherwise.
|
|
485
|
+
def refresh
|
|
486
|
+
require_installed
|
|
487
|
+
Smplkit.debug("resolution", "refresh() called, triggering full resolution pass")
|
|
488
|
+
fetch_and_apply_deltas(trigger: "refresh()", source: "manual")
|
|
132
489
|
end
|
|
133
490
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
491
|
+
# Release resources — only those this client owns.
|
|
492
|
+
#
|
|
493
|
+
# Uninstalls the adapter hooks, unsubscribes from the WebSocket, and tears
|
|
494
|
+
# down the owned WebSocket (standalone install). A wired client borrows the
|
|
495
|
+
# parent's transport and WebSocket and closes neither.
|
|
496
|
+
def close
|
|
497
|
+
Smplkit.debug("lifecycle", "LoggingClient.close() called")
|
|
498
|
+
@adapters.each do |adapter|
|
|
499
|
+
adapter.uninstall_hook
|
|
500
|
+
rescue StandardError => e
|
|
501
|
+
Smplkit.debug("logging", "adapter #{adapter.name} uninstall_hook failed: #{e.class}: #{e.message}")
|
|
502
|
+
end
|
|
503
|
+
if @ws_manager
|
|
504
|
+
ws_handlers.each { |event, handler| @ws_manager.off(event, handler) }
|
|
505
|
+
if @owns_ws
|
|
506
|
+
@ws_manager.stop
|
|
507
|
+
@owns_ws = false
|
|
508
|
+
end
|
|
509
|
+
@ws_manager = nil
|
|
510
|
+
end
|
|
511
|
+
@connected = false
|
|
512
|
+
end
|
|
513
|
+
alias _close close
|
|
138
514
|
|
|
515
|
+
# Construct, yield to the block, and close on exit.
|
|
516
|
+
def self.open(**kwargs)
|
|
517
|
+
client = new(**kwargs)
|
|
139
518
|
begin
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
rescue LoadError
|
|
144
|
-
Smplkit.debug("registration", "semantic_logger gem not installed; semantic-logger adapter skipped")
|
|
519
|
+
yield client
|
|
520
|
+
ensure
|
|
521
|
+
client.close
|
|
145
522
|
end
|
|
523
|
+
end
|
|
146
524
|
|
|
147
|
-
|
|
525
|
+
private
|
|
148
526
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
# constructible.
|
|
152
|
-
# :nocov:
|
|
153
|
-
Smplkit.debug("registration", "no logging adapters loaded; runtime features disabled")
|
|
154
|
-
# :nocov:
|
|
527
|
+
def require_installed
|
|
528
|
+
raise NotInstalledError, NOT_INSTALLED_MESSAGE unless @connected
|
|
155
529
|
end
|
|
156
530
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
531
|
+
# Memoized event → handler map so +install+ registers and +close+
|
|
532
|
+
# unsubscribes the exact same callback objects. They are stored as procs
|
|
533
|
+
# (not bound methods) because +SharedWebSocket#off+ removes by object
|
|
534
|
+
# identity, and +on(event, &proc)+ stores the very proc passed here.
|
|
535
|
+
def ws_handlers
|
|
536
|
+
@ws_handlers ||= {
|
|
537
|
+
"logger_changed" => proc { |data| handle_logger_changed(data) },
|
|
538
|
+
"logger_deleted" => proc { |data| handle_logger_deleted(data) },
|
|
539
|
+
"group_changed" => proc { |data| handle_group_changed(data) },
|
|
540
|
+
"group_deleted" => proc { |data| handle_group_deleted(data) },
|
|
541
|
+
"loggers_changed" => proc { |data| handle_loggers_changed(data) }
|
|
542
|
+
}
|
|
167
543
|
end
|
|
168
544
|
|
|
169
|
-
def
|
|
170
|
-
@
|
|
171
|
-
|
|
172
|
-
|
|
545
|
+
def ensure_ws
|
|
546
|
+
return @parent._ensure_ws unless @parent.nil?
|
|
547
|
+
|
|
548
|
+
if @ws_manager.nil?
|
|
549
|
+
@ws_manager = SharedWebSocket.new(
|
|
550
|
+
app_base_url: @app_base_url, api_key: @standalone_api_key, metrics: @metrics
|
|
551
|
+
)
|
|
552
|
+
@ws_manager.start
|
|
553
|
+
@owns_ws = true
|
|
554
|
+
end
|
|
555
|
+
@ws_manager
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# --- Internal ---
|
|
559
|
+
|
|
560
|
+
# Build a LoggerSource from an adapter's (name, explicit, effective)
|
|
561
|
+
# discovery tuple.
|
|
562
|
+
def loggersource_for(name, explicit_level, effective_level)
|
|
563
|
+
LoggerSource.new(
|
|
564
|
+
name: name,
|
|
565
|
+
resolved_level: effective_level,
|
|
566
|
+
level: explicit_level,
|
|
567
|
+
service: @service,
|
|
568
|
+
environment: @environment
|
|
569
|
+
)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Callback from adapters when a new logger is created.
|
|
573
|
+
def on_new_logger(name, explicit_level, effective_level)
|
|
574
|
+
normalized = Normalize.normalize_logger_name(name)
|
|
575
|
+
@name_map[name] = normalized
|
|
576
|
+
@loggers.register(loggersource_for(name, explicit_level, effective_level))
|
|
577
|
+
|
|
578
|
+
# If connected, try to apply level from cache.
|
|
579
|
+
return unless @connected && @loggers_cache.key?(normalized)
|
|
580
|
+
|
|
581
|
+
entry = @loggers_cache[normalized]
|
|
582
|
+
return unless entry["managed"]
|
|
583
|
+
|
|
584
|
+
resolved = Resolution.resolve_level(normalized, @environment, @loggers_cache, @groups_cache)
|
|
585
|
+
coerced = LogLevel.coerce(resolved)
|
|
586
|
+
push_to_adapters(name, coerced)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def push_to_adapters(original_name, coerced_level)
|
|
590
|
+
@adapters.each do |adapter|
|
|
591
|
+
adapter.apply_level(original_name, coerced_level)
|
|
592
|
+
rescue StandardError => e
|
|
593
|
+
Smplkit.debug("logging", "adapter #{adapter.name} apply_level failed for #{original_name}: " \
|
|
594
|
+
"#{e.class}: #{e.message}")
|
|
595
|
+
end
|
|
173
596
|
end
|
|
174
597
|
|
|
175
|
-
#
|
|
176
|
-
def
|
|
598
|
+
# Re-fetch loggers/groups into the cache (no apply, no fire).
|
|
599
|
+
def fetch_cache(trigger)
|
|
177
600
|
Smplkit.debug("resolution", "full resolution pass starting (trigger: #{trigger})")
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
|
|
601
|
+
@loggers_cache = @loggers.list_logger_entries
|
|
602
|
+
@groups_cache = @log_groups.list_group_entries
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Fetch loggers/groups and unconditionally apply levels (initial install
|
|
606
|
+
# path).
|
|
607
|
+
#
|
|
608
|
+
# Silent — does not fire change-listener events. Use
|
|
609
|
+
# +fetch_and_apply_deltas+ from the WS / refresh paths to get per-logger
|
|
610
|
+
# fanout.
|
|
611
|
+
def fetch_and_apply(trigger: "unknown")
|
|
612
|
+
fetch_cache(trigger)
|
|
613
|
+
apply_levels
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Fetch loggers/groups; apply + fire listeners only on effective-level
|
|
617
|
+
# deltas.
|
|
618
|
+
def fetch_and_apply_deltas(trigger:, source:)
|
|
619
|
+
pre = snapshot_effective_levels
|
|
620
|
+
fetch_cache(trigger)
|
|
621
|
+
apply_deltas_and_fire(pre, source)
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Apply resolved levels to all managed, locally-present loggers.
|
|
625
|
+
def apply_levels
|
|
626
|
+
@name_map.each do |original_name, normalized_id|
|
|
196
627
|
entry = @loggers_cache[normalized_id]
|
|
197
628
|
next if entry.nil?
|
|
198
629
|
next unless entry["managed"]
|
|
199
630
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
631
|
+
resolved = Resolution.resolve_level(normalized_id, @environment, @loggers_cache, @groups_cache)
|
|
632
|
+
push_to_adapters(original_name, LogLevel.coerce(resolved))
|
|
633
|
+
@metrics&.record("logging.level_changes", unit: "changes", dimensions: { "logger" => normalized_id })
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Effective level for every locally-tracked managed logger.
|
|
638
|
+
#
|
|
639
|
+
# This is the universe of loggers the adapter applies levels to — the only
|
|
640
|
+
# loggers whose listener can fire. A logger not in +name_map+ (never
|
|
641
|
+
# instantiated locally) or marked +managed=false+ in the cache is excluded.
|
|
642
|
+
def snapshot_effective_levels
|
|
643
|
+
snapshot = {}
|
|
644
|
+
@name_map.each_value do |normalized_id|
|
|
645
|
+
entry = @loggers_cache[normalized_id]
|
|
646
|
+
next if entry.nil? || !entry["managed"]
|
|
205
647
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
fire_change_event(normalized_id, coerced, source: source)
|
|
648
|
+
snapshot[normalized_id] = Resolution.resolve_level(
|
|
649
|
+
normalized_id, @environment, @loggers_cache, @groups_cache
|
|
650
|
+
)
|
|
210
651
|
end
|
|
652
|
+
snapshot
|
|
211
653
|
end
|
|
212
654
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
655
|
+
# Apply + fire per-logger whenever the effective level moved.
|
|
656
|
+
#
|
|
657
|
+
# For every locally-tracked managed logger, recompute the effective level
|
|
658
|
+
# and compare to +pre+. On a delta: call +apply_level+ on every adapter AND
|
|
659
|
+
# fire one +LoggerChangeEvent+ per affected logger — once to each matching
|
|
660
|
+
# key-scoped listener and once to every global listener (a global is
|
|
661
|
+
# semantically a key-scoped subscription on every logger). No-op when
|
|
662
|
+
# nothing moved: no apply, no fire.
|
|
663
|
+
def apply_deltas_and_fire(pre, source)
|
|
664
|
+
@name_map.each do |original_name, normalized_id|
|
|
665
|
+
entry = @loggers_cache[normalized_id]
|
|
666
|
+
next if entry.nil? || !entry["managed"]
|
|
667
|
+
|
|
668
|
+
new_level = Resolution.resolve_level(normalized_id, @environment, @loggers_cache, @groups_cache)
|
|
669
|
+
next if pre[normalized_id] == new_level
|
|
670
|
+
|
|
671
|
+
push_to_adapters(original_name, LogLevel.coerce(new_level))
|
|
672
|
+
@metrics&.record("logging.level_changes", unit: "changes", dimensions: { "logger" => normalized_id })
|
|
673
|
+
fire_for_logger(normalized_id, new_level, source)
|
|
218
674
|
end
|
|
219
675
|
end
|
|
220
676
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
677
|
+
# Fire one +LoggerChangeEvent+ to every matching subscriber.
|
|
678
|
+
#
|
|
679
|
+
# Both the key-scoped listeners registered for +logger_id+ and every global
|
|
680
|
+
# listener receive the same payload.
|
|
681
|
+
def fire_for_logger(logger_id, level, source)
|
|
682
|
+
event = LoggerChangeEvent.new(name: logger_id, level: level, source: source)
|
|
683
|
+
(@global_listeners + @key_listeners[logger_id]).each do |cb|
|
|
224
684
|
cb.call(event)
|
|
225
685
|
rescue StandardError => e
|
|
226
686
|
Smplkit.debug("logging", "listener raised: #{e.class}: #{e.message}")
|
|
227
687
|
end
|
|
228
688
|
end
|
|
229
689
|
|
|
230
|
-
|
|
231
|
-
key = data["id"] || data["name"] || ""
|
|
232
|
-
normalized = Normalize.normalize_logger_name(key)
|
|
233
|
-
return if normalized.empty?
|
|
690
|
+
# --- Internal: event handlers (called by SharedWebSocket) ---
|
|
234
691
|
|
|
692
|
+
def handle_logger_changed(data)
|
|
693
|
+
key = data["id"] || ""
|
|
694
|
+
Smplkit.debug("websocket", "logger_changed: fetching logger #{key.inspect}")
|
|
695
|
+
pre = snapshot_effective_levels
|
|
235
696
|
begin
|
|
236
|
-
entry_id, entry = @
|
|
237
|
-
@loggers_cache[entry_id ||
|
|
697
|
+
entry_id, entry = @loggers.get_logger_entry(key)
|
|
698
|
+
@loggers_cache[entry_id || key] = entry
|
|
238
699
|
rescue StandardError => e
|
|
239
|
-
Smplkit.debug("websocket", "
|
|
700
|
+
Smplkit.debug("websocket", "failed to fetch logger #{key.inspect} after WS event: #{e.class}: #{e.message}")
|
|
240
701
|
return
|
|
241
702
|
end
|
|
242
|
-
|
|
243
|
-
apply_levels(source: "websocket")
|
|
703
|
+
apply_deltas_and_fire(pre, "websocket")
|
|
244
704
|
end
|
|
245
705
|
|
|
246
|
-
# Deletion is a pure cache eviction. The deleted key itself fires
|
|
247
|
-
# nothing; dependents whose effective level moves fire through the
|
|
248
|
-
# normal apply path.
|
|
249
706
|
def handle_logger_deleted(data)
|
|
250
|
-
key = data["id"] ||
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
apply_levels(source: "websocket")
|
|
707
|
+
key = data["id"] || ""
|
|
708
|
+
Smplkit.debug("websocket", "logger_deleted: removing logger #{key.inspect}")
|
|
709
|
+
pre = snapshot_effective_levels
|
|
710
|
+
@loggers_cache.delete(key)
|
|
711
|
+
apply_deltas_and_fire(pre, "websocket")
|
|
256
712
|
end
|
|
257
713
|
|
|
258
714
|
def handle_group_changed(data)
|
|
259
|
-
key = data["id"] ||
|
|
260
|
-
|
|
261
|
-
|
|
715
|
+
key = data["id"] || ""
|
|
716
|
+
Smplkit.debug("websocket", "group_changed: fetching group #{key.inspect}")
|
|
717
|
+
pre = snapshot_effective_levels
|
|
262
718
|
begin
|
|
263
|
-
entry_id, entry = @
|
|
719
|
+
entry_id, entry = @log_groups.get_group_entry(key)
|
|
264
720
|
@groups_cache[entry_id || key] = entry
|
|
265
721
|
rescue StandardError => e
|
|
266
|
-
Smplkit.debug("websocket",
|
|
722
|
+
Smplkit.debug("websocket",
|
|
723
|
+
"failed to fetch log group #{key.inspect} after WS event: #{e.class}: #{e.message}")
|
|
267
724
|
return
|
|
268
725
|
end
|
|
269
|
-
|
|
270
|
-
apply_levels(source: "websocket")
|
|
726
|
+
apply_deltas_and_fire(pre, "websocket")
|
|
271
727
|
end
|
|
272
728
|
|
|
273
729
|
def handle_group_deleted(data)
|
|
274
|
-
key = data["id"] ||
|
|
275
|
-
|
|
276
|
-
|
|
730
|
+
key = data["id"] || ""
|
|
731
|
+
Smplkit.debug("websocket", "group_deleted: removing group #{key.inspect}")
|
|
732
|
+
pre = snapshot_effective_levels
|
|
277
733
|
@groups_cache.delete(key)
|
|
278
|
-
|
|
734
|
+
apply_deltas_and_fire(pre, "websocket")
|
|
279
735
|
end
|
|
280
736
|
|
|
281
737
|
def handle_loggers_changed(_data)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def ==(other)
|
|
288
|
-
other.is_a?(LoggerChangeEvent) &&
|
|
289
|
-
name == other.name && level == other.level && source == other.source
|
|
738
|
+
Smplkit.debug("websocket", "loggers_changed: full re-fetch")
|
|
739
|
+
fetch_and_apply_deltas(trigger: "loggers_changed WS event", source: "websocket")
|
|
740
|
+
rescue StandardError => e
|
|
741
|
+
Smplkit.debug("websocket",
|
|
742
|
+
"failed to re-fetch/apply logging levels after loggers_changed event: #{e.class}: #{e.message}")
|
|
290
743
|
end
|
|
291
744
|
end
|
|
292
745
|
end
|
|
746
|
+
|
|
747
|
+
LoggingClient = Logging::LoggingClient
|
|
293
748
|
end
|