smplkit 3.0.96 → 3.0.97

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.
@@ -5,7 +5,7 @@
5
5
  # Smpl Logging has two surfaces on a single client, mirroring how the config,
6
6
  # flags, audit, and jobs clients expose their full surface from one class:
7
7
  #
8
- # * *Management surface* — works immediately, no +install+ required. Two
8
+ # * *CRUD surface* — works immediately, no +install+ required. Two
9
9
  # sub-clients (the audit pattern):
10
10
  #
11
11
  # * +client.logging.loggers+ — logger CRUD + discovery: +new+ / +list+ / +get+
@@ -47,15 +47,17 @@ module Smplkit
47
47
  # defaults). The app transport is needed for the WebSocket gateway, which
48
48
  # lives on the app service (like flags); the app base URL is returned so a
49
49
  # standalone client can open its own WebSocket against the event gateway.
50
+ #
51
+ # @api private
50
52
  def self.logging_transport(api_key:, base_url:, profile:, base_domain:, scheme:, debug:, extra_headers:)
51
- cfg = ConfigResolution.resolve_management_config(
53
+ cfg = ConfigResolution.resolve_client_config(
52
54
  profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme, debug: debug
53
55
  )
54
56
  resolved_key = api_key.nil? ? cfg.api_key : api_key
55
57
  merged = {}
56
58
  merged.merge!(cfg.extra_headers || {})
57
59
  merged.merge!(extra_headers || {})
58
- tcfg = ConfigResolution::ResolvedManagementConfig.new(
60
+ tcfg = ConfigResolution::ResolvedClientConfig.new(
59
61
  api_key: resolved_key, base_domain: cfg.base_domain, scheme: cfg.scheme,
60
62
  debug: cfg.debug, extra_headers: merged
61
63
  )
@@ -66,6 +68,8 @@ module Smplkit
66
68
  end
67
69
 
68
70
  # Discover and load the SDK's built-in logging adapters.
71
+ #
72
+ # @api private
69
73
  def self.auto_load_adapters
70
74
  adapters = [Adapters::StdlibLoggerAdapter.new]
71
75
 
@@ -80,10 +84,20 @@ module Smplkit
80
84
  adapters
81
85
  end
82
86
 
83
- LoggerChangeEvent = Struct.new(:name, :level, :source, keyword_init: true) do
87
+ # Fired once per managed logger whose effective level the SDK just applied.
88
+ #
89
+ # @!attribute [rw] id
90
+ # @return [String] The affected logger's normalized id.
91
+ # @!attribute [rw] level
92
+ # @return [String] The newly-applied effective smplkit level string (e.g.
93
+ # +"INFO"+, +"DEBUG"+) — the same value the resolution algorithm returns.
94
+ # @!attribute [rw] source
95
+ # @return [String] Short string identifying the trigger — typically
96
+ # +"websocket"+ or +"manual"+ (a +refresh+ call).
97
+ LoggerChangeEvent = Struct.new(:id, :level, :source, keyword_init: true) do
84
98
  def ==(other)
85
99
  other.is_a?(LoggerChangeEvent) &&
86
- name == other.name && level == other.level && source == other.source
100
+ id == other.id && level == other.level && source == other.source
87
101
  end
88
102
  end
89
103
 
@@ -98,7 +112,17 @@ module Smplkit
98
112
  @buffer = buffer
99
113
  end
100
114
 
101
- # Buffer logger sources for registration; optionally flush immediately.
115
+ # Queue one or more logger sources for registration with the server.
116
+ #
117
+ # Sources are buffered locally and sent in a batch. The batch is sent
118
+ # automatically once enough sources accumulate; pass +flush: true+ to send
119
+ # the current batch right away instead of waiting.
120
+ #
121
+ # @param items [LoggerSource, Array<LoggerSource>] A single logger source,
122
+ # or an array of them, to queue.
123
+ # @param flush [Boolean] When +true+, send the buffered sources immediately
124
+ # rather than waiting for the batch to fill.
125
+ # @return [void]
102
126
  def register(items, flush: false)
103
127
  batch = items.is_a?(Array) ? items : [items]
104
128
  batch.each do |src|
@@ -118,6 +142,8 @@ module Smplkit
118
142
  end
119
143
 
120
144
  # Drain the buffer and POST pending logger sources to the bulk endpoint.
145
+ #
146
+ # @return [void]
121
147
  def flush
122
148
  batch = @buffer.drain
123
149
  return if batch.empty?
@@ -133,21 +159,38 @@ module Smplkit
133
159
  end
134
160
 
135
161
  # Synchronous flush — alias of +flush+ for the periodic-flush path.
162
+ #
163
+ # @return [void]
136
164
  def flush_sync
137
165
  flush
138
166
  end
139
167
 
140
168
  # Number of sources queued and awaiting flush.
169
+ #
170
+ # @return [Integer] count of buffered sources not yet sent.
141
171
  def pending_count
142
172
  @buffer.pending_count
143
173
  end
144
174
 
145
- # Return a new unsaved +SmplLogger+. Call +SmplLogger#save+ to persist.
175
+ # Build a new unsaved logger. The returned +SmplLogger+ is local only;
176
+ # call its +SmplLogger#save+ to persist it.
177
+ #
178
+ # @param id [String] Identifier for the logger (its normalized name).
179
+ # @param managed [Boolean] When +true+ (the default), smplkit controls
180
+ # this logger's level at runtime. Set +false+ to register the logger for
181
+ # visibility without taking over its level.
182
+ # @return [SmplLogger] An unsaved logger bound to this client.
146
183
  def new(id, managed: true)
147
184
  SmplLogger.new(self, id: id, name: id, resolved_level: nil, managed: managed)
148
185
  end
149
186
 
150
187
  # List loggers for the authenticated account.
188
+ #
189
+ # @param page_number [Integer, nil] 1-based page index to fetch. When
190
+ # omitted, the server returns the first page.
191
+ # @param page_size [Integer, nil] Maximum number of loggers per page. When
192
+ # omitted, the server applies its default page size.
193
+ # @return [Array<SmplLogger>] The loggers on the requested page.
151
194
  def list(page_number: nil, page_size: nil)
152
195
  opts = {}
153
196
  opts[:page_number] = page_number unless page_number.nil?
@@ -156,18 +199,27 @@ module Smplkit
156
199
  (response.data || []).map { |r| Helpers.logger_resource_to_model(self, ApiSupport::ResourceShim.from_model(r)) }
157
200
  end
158
201
 
159
- # Fetch the editable +SmplLogger+ resource by id.
202
+ # Fetch a single logger by id.
203
+ #
204
+ # @param id [String] Identifier of the logger to fetch.
205
+ # @return [SmplLogger] The editable logger resource.
206
+ # @raise [Smplkit::NotFoundError] If no logger with that id exists.
160
207
  def get(id)
161
208
  response = ApiSupport::ErrorMapping.call { @api.get_logger(id) }
162
209
  Helpers.logger_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
163
210
  end
164
211
 
165
212
  # Delete a logger by id.
213
+ #
214
+ # @param id [String] Identifier of the logger to delete.
215
+ # @return [void]
216
+ # @raise [Smplkit::NotFoundError] If no logger with that id exists.
166
217
  def delete(id)
167
218
  ApiSupport::ErrorMapping.call { @api.delete_logger(id) }
168
219
  nil
169
220
  end
170
221
 
222
+ # @api private
171
223
  def _update_logger(logger)
172
224
  response = ApiSupport::ErrorMapping.call { @api.update_logger(logger.id || logger.name, logger_body(logger)) }
173
225
  Helpers.logger_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
@@ -175,6 +227,8 @@ module Smplkit
175
227
 
176
228
  # Runtime entry — walks every page and returns an id-keyed Hash of
177
229
  # resolution-cache entries (+level+, +group+, +managed+, +environments+).
230
+ #
231
+ # @api private
178
232
  def list_logger_entries
179
233
  rows = ApiSupport::PaginatedFetch.collect { |opts| @api.list_loggers(opts) }
180
234
  rows.to_h { |r| logger_entry_from_resource(ApiSupport::ResourceShim.from_model(r)) }
@@ -182,6 +236,8 @@ module Smplkit
182
236
 
183
237
  # Fetch one logger as a resolution-cache entry. Used by the +logger_changed+
184
238
  # WS handler.
239
+ #
240
+ # @api private
185
241
  def get_logger_entry(id)
186
242
  response = ApiSupport::ErrorMapping.call { @api.get_logger(id) }
187
243
  logger_entry_from_resource(ApiSupport::ResourceShim.from_model(response.data))
@@ -236,14 +292,28 @@ module Smplkit
236
292
  @api = SmplkitGeneratedClient::Logging::LogGroupsApi.new(http_client)
237
293
  end
238
294
 
239
- # Return a new unsaved +SmplLogGroup+. Call +SmplLogGroup#save+ to persist.
295
+ # Build a new unsaved log group. The returned +SmplLogGroup+ is local
296
+ # only; call its +SmplLogGroup#save+ to persist it.
297
+ #
298
+ # @param id [String] Identifier for the log group.
299
+ # @param name [String, nil] Human-readable display name. Defaults to a
300
+ # title-cased version of +id+ when omitted.
301
+ # @param group [String, nil] Identifier of the parent log group, when
302
+ # nesting groups. +nil+ for a top-level group.
303
+ # @return [SmplLogGroup] An unsaved log group bound to this client.
240
304
  def new(id, name: nil, group: nil)
241
305
  SmplLogGroup.new(
242
- self, key: id, name: name.nil? ? Smplkit::Helpers.key_to_display_name(id) : name, parent_id: group
306
+ self, key: id, name: name.nil? ? Smplkit::Helpers.key_to_display_name(id) : name, group: group
243
307
  )
244
308
  end
245
309
 
246
310
  # List log groups for the authenticated account.
311
+ #
312
+ # @param page_number [Integer, nil] 1-based page index to fetch. When
313
+ # omitted, the server returns the first page.
314
+ # @param page_size [Integer, nil] Maximum number of log groups per page.
315
+ # When omitted, the server applies its default page size.
316
+ # @return [Array<SmplLogGroup>] The log groups on the requested page.
247
317
  def list(page_number: nil, page_size: nil)
248
318
  opts = {}
249
319
  opts[:page_number] = page_number unless page_number.nil?
@@ -254,23 +324,33 @@ module Smplkit
254
324
  end
255
325
  end
256
326
 
257
- # Fetch the editable +SmplLogGroup+ resource by id.
327
+ # Fetch a single log group by id.
328
+ #
329
+ # @param id [String] Identifier of the log group to fetch.
330
+ # @return [SmplLogGroup] The editable log group resource.
331
+ # @raise [Smplkit::NotFoundError] If no log group with that id exists.
258
332
  def get(id)
259
333
  response = ApiSupport::ErrorMapping.call { @api.get_log_group(id) }
260
334
  Helpers.log_group_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
261
335
  end
262
336
 
263
337
  # Delete a log group by id.
338
+ #
339
+ # @param id [String] Identifier of the log group to delete.
340
+ # @return [void]
341
+ # @raise [Smplkit::NotFoundError] If no log group with that id exists.
264
342
  def delete(id)
265
343
  ApiSupport::ErrorMapping.call { @api.delete_log_group(id) }
266
344
  nil
267
345
  end
268
346
 
347
+ # @api private
269
348
  def _create_log_group(group)
270
349
  response = ApiSupport::ErrorMapping.call { @api.create_log_group(log_group_body(group)) }
271
350
  Helpers.log_group_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
272
351
  end
273
352
 
353
+ # @api private
274
354
  def _update_log_group(group)
275
355
  response = ApiSupport::ErrorMapping.call { @api.update_log_group(group.key, log_group_body(group)) }
276
356
  Helpers.log_group_resource_to_model(self, ApiSupport::ResourceShim.from_model(response.data))
@@ -280,11 +360,17 @@ module Smplkit
280
360
  # resolution-cache entries (+level+, +group+, +environments+). The +group+
281
361
  # key carries the *parent group id* so the resolution algorithm can walk
282
362
  # the chain with the same key shape it uses for loggers.
363
+ #
364
+ # @api private
283
365
  def list_group_entries
284
366
  rows = ApiSupport::PaginatedFetch.collect { |opts| @api.list_log_groups(opts) }
285
367
  rows.to_h { |r| group_entry_from_resource(ApiSupport::ResourceShim.from_model(r)) }
286
368
  end
287
369
 
370
+ # Fetch one log group as a resolution-cache entry. Used by the
371
+ # +group_changed+ WS handler.
372
+ #
373
+ # @api private
288
374
  def get_group_entry(key)
289
375
  response = ApiSupport::ErrorMapping.call { @api.get_log_group(key) }
290
376
  group_entry_from_resource(ApiSupport::ResourceShim.from_model(response.data))
@@ -305,7 +391,7 @@ module Smplkit
305
391
  end
306
392
 
307
393
  def log_group_body(group)
308
- # LogGroup server schema: name, level, parent_id (no description).
394
+ # LogGroup server schema: name, level, parent_id, environments (no description).
309
395
  SmplkitGeneratedClient::Logging::LogGroupResponse.new(
310
396
  data: SmplkitGeneratedClient::Logging::LogGroupResource.new(
311
397
  type: "log_group",
@@ -313,7 +399,8 @@ module Smplkit
313
399
  attributes: SmplkitGeneratedClient::Logging::LogGroup.new(
314
400
  name: group.name,
315
401
  level: group.level&.to_s,
316
- parent_id: group.parent_id
402
+ parent_id: group.group,
403
+ environments: Logging.environments_to_wire(group.environments)
317
404
  )
318
405
  )
319
406
  )
@@ -329,7 +416,7 @@ module Smplkit
329
416
  # logging.loggers.new("sqlalchemy.engine").save
330
417
  # logging.install
331
418
  #
332
- # The management surface (+loggers+ / +log_groups+ sub-clients) works
419
+ # The CRUD surface (+loggers+ / +log_groups+ sub-clients) works
333
420
  # immediately. +register_adapter+ is a pre-install configuration call. The
334
421
  # live surface (+install+ / +on_change+ / +refresh+) requires +install+
335
422
  # first; calling +on_change+ / +refresh+ earlier raises +NotInstalledError+.
@@ -392,6 +479,10 @@ module Smplkit
392
479
  self
393
480
  end
394
481
 
482
+ # Registered logging adapters.
483
+ #
484
+ # @return [Array<Adapters::Base>] a copy of the adapters this client uses
485
+ # to discover loggers and apply levels.
395
486
  def adapters
396
487
  @adapters.dup
397
488
  end
@@ -510,9 +601,27 @@ module Smplkit
510
601
  end
511
602
  @connected = false
512
603
  end
604
+
605
+ # Release resources held by this client.
606
+ #
607
+ # @api private
608
+ # @return [void]
513
609
  alias _close close
514
610
 
515
- # Construct, yield to the block, and close on exit.
611
+ # Construct a +LoggingClient+, yield it to the block, and close it on exit.
612
+ #
613
+ # Mirrors Ruby's +File.open+ block form: the client is closed
614
+ # automatically when the block returns or raises, so a standalone client's
615
+ # owned transports and WebSocket are always torn down.
616
+ #
617
+ # Smplkit::LoggingClient.open(environment: "production") do |logging|
618
+ # logging.loggers.new("sqlalchemy.engine").save
619
+ # logging.install
620
+ # end
621
+ #
622
+ # @param kwargs [Hash] keyword arguments forwarded to +new+.
623
+ # @yieldparam client [LoggingClient] the constructed client.
624
+ # @return [Object] the block's return value.
516
625
  def self.open(**kwargs)
517
626
  client = new(**kwargs)
518
627
  begin
@@ -679,7 +788,7 @@ module Smplkit
679
788
  # Both the key-scoped listeners registered for +logger_id+ and every global
680
789
  # listener receive the same payload.
681
790
  def fire_for_logger(logger_id, level, source)
682
- event = LoggerChangeEvent.new(name: logger_id, level: level, source: source)
791
+ event = LoggerChangeEvent.new(id: logger_id, level: level, source: source)
683
792
  (@global_listeners + @key_listeners[logger_id]).each do |cb|
684
793
  cb.call(event)
685
794
  rescue StandardError => e
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Smplkit
4
4
  module Logging
5
+ # Resource-to-model conversion helpers for the logging wrapper.
6
+ #
7
+ # @api private
5
8
  module Helpers
6
9
  module_function
7
10
 
@@ -33,7 +36,7 @@ module Smplkit
33
36
  name: attrs["name"],
34
37
  level: attrs["level"] && LogLevel.coerce(attrs["level"]),
35
38
  description: attrs["description"],
36
- parent_id: attrs["parent_id"],
39
+ group: attrs["parent_id"],
37
40
  environments: attrs["environments"] || {},
38
41
  created_at: attrs["created_at"],
39
42
  updated_at: attrs["updated_at"]
@@ -6,7 +6,7 @@ require_relative "../log_level"
6
6
  module Smplkit
7
7
  module Logging
8
8
  # Bidirectional mapping between Ruby stdlib +Logger+ levels and smplkit
9
- # canonical levels (per ADR-046 §2.3).
9
+ # canonical levels.
10
10
  #
11
11
  # Stdlib +Logger+ has DEBUG/INFO/WARN/ERROR/FATAL/UNKNOWN — no TRACE. The
12
12
  # +stdlib-logger+ adapter maps smplkit TRACE to stdlib DEBUG when
@@ -14,6 +14,8 @@ module Smplkit
14
14
  # discovering — there is no way to distinguish smplkit-TRACE-mapped-to-
15
15
  # DEBUG from genuine DEBUG, which is consistent with how the Python
16
16
  # +stdlib-logging+ adapter handles the same gap.
17
+ #
18
+ # @api private
17
19
  module Levels
18
20
  STDLIB_TO_SMPL = {
19
21
  ::Logger::DEBUG => LogLevel::DEBUG,
@@ -21,6 +21,8 @@ module Smplkit
21
21
  #
22
22
  # Accepts both pre-built +LoggerEnvironment+ instances and the wire-shaped
23
23
  # +{env_id => {"level" => "ERROR"}}+ dicts.
24
+ #
25
+ # @api private
24
26
  def self.convert_environments(value)
25
27
  return {} if value.nil? || value.empty?
26
28
 
@@ -29,6 +31,7 @@ module Smplkit
29
31
  end
30
32
  end
31
33
 
34
+ # @api private
32
35
  def self.build_logger_environment(env_data)
33
36
  return env_data if env_data.is_a?(LoggerEnvironment)
34
37
  return LoggerEnvironment.new unless env_data.is_a?(Hash)
@@ -45,6 +48,8 @@ module Smplkit
45
48
 
46
49
  # Convert a typed environments dict to the wire-shaped dict for sending.
47
50
  # Entries with +level=nil+ are skipped (no override to send).
51
+ #
52
+ # @api private
48
53
  def self.environments_to_wire(environments)
49
54
  environments.each_with_object({}) do |(env_id, env), out|
50
55
  out[env_id] = { "level" => env.level.to_s } unless env.level.nil?
@@ -85,6 +90,9 @@ module Smplkit
85
90
  @updated_at = updated_at
86
91
  end
87
92
 
93
+ # Whether the SDK applies server-driven level changes to this logger.
94
+ #
95
+ # @return [Boolean] +true+ when smplkit controls this logger's level.
88
96
  def managed? = !!@managed
89
97
 
90
98
  # Read-only view of per-environment level overrides. Mutate via
@@ -125,6 +133,10 @@ module Smplkit
125
133
  @environments = {}
126
134
  end
127
135
 
136
+ # Persist this logger to the server (create or update).
137
+ #
138
+ # @return [self] this logger, refreshed from the server response.
139
+ # @raise [RuntimeError] If the logger was constructed without a client.
128
140
  def save
129
141
  raise "SmplLogger was constructed without a client; cannot save" if @client.nil?
130
142
 
@@ -134,6 +146,11 @@ module Smplkit
134
146
  end
135
147
  alias save! save
136
148
 
149
+ # Delete this logger from the server.
150
+ #
151
+ # @return [void]
152
+ # @raise [RuntimeError] If the logger was constructed without a client.
153
+ # @raise [Smplkit::NotFoundError] If the logger no longer exists.
137
154
  def delete
138
155
  raise "SmplLogger was constructed without a client; cannot delete" if @client.nil?
139
156
 
@@ -141,6 +158,7 @@ module Smplkit
141
158
  end
142
159
  alias delete! delete
143
160
 
161
+ # @api private
144
162
  def _apply(other)
145
163
  @id = other.id
146
164
  @name = other.name
@@ -160,11 +178,11 @@ module Smplkit
160
178
  # A log group resource — a hierarchical bag of loggers with a shared
161
179
  # configured level.
162
180
  class SmplLogGroup
163
- attr_accessor :id, :key, :name, :level, :description, :parent_id, :environments,
181
+ attr_accessor :id, :key, :name, :level, :description, :group,
164
182
  :created_at, :updated_at
165
183
 
166
184
  def initialize(client = nil, key:, id: nil, name: nil, level: nil,
167
- description: nil, parent_id: nil, environments: nil,
185
+ description: nil, group: nil, environments: nil,
168
186
  created_at: nil, updated_at: nil)
169
187
  @client = client
170
188
  @id = id
@@ -172,12 +190,54 @@ module Smplkit
172
190
  @name = name
173
191
  @level = level
174
192
  @description = description
175
- @parent_id = parent_id
176
- @environments = environments || {}
193
+ @group = group
194
+ @environments = Logging.convert_environments(environments)
177
195
  @created_at = created_at
178
196
  @updated_at = updated_at
179
197
  end
180
198
 
199
+ # Read-only view of per-environment level overrides. Mutate via
200
+ # +set_level+ / +clear_level+ (with +environment: "..."+).
201
+ def environments
202
+ @environments.dup
203
+ end
204
+
205
+ # Set the log level.
206
+ #
207
+ # With +environment: nil+ (the default), sets the base log level used when
208
+ # no environment-specific override applies. With +environment: "..."+,
209
+ # sets the per-environment override. Changes are local until +save+.
210
+ def set_level(level, environment: nil)
211
+ if environment.nil?
212
+ @level = level
213
+ else
214
+ @environments[environment] = LoggerEnvironment.new(level: level)
215
+ end
216
+ end
217
+
218
+ # Remove a log level.
219
+ #
220
+ # With +environment: nil+ (the default), removes the base log level (the
221
+ # group then inherits from its parent group / ancestor / system default).
222
+ # With +environment: "..."+, removes the per-environment override only.
223
+ # Changes are local until +save+.
224
+ def clear_level(environment: nil)
225
+ if environment.nil?
226
+ @level = nil
227
+ else
228
+ @environments.delete(environment)
229
+ end
230
+ end
231
+
232
+ # Remove all per-environment level overrides.
233
+ def clear_all_environment_levels
234
+ @environments = {}
235
+ end
236
+
237
+ # Persist this group to the server (create or update).
238
+ #
239
+ # @return [self] this group, refreshed from the server response.
240
+ # @raise [RuntimeError] If the group was constructed without a client.
181
241
  def save
182
242
  raise "SmplLogGroup was constructed without a client; cannot save" if @client.nil?
183
243
 
@@ -192,6 +252,11 @@ module Smplkit
192
252
  end
193
253
  alias save! save
194
254
 
255
+ # Delete this group from the server.
256
+ #
257
+ # @return [void]
258
+ # @raise [RuntimeError] If the group was constructed without a client.
259
+ # @raise [Smplkit::NotFoundError] If the group no longer exists.
195
260
  def delete
196
261
  raise "SmplLogGroup was constructed without a client; cannot delete" if @client.nil?
197
262
 
@@ -199,13 +264,14 @@ module Smplkit
199
264
  end
200
265
  alias delete! delete
201
266
 
267
+ # @api private
202
268
  def _apply(other)
203
269
  @id = other.id
204
270
  @key = other.key
205
271
  @name = other.name
206
272
  @level = other.level
207
273
  @description = other.description
208
- @parent_id = other.parent_id
274
+ @group = other.group
209
275
  @environments = other.environments
210
276
  @created_at = other.created_at
211
277
  @updated_at = other.updated_at
@@ -2,9 +2,11 @@
2
2
 
3
3
  module Smplkit
4
4
  module Logging
5
- # Logger name normalization per ADR-034 §5.
5
+ # Logger name normalization.
6
6
  #
7
7
  # Replace +/+ with +.+, replace +:+ with +.+, lowercase everything.
8
+ #
9
+ # @api private
8
10
  module Normalize
9
11
  module_function
10
12
 
@@ -4,12 +4,12 @@ require "set"
4
4
 
5
5
  module Smplkit
6
6
  module Logging
7
- # Client-side level resolution per ADR-034 §3.1.
7
+ # Client-side level resolution.
8
8
  #
9
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.
10
+ # responsible for walking the inheritance chain.
11
+ #
12
+ # @api private
13
13
  module Resolution
14
14
  FALLBACK_LEVEL = "INFO"
15
15