smplkit 3.0.14 → 3.0.16

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c96e2ae58a16657eb4fb39372c370ca2a49671fcb7414d098a4f6e0019a5be9e
4
- data.tar.gz: d2a539076441031f5b476bf56bfd5ca06c70ebca6754db13fded0bcaafc68c4a
3
+ metadata.gz: 215caf14863d35bebf83849e104befe12423a9ef468a547cb44714e6a9ea1932
4
+ data.tar.gz: 928ab9e89ea2dca1548eb82a06aa8d9b525f874026437bb6b862b4cdc254a4c9
5
5
  SHA512:
6
- metadata.gz: 41fdd36eedf126ba56313482c3fc568f370f0537b49057a62716e446c05fee96083d12c28ba728f1c2d241147e9e8844de17fe62a7ecc8d4ae88b613a7f26328
7
- data.tar.gz: 17067605a53e4faff4b07b61bc29ada2292ec41f369edd05be41ff30c6ffce8e2ffee1d9148d7ca9f6619683a90f6e4a347c424aaac34b091424c34553299378
6
+ metadata.gz: 1f77af115d5412a34711fb54af8de0c402e47f33e53512fb808794a103a046d514494d947a6ed9d7d42998d9aca3f196db433914de52f96b785e3b35e5c63972
7
+ data.tar.gz: d028dd314e06e87fc6185f80695ba0c4bdf21b5d77d548c62e32bc2c4138590f7566c327cc83946191c26e233be9b0d923d1ba0d3d6fb59f835f91ca9f6f3bfa
@@ -9,6 +9,11 @@ module Smplkit
9
9
  # Obtained via +Smplkit::Client#logging+. Manages the discovery and level
10
10
  # application for a customer's logging frameworks via pluggable adapters.
11
11
  # CRUD has moved to +mgmt.loggers.*+ / +mgmt.log_groups.*+.
12
+ #
13
+ # Level resolution is client-side: the server stores raw configuration
14
+ # and the SDK walks the chain (env override → base → group chain →
15
+ # dot-notation ancestry) to compute each managed logger's effective
16
+ # level. See {Smplkit::Logging::Resolution}.
12
17
  class LoggingClient
13
18
  def initialize(parent, manage:, metrics:, logging_base_url:, app_base_url:)
14
19
  @parent = parent
@@ -21,6 +26,23 @@ module Smplkit
21
26
  @global_listeners = []
22
27
  @key_listeners = Hash.new { |h, k| h[k] = [] }
23
28
  @lock = Mutex.new
29
+ # original_name → normalized_id for every adapter-discovered logger.
30
+ # We keep originals so adapter.apply_level receives whatever the
31
+ # framework's registry indexes by.
32
+ @name_map = {}
33
+ # normalized_id → resolution-cache entry. Populated by
34
+ # +_fetch_and_apply+ and mutated by the +logger_changed+ /
35
+ # +logger_deleted+ WS handlers.
36
+ @loggers_cache = {}
37
+ # group id → resolution-cache entry. Without this, any managed
38
+ # logger with +level=null+ that inherits from a group silently
39
+ # keeps whatever level its adapter had at startup.
40
+ @groups_cache = {}
41
+ # normalized_id → resolved level (string). Used to decide whether
42
+ # to fire change listeners on a re-resolution — a group-driven
43
+ # change isn't visible in the raw +loggers_cache+ but moves the
44
+ # resolved value.
45
+ @resolved_levels = {}
24
46
  end
25
47
 
26
48
  # Install the logging integration.
@@ -39,8 +61,15 @@ module Smplkit
39
61
  adapter.install_hook { |name, _explicit, effective| observe_logger(adapter, name, effective) }
40
62
  end
41
63
 
64
+ flush_initial_registration
65
+ fetch_and_apply(trigger: "install")
66
+
42
67
  @ws_manager = @parent._ensure_ws
43
68
  @ws_manager.on("logger_changed") { |data| handle_logger_changed(data) }
69
+ @ws_manager.on("logger_deleted") { |data| handle_logger_deleted(data) }
70
+ @ws_manager.on("group_changed") { |data| handle_group_changed(data) }
71
+ @ws_manager.on("group_deleted") { |data| handle_group_deleted(data) }
72
+ @ws_manager.on("loggers_changed") { |data| handle_loggers_changed(data) }
44
73
  @installed = true
45
74
  self
46
75
  end
@@ -63,14 +92,20 @@ module Smplkit
63
92
  @manage.loggers.get(name)
64
93
  end
65
94
 
66
- def list
67
- @manage.loggers.list
95
+ def list(page_number: nil, page_size: nil)
96
+ @manage.loggers.list(page_number: page_number, page_size: page_size)
68
97
  end
69
98
 
70
99
  def delete(name)
71
100
  @manage.loggers.delete(name)
72
101
  end
73
102
 
103
+ # Re-fetch all loggers and groups and re-apply resolved levels. Fires
104
+ # change listeners for any logger whose resolved level moved.
105
+ def refresh
106
+ fetch_and_apply(trigger: "refresh")
107
+ end
108
+
74
109
  def on_change(name = nil, &block)
75
110
  raise ArgumentError, "on_change requires a block" unless block
76
111
 
@@ -102,15 +137,17 @@ module Smplkit
102
137
 
103
138
  return unless @adapters.empty?
104
139
 
105
- # :nocov: defensive log — unreachable in practice because stdlib
106
- # +Logger+ is always present, so +StdlibLoggerAdapter+ is always
140
+ # Defensive log — unreachable in practice because stdlib +Logger+
141
+ # is always present, so +StdlibLoggerAdapter+ is always
107
142
  # constructible.
143
+ # :nocov:
108
144
  Smplkit.debug("registration", "no logging adapters loaded; runtime features disabled")
109
145
  # :nocov:
110
146
  end
111
147
 
112
148
  def observe_logger(_adapter, raw_name, level)
113
149
  normalized = Normalize.normalize_logger_name(raw_name)
150
+ @name_map[raw_name] = normalized
114
151
  @manage.loggers.register(LoggerSource.new(
115
152
  name: normalized,
116
153
  resolved_level: level,
@@ -120,16 +157,135 @@ module Smplkit
120
157
  ))
121
158
  end
122
159
 
160
+ def flush_initial_registration
161
+ @manage.loggers.flush
162
+ rescue StandardError => e
163
+ Smplkit.debug("registration", "initial logger flush failed: #{e.class}: #{e.message}")
164
+ end
165
+
166
+ # Full re-fetch of loggers + groups, then apply resolved levels.
167
+ # Fires change listeners for any logger whose resolved level moved.
168
+ def fetch_and_apply(trigger:)
169
+ Smplkit.debug("resolution", "full resolution pass starting (trigger: #{trigger})")
170
+ loggers = @manage.loggers.list_logger_entries
171
+ groups = @manage.log_groups.list_group_entries
172
+ @loggers_cache = loggers
173
+ @groups_cache = groups
174
+ apply_levels(source: "websocket")
175
+ rescue StandardError => e
176
+ Smplkit.debug("resolution", "fetch_and_apply failed (trigger: #{trigger}): #{e.class}: #{e.message}")
177
+ end
178
+
179
+ # Resolve the effective level for every locally-known managed logger
180
+ # and push it to every adapter. Returns the list of normalized ids
181
+ # whose resolved level changed.
182
+ #
183
+ # +source+ is the +LoggerChangeEvent#source+ for any change-listener
184
+ # event we fire. The default reflects callers that arrived through a
185
+ # server event (WebSocket).
186
+ def apply_levels(source: "websocket")
187
+ changed = []
188
+ @name_map.each do |raw_name, normalized_id|
189
+ entry = @loggers_cache[normalized_id]
190
+ next if entry.nil?
191
+ next unless entry["managed"]
192
+
193
+ resolved_string = Resolution.resolve_level(
194
+ normalized_id, @parent._environment, @loggers_cache, @groups_cache
195
+ )
196
+ coerced = LogLevel.coerce(resolved_string)
197
+ push_to_adapters(raw_name, coerced)
198
+ previous = @resolved_levels[normalized_id]
199
+ if previous != resolved_string
200
+ @resolved_levels[normalized_id] = resolved_string
201
+ changed << [normalized_id, coerced]
202
+ end
203
+ end
204
+ fire_resolved_change_events(changed, source: source)
205
+ changed
206
+ end
207
+
208
+ def push_to_adapters(raw_name, coerced_level)
209
+ @adapters.each do |a|
210
+ a.apply_level(raw_name, coerced_level)
211
+ rescue StandardError => e
212
+ Smplkit.debug("logging", "adapter apply_level raised: #{e.class}: #{e.message}")
213
+ end
214
+ end
215
+
216
+ def fire_resolved_change_events(changed, source:)
217
+ changed.each do |(normalized_id, coerced_level)|
218
+ event = LoggerChangeEvent.new(name: normalized_id, level: coerced_level, source: source)
219
+ (@global_listeners + @key_listeners[normalized_id]).each do |cb|
220
+ cb.call(event)
221
+ rescue StandardError => e
222
+ Smplkit.debug("logging", "listener raised: #{e.class}: #{e.message}")
223
+ end
224
+ end
225
+ end
226
+
123
227
  def handle_logger_changed(data)
124
- name = Normalize.normalize_logger_name(data["name"] || data["id"] || "")
125
- return if name.empty?
228
+ key = data["id"] || data["name"] || ""
229
+ normalized = Normalize.normalize_logger_name(key)
230
+ return if normalized.empty?
126
231
 
127
- level = data["resolved_level"] || data["level"]
128
- coerced = level && LogLevel.coerce(level)
129
- @adapters.each { |a| a.apply_level(name, coerced) } if coerced
232
+ begin
233
+ entry_id, entry = @manage.loggers.get_logger_entry(normalized)
234
+ @loggers_cache[entry_id || normalized] = entry
235
+ rescue StandardError => e
236
+ Smplkit.debug("websocket", "logger_changed fetch failed for #{normalized.inspect}: #{e.class}: #{e.message}")
237
+ return
238
+ end
239
+
240
+ apply_levels(source: "websocket")
241
+ end
242
+
243
+ def handle_logger_deleted(data)
244
+ key = data["id"] || data["name"] || ""
245
+ normalized = Normalize.normalize_logger_name(key)
246
+ return if normalized.empty?
247
+
248
+ existed = @loggers_cache.delete(normalized)
249
+ @resolved_levels.delete(normalized)
250
+ return unless existed
251
+
252
+ apply_levels(source: "websocket")
253
+ fire_deletion_event(normalized)
254
+ end
255
+
256
+ def handle_group_changed(data)
257
+ key = data["id"] || data["key"] || ""
258
+ return if key.to_s.empty?
259
+
260
+ begin
261
+ entry_id, entry = @manage.log_groups.get_group_entry(key)
262
+ @groups_cache[entry_id || key] = entry
263
+ rescue StandardError => e
264
+ Smplkit.debug("websocket", "group_changed fetch failed for #{key.inspect}: #{e.class}: #{e.message}")
265
+ return
266
+ end
267
+
268
+ apply_levels(source: "websocket")
269
+ end
130
270
 
131
- event = LoggerChangeEvent.new(name: name, level: coerced, source: "websocket")
132
- (@global_listeners + @key_listeners[name]).each do |cb|
271
+ def handle_group_deleted(data)
272
+ key = data["id"] || data["key"] || ""
273
+ return if key.to_s.empty?
274
+
275
+ existed = @groups_cache.delete(key)
276
+ return unless existed
277
+
278
+ apply_levels(source: "websocket")
279
+ fire_deletion_event(key)
280
+ end
281
+
282
+ def handle_loggers_changed(_data)
283
+ fetch_and_apply(trigger: "loggers_changed WS event")
284
+ end
285
+
286
+ def fire_deletion_event(key)
287
+ event = LoggerChangeEvent.new(name: key, level: nil, source: "websocket", deleted: true)
288
+ (@global_listeners + @key_listeners[key]).each do |cb|
133
289
  cb.call(event)
134
290
  rescue StandardError => e
135
291
  Smplkit.debug("logging", "listener raised: #{e.class}: #{e.message}")
@@ -137,9 +293,15 @@ module Smplkit
137
293
  end
138
294
  end
139
295
 
140
- LoggerChangeEvent = Struct.new(:name, :level, :source, keyword_init: true) do
296
+ LoggerChangeEvent = Struct.new(:name, :level, :source, :deleted, keyword_init: true) do
297
+ def initialize(name:, level:, source:, deleted: false)
298
+ super
299
+ end
300
+
141
301
  def ==(other)
142
- other.is_a?(LoggerChangeEvent) && name == other.name && level == other.level && source == other.source
302
+ other.is_a?(LoggerChangeEvent) &&
303
+ name == other.name && level == other.level &&
304
+ source == other.source && deleted == other.deleted
143
305
  end
144
306
  end
145
307
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Smplkit
6
+ module Logging
7
+ # Client-side level resolution per ADR-034 §3.1.
8
+ #
9
+ # The server stores raw configuration and returns it as-is; the SDK is
10
+ # responsible for walking the inheritance chain. Mirrors the Python
11
+ # SDK's +smplkit.logging._resolution+ verbatim — both implementations
12
+ # MUST resolve identically for any given (loggers, groups, env) input.
13
+ module Resolution
14
+ FALLBACK_LEVEL = "INFO"
15
+
16
+ module_function
17
+
18
+ # Resolve the effective level for +logger_id+ in +environment+.
19
+ #
20
+ # Resolution chain (first non-nil wins):
21
+ #
22
+ # 1. Logger's own +environments[env].level+
23
+ # 2. Logger's own +level+
24
+ # 3. Group chain (recursive: group env level → group level → parent group …)
25
+ # 4. Dot-notation ancestry (+com.acme.payments+ → +com.acme+ → +com+,
26
+ # applying steps 1–3 at each)
27
+ # 5. System fallback: +"INFO"+
28
+ #
29
+ # +loggers+ and +groups+ are id-keyed Hashes whose values are Hashes
30
+ # with the same shape as the Python SDK: +"level"+, +"group"+ (parent
31
+ # group id for loggers; parent_id for groups), +"environments"+
32
+ # (Hash keyed by env name with +{"level" => "..."}+ values).
33
+ def resolve_level(logger_id, environment, loggers, groups)
34
+ result = resolve_for_entry(logger_id, environment, loggers, groups)
35
+ if result
36
+ if Smplkit::Debug.enabled
37
+ source = find_resolution_source(logger_id, environment, loggers, groups)
38
+ Smplkit.debug("resolution", "#{logger_id} -> #{result} (source: #{source})")
39
+ end
40
+ return result
41
+ end
42
+
43
+ parts = logger_id.split(".")
44
+ (parts.length - 1).downto(1) do |i|
45
+ ancestor_id = parts[0, i].join(".")
46
+ result = resolve_for_entry(ancestor_id, environment, loggers, groups)
47
+ if result
48
+ Smplkit.debug("resolution", "#{logger_id} -> #{result} (source: ancestor \"#{ancestor_id}\")")
49
+ return result
50
+ end
51
+ end
52
+
53
+ Smplkit.debug("resolution", "#{logger_id} -> #{FALLBACK_LEVEL} (source: system default)")
54
+ FALLBACK_LEVEL
55
+ end
56
+
57
+ # Try to resolve a level for a single entry (logger or ancestor).
58
+ # Returns +nil+ if no level is found at any step of 1–3.
59
+ def resolve_for_entry(logger_id, environment, loggers, groups)
60
+ entry = loggers[logger_id]
61
+ return nil if entry.nil?
62
+
63
+ env_level = env_level_of(entry, environment)
64
+ return env_level if env_level
65
+
66
+ base = entry["level"]
67
+ return base if base
68
+
69
+ resolve_group_chain(entry["group"], environment, groups)
70
+ end
71
+
72
+ # Walk the group chain looking for a level. Cycle-safe via +visited+.
73
+ def resolve_group_chain(group_id, environment, groups)
74
+ visited = Set.new
75
+ current_id = group_id
76
+ while !current_id.nil? && !visited.include?(current_id)
77
+ visited.add(current_id)
78
+ group = groups[current_id]
79
+ break if group.nil?
80
+
81
+ env_level = env_level_of(group, environment)
82
+ return env_level if env_level
83
+
84
+ base = group["level"]
85
+ return base if base
86
+
87
+ current_id = group["group"]
88
+ end
89
+ nil
90
+ end
91
+
92
+ # Human-readable label for which resolution step won. Only consulted
93
+ # when debug logging is enabled; mirrors Python's +_find_resolution_source+.
94
+ def find_resolution_source(logger_id, environment, loggers, groups)
95
+ entry = loggers[logger_id]
96
+ return "not found" if entry.nil?
97
+
98
+ return %(env override "#{environment}") if env_level_of(entry, environment)
99
+ return "base level" if entry["level"]
100
+
101
+ group_id = entry["group"]
102
+ return %(group "#{group_id}") if resolve_group_chain(group_id, environment, groups)
103
+
104
+ "unknown"
105
+ end
106
+
107
+ def env_level_of(entry, environment)
108
+ envs = entry["environments"]
109
+ return nil unless envs.is_a?(Hash)
110
+
111
+ env_data = envs[environment]
112
+ return nil unless env_data.is_a?(Hash)
113
+
114
+ env_data["level"]
115
+ end
116
+ end
117
+ end
118
+ end
@@ -27,6 +27,12 @@ module Smplkit
27
27
  # etc.) and converts at the boundary via the existing
28
28
  # +<resource>_from_resource+ helpers.
29
29
  class ManagementClient
30
+ # Default page[size] the runtime asks for when walking a list
31
+ # endpoint to completion. The platform caps page[size] at 1000;
32
+ # using the same value here makes the minimum number of round-trips
33
+ # per exhaustive fetch.
34
+ RUNTIME_PAGE_SIZE = 1000
35
+
30
36
  attr_reader :contexts, :context_types, :environments, :account_settings,
31
37
  :config, :flags, :loggers, :log_groups, :audit
32
38
 
@@ -126,6 +132,33 @@ module Smplkit
126
132
  end
127
133
  end
128
134
 
135
+ # Walk a generated paginated list endpoint to completion.
136
+ #
137
+ # The block receives a per-page +opts+ hash with +page_number+ and
138
+ # +page_size+ filled in, calls the generated list method through
139
+ # {ErrorMapping.call}, and returns the response object. Pages stop
140
+ # when the server returns fewer rows than requested — the platform's
141
+ # standard last-page signal across every offset-paginated list
142
+ # endpoint. Returns the concatenated +response.data+ rows.
143
+ module PaginatedFetch
144
+ module_function
145
+
146
+ def collect(page_size: RUNTIME_PAGE_SIZE)
147
+ rows = []
148
+ page_number = 1
149
+ loop do
150
+ opts = { page_number: page_number, page_size: page_size }
151
+ response = ErrorMapping.call { yield(opts) }
152
+ page = response.data || []
153
+ rows.concat(page)
154
+ break if page.length < page_size
155
+
156
+ page_number += 1
157
+ end
158
+ rows
159
+ end
160
+ end
161
+
129
162
  # Deep-stringify Hash keys so resources returned by generated +to_hash+
130
163
  # (symbol-keyed) match what the wrapper helpers expect (string-keyed).
131
164
  module ResourceShim
@@ -175,8 +208,11 @@ module Smplkit
175
208
  Smplkit.debug("registration", "context flush failed: #{e.class}: #{e.message}")
176
209
  end
177
210
 
178
- def list
179
- response = ErrorMapping.call { @api.list_contexts }
211
+ def list(page_number: nil, page_size: nil)
212
+ opts = {}
213
+ opts[:page_number] = page_number unless page_number.nil?
214
+ opts[:page_size] = page_size unless page_size.nil?
215
+ response = ErrorMapping.call { @api.list_contexts(opts) }
180
216
  (response.data || []).map { |r| context_from_resource(ResourceShim.from_model(r)) }
181
217
  end
182
218
 
@@ -237,8 +273,11 @@ module Smplkit
237
273
  @api = SmplkitGeneratedClient::App::ContextTypesApi.new(api_client)
238
274
  end
239
275
 
240
- def list
241
- response = ErrorMapping.call { @api.list_context_types }
276
+ def list(page_number: nil, page_size: nil)
277
+ opts = {}
278
+ opts[:page_number] = page_number unless page_number.nil?
279
+ opts[:page_size] = page_size unless page_size.nil?
280
+ response = ErrorMapping.call { @api.list_context_types(opts) }
242
281
  (response.data || []).map { |r| from_resource(ResourceShim.from_model(r)) }
243
282
  end
244
283
 
@@ -296,8 +335,11 @@ module Smplkit
296
335
  @api = SmplkitGeneratedClient::App::EnvironmentsApi.new(api_client)
297
336
  end
298
337
 
299
- def list
300
- response = ErrorMapping.call { @api.list_environments }
338
+ def list(page_number: nil, page_size: nil)
339
+ opts = {}
340
+ opts[:page_number] = page_number unless page_number.nil?
341
+ opts[:page_size] = page_size unless page_size.nil?
342
+ response = ErrorMapping.call { @api.list_environments(opts) }
301
343
  (response.data || []).map { |r| from_resource(ResourceShim.from_model(r)) }
302
344
  end
303
345
 
@@ -413,8 +455,11 @@ module Smplkit
413
455
  @api = SmplkitGeneratedClient::Config::ConfigsApi.new(api_client)
414
456
  end
415
457
 
416
- def list
417
- response = ErrorMapping.call { @api.list_configs }
458
+ def list(page_number: nil, page_size: nil)
459
+ opts = {}
460
+ opts[:page_number] = page_number unless page_number.nil?
461
+ opts[:page_size] = page_size unless page_size.nil?
462
+ response = ErrorMapping.call { @api.list_configs(opts) }
418
463
  (response.data || []).map { |r| Smplkit::Config::Helpers.config_from_json(self, ResourceShim.from_model(r)) }
419
464
  end
420
465
 
@@ -451,8 +496,11 @@ module Smplkit
451
496
  # Build the parent-chain for a given config, walking +parent_id+
452
497
  # pointers across the full config list. Mirrors the Python SDK's
453
498
  # client-side resolution — there is no server +/chain+ endpoint.
499
+ #
500
+ # Walks every page of +list_configs+ so an account with more than
501
+ # +RUNTIME_PAGE_SIZE+ configs still resolves chains correctly.
454
502
  def fetch_chain(target_key)
455
- all_configs = list
503
+ all_configs = fetch_all_configs
456
504
  by_key = all_configs.to_h { |c| [c.key, c] }
457
505
  by_id = all_configs.to_h { |c| [c.id, c] }
458
506
 
@@ -475,6 +523,11 @@ module Smplkit
475
523
 
476
524
  private
477
525
 
526
+ def fetch_all_configs
527
+ rows = PaginatedFetch.collect { |opts| @api.list_configs(opts) }
528
+ rows.map { |r| Smplkit::Config::Helpers.config_from_json(self, ResourceShim.from_model(r)) }
529
+ end
530
+
478
531
  def config_body(config)
479
532
  SmplkitGeneratedClient::Config::ConfigResponse.new(
480
533
  data: SmplkitGeneratedClient::Config::ConfigResource.new(
@@ -576,8 +629,11 @@ module Smplkit
576
629
  @buffer.pending_count
577
630
  end
578
631
 
579
- def list
580
- response = ErrorMapping.call { @api.list_flags }
632
+ def list(page_number: nil, page_size: nil)
633
+ opts = {}
634
+ opts[:page_number] = page_number unless page_number.nil?
635
+ opts[:page_size] = page_size unless page_size.nil?
636
+ response = ErrorMapping.call { @api.list_flags(opts) }
581
637
  (response.data || []).map { |r| flag_from_resource(ResourceShim.from_model(r)) }
582
638
  end
583
639
 
@@ -634,9 +690,11 @@ module Smplkit
634
690
  Smplkit::Flags::Helpers.flag_dict_from_json(ResourceShim.from_model(response.data))
635
691
  end
636
692
 
693
+ # Runtime entry — walks every page so an account holding more than
694
+ # +RUNTIME_PAGE_SIZE+ flags still gets a complete in-memory store.
637
695
  def list_flags
638
- response = ErrorMapping.call { @api.list_flags }
639
- (response.data || []).map { |r| Smplkit::Flags::Helpers.flag_dict_from_json(ResourceShim.from_model(r)) }
696
+ rows = PaginatedFetch.collect { |opts| @api.list_flags(opts) }
697
+ rows.map { |r| Smplkit::Flags::Helpers.flag_dict_from_json(ResourceShim.from_model(r)) }
640
698
  end
641
699
 
642
700
  private
@@ -731,8 +789,11 @@ module Smplkit
731
789
  Smplkit.debug("registration", "logger flush failed: #{e.class}: #{e.message}")
732
790
  end
733
791
 
734
- def list
735
- response = ErrorMapping.call { @api.list_loggers }
792
+ def list(page_number: nil, page_size: nil)
793
+ opts = {}
794
+ opts[:page_number] = page_number unless page_number.nil?
795
+ opts[:page_size] = page_size unless page_size.nil?
796
+ response = ErrorMapping.call { @api.list_loggers(opts) }
736
797
  (response.data || []).map do |r|
737
798
  Smplkit::Logging::Helpers.logger_resource_to_model(self, ResourceShim.from_model(r))
738
799
  end
@@ -755,8 +816,38 @@ module Smplkit
755
816
  Smplkit::Logging::Helpers.logger_resource_to_model(self, ResourceShim.from_model(response.data))
756
817
  end
757
818
 
819
+ # Runtime entry — walks every page and returns an id-keyed Hash of
820
+ # resolution-cache entries (+level+, +group+, +managed+,
821
+ # +environments+). Mirrors the Python SDK's
822
+ # +LoggingClient._fetch_and_apply+ loggers branch.
823
+ def list_logger_entries
824
+ rows = PaginatedFetch.collect { |opts| @api.list_loggers(opts) }
825
+ rows.to_h { |r| logger_entry_from_resource(ResourceShim.from_model(r)) }
826
+ end
827
+
828
+ # Fetch one logger as a resolution-cache entry. Used by the
829
+ # +logger_changed+ WS handler.
830
+ def get_logger_entry(id)
831
+ normalized = Smplkit::Logging::Normalize.normalize_logger_name(id)
832
+ response = ErrorMapping.call { @api.get_logger(normalized) }
833
+ logger_entry_from_resource(ResourceShim.from_model(response.data))
834
+ end
835
+
758
836
  private
759
837
 
838
+ def logger_entry_from_resource(resource)
839
+ attrs = resource["attributes"] || {}
840
+ [
841
+ resource["id"],
842
+ {
843
+ "level" => attrs["level"],
844
+ "group" => attrs["group"],
845
+ "managed" => attrs.key?("managed") ? attrs["managed"] : true,
846
+ "environments" => attrs["environments"] || {}
847
+ }
848
+ ]
849
+ end
850
+
760
851
  def logger_body(logger)
761
852
  # Logger server schema: name, level, group, managed.
762
853
  # +resolved_level+ is read-only, +service+/+environment+ are
@@ -781,8 +872,11 @@ module Smplkit
781
872
  @api = SmplkitGeneratedClient::Logging::LogGroupsApi.new(api_client)
782
873
  end
783
874
 
784
- def list
785
- response = ErrorMapping.call { @api.list_log_groups }
875
+ def list(page_number: nil, page_size: nil)
876
+ opts = {}
877
+ opts[:page_number] = page_number unless page_number.nil?
878
+ opts[:page_size] = page_size unless page_size.nil?
879
+ response = ErrorMapping.call { @api.list_log_groups(opts) }
786
880
  (response.data || []).map do |r|
787
881
  Smplkit::Logging::Helpers.log_group_resource_to_model(self, ResourceShim.from_model(r))
788
882
  end
@@ -816,8 +910,35 @@ module Smplkit
816
910
  Smplkit::Logging::Helpers.log_group_resource_to_model(self, ResourceShim.from_model(response.data))
817
911
  end
818
912
 
913
+ # Runtime entry — walks every page and returns an id-keyed Hash of
914
+ # resolution-cache entries (+level+, +group+, +environments+). The
915
+ # +group+ key carries the *parent group id* so the resolution
916
+ # algorithm can walk the chain with the same key shape it uses for
917
+ # loggers.
918
+ def list_group_entries
919
+ rows = PaginatedFetch.collect { |opts| @api.list_log_groups(opts) }
920
+ rows.to_h { |r| group_entry_from_resource(ResourceShim.from_model(r)) }
921
+ end
922
+
923
+ def get_group_entry(key)
924
+ response = ErrorMapping.call { @api.get_log_group(key) }
925
+ group_entry_from_resource(ResourceShim.from_model(response.data))
926
+ end
927
+
819
928
  private
820
929
 
930
+ def group_entry_from_resource(resource)
931
+ attrs = resource["attributes"] || {}
932
+ [
933
+ resource["id"],
934
+ {
935
+ "level" => attrs["level"],
936
+ "group" => attrs["parent_id"],
937
+ "environments" => attrs["environments"] || {}
938
+ }
939
+ ]
940
+ end
941
+
821
942
  def log_group_body(group)
822
943
  # LogGroup server schema: name, level, parent_id (no description).
823
944
  SmplkitGeneratedClient::Logging::LogGroupResponse.new(
data/lib/smplkit.rb CHANGED
@@ -55,6 +55,7 @@ require_relative "smplkit/logging/normalize"
55
55
  require_relative "smplkit/logging/sources"
56
56
  require_relative "smplkit/logging/models"
57
57
  require_relative "smplkit/logging/helpers"
58
+ require_relative "smplkit/logging/resolution"
58
59
  require_relative "smplkit/logging/adapters/base"
59
60
  require_relative "smplkit/logging/adapters/stdlib_logger_adapter"
60
61
  require_relative "smplkit/logging/client"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smplkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.14
4
+ version: 3.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Smpl Solutions LLC
@@ -701,6 +701,7 @@ files:
701
701
  - lib/smplkit/logging/levels.rb
702
702
  - lib/smplkit/logging/models.rb
703
703
  - lib/smplkit/logging/normalize.rb
704
+ - lib/smplkit/logging/resolution.rb
704
705
  - lib/smplkit/logging/sources.rb
705
706
  - lib/smplkit/management/audit.rb
706
707
  - lib/smplkit/management/buffer.rb