smplkit 1.0.5

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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +21 -0
  4. data/README.md +105 -0
  5. data/lib/smplkit/client.rb +218 -0
  6. data/lib/smplkit/config/client.rb +238 -0
  7. data/lib/smplkit/config/helpers.rb +108 -0
  8. data/lib/smplkit/config/models.rb +192 -0
  9. data/lib/smplkit/config_resolution.rb +202 -0
  10. data/lib/smplkit/context.rb +68 -0
  11. data/lib/smplkit/debug.rb +50 -0
  12. data/lib/smplkit/errors.rb +114 -0
  13. data/lib/smplkit/flags/client.rb +480 -0
  14. data/lib/smplkit/flags/helpers.rb +76 -0
  15. data/lib/smplkit/flags/models.rb +258 -0
  16. data/lib/smplkit/flags/types.rb +233 -0
  17. data/lib/smplkit/generators/install_generator.rb +42 -0
  18. data/lib/smplkit/helpers.rb +15 -0
  19. data/lib/smplkit/log_level.rb +57 -0
  20. data/lib/smplkit/logging/adapters/base.rb +63 -0
  21. data/lib/smplkit/logging/adapters/semantic_logger_adapter.rb +88 -0
  22. data/lib/smplkit/logging/adapters/stdlib_logger_adapter.rb +143 -0
  23. data/lib/smplkit/logging/client.rb +142 -0
  24. data/lib/smplkit/logging/helpers.rb +69 -0
  25. data/lib/smplkit/logging/levels.rb +86 -0
  26. data/lib/smplkit/logging/models.rb +124 -0
  27. data/lib/smplkit/logging/normalize.rb +16 -0
  28. data/lib/smplkit/logging/sources.rb +44 -0
  29. data/lib/smplkit/management/buffer.rb +111 -0
  30. data/lib/smplkit/management/client.rb +623 -0
  31. data/lib/smplkit/management/models.rb +133 -0
  32. data/lib/smplkit/management/types.rb +65 -0
  33. data/lib/smplkit/metrics.rb +78 -0
  34. data/lib/smplkit/railtie.rb +48 -0
  35. data/lib/smplkit/version.rb +5 -0
  36. data/lib/smplkit/ws.rb +92 -0
  37. data/lib/smplkit.rb +43 -0
  38. data/sig/smplkit.rbs +141 -0
  39. metadata +139 -0
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Flags
5
+ # A constrained value entry on a +Flag+.
6
+ #
7
+ # Lives in +Flag#values+. Frozen — author values via +Flag#add_value+ /
8
+ # +Flag#remove_value+ / +Flag#clear_values+.
9
+ FlagValue = Data.define(:name, :value)
10
+
11
+ # A single targeting rule on a +Flag+.
12
+ #
13
+ # Lives in +FlagEnvironment#rules+. Frozen — author rules via the
14
+ # +Smplkit::Rule+ fluent builder and pass through +Flag#add_rule+.
15
+ #
16
+ # Attributes:
17
+ # - logic: JSON Logic predicate. Empty Hash means "always match".
18
+ # - value: Value to serve when +logic+ evaluates truthy.
19
+ # - description: Human-readable label (optional).
20
+ class FlagRule
21
+ attr_reader :logic, :value, :description
22
+
23
+ def initialize(logic:, value: nil, description: nil)
24
+ @logic = logic
25
+ @value = value
26
+ @description = description
27
+ freeze
28
+ end
29
+
30
+ def ==(other)
31
+ other.is_a?(FlagRule) && logic == other.logic && value == other.value && description == other.description
32
+ end
33
+ alias eql? ==
34
+
35
+ def hash = [logic, value, description].hash
36
+ end
37
+
38
+ # Per-environment configuration on a +Flag+.
39
+ #
40
+ # Lives at +flag.environments[env_name]+. Frozen — mutate via +Flag#add_rule+
41
+ # / +Flag#enable_rules+ / +Flag#disable_rules+ / +Flag#set_default+ /
42
+ # +Flag#clear_rules+ (with +environment:+).
43
+ #
44
+ # Attributes:
45
+ # - enabled: Whether the flag is active in this environment.
46
+ # - default: Environment-specific default override (+nil+ means no override).
47
+ # - rules: Targeting rules to evaluate, in order.
48
+ class FlagEnvironment
49
+ attr_reader :enabled, :default, :rules
50
+
51
+ def initialize(enabled: true, default: nil, rules: [])
52
+ @enabled = enabled
53
+ @default = default
54
+ @rules = rules.is_a?(Array) ? rules.dup.freeze : rules
55
+ freeze
56
+ end
57
+
58
+ def enabled? = @enabled
59
+
60
+ # Return a new FlagEnvironment with the given fields replaced.
61
+ def with(**changes)
62
+ self.class.new(
63
+ enabled: changes.fetch(:enabled, @enabled),
64
+ default: changes.fetch(:default, @default),
65
+ rules: changes.fetch(:rules, @rules)
66
+ )
67
+ end
68
+
69
+ def ==(other)
70
+ other.is_a?(FlagEnvironment) && enabled == other.enabled && default == other.default && rules == other.rules
71
+ end
72
+ alias eql? ==
73
+
74
+ def hash = [enabled, default, rules].hash
75
+ end
76
+
77
+ # A flag resource.
78
+ #
79
+ # Provides management operations (save, add_rule, environment settings)
80
+ # and runtime evaluation via +get+.
81
+ #
82
+ # Use typed variants (BooleanFlag, StringFlag, NumberFlag, JsonFlag)
83
+ # for type-safe +get+ return values.
84
+ class Flag
85
+ attr_accessor :id, :name, :type, :default, :description, :created_at, :updated_at
86
+
87
+ def initialize(client = nil, name:, type:, default:, id: nil, values: nil,
88
+ description: nil, environments: nil, created_at: nil, updated_at: nil)
89
+ @client = client
90
+ @id = id
91
+ @name = name
92
+ @type = type
93
+ @default = default
94
+ @values = values&.dup
95
+ @description = description
96
+ @environments = environments ? environments.dup : {}
97
+ @created_at = created_at
98
+ @updated_at = updated_at
99
+ end
100
+
101
+ # Read-only view of constrained values. +nil+ means unconstrained.
102
+ def values
103
+ @values&.dup
104
+ end
105
+
106
+ # Read-only view of per-environment configuration.
107
+ def environments
108
+ @environments.dup
109
+ end
110
+
111
+ # Persist this flag to the server.
112
+ #
113
+ # Creates a new flag if unsaved, or updates the existing one. Requires a
114
+ # management client (i.e. the flag was constructed via +mgmt.flags.new_*+
115
+ # or returned from +mgmt.flags.get/list+).
116
+ def save
117
+ raise "Flag was constructed without a client; cannot save" if @client.nil?
118
+
119
+ updated =
120
+ if @created_at.nil?
121
+ @client._create_flag(self)
122
+ else
123
+ @client._update_flag(self)
124
+ end
125
+ _apply(updated)
126
+ self
127
+ end
128
+ alias save! save
129
+
130
+ def delete
131
+ raise "Flag was constructed without a client or id; cannot delete" if @client.nil? || @id.nil?
132
+
133
+ @client.delete(@id)
134
+ end
135
+ alias delete! delete
136
+
137
+ # Append a rule to a specific environment.
138
+ #
139
+ # The +built_rule+ Hash must include an +"environment"+ key.
140
+ # Call +save+ to persist.
141
+ def add_rule(built_rule)
142
+ env_key = built_rule["environment"]
143
+ if env_key.nil?
144
+ raise ArgumentError,
145
+ "Built rule must include 'environment' key. " \
146
+ "Use Smplkit::Rule.new(..., environment: 'env_key').when(...).serve(...)"
147
+ end
148
+
149
+ flag_rule = FlagRule.new(
150
+ logic: (built_rule["logic"] || {}).dup,
151
+ value: built_rule["value"],
152
+ description: built_rule["description"]
153
+ )
154
+ existing = @environments[env_key] || FlagEnvironment.new
155
+ @environments[env_key] = existing.with(rules: (existing.rules + [flag_rule]).freeze)
156
+ self
157
+ end
158
+
159
+ def enable_rules(environment: nil)
160
+ scoped(environment) { |k| @environments[k] = (@environments[k] || FlagEnvironment.new).with(enabled: true) }
161
+ self
162
+ end
163
+
164
+ def disable_rules(environment: nil)
165
+ scoped(environment) { |k| @environments[k] = (@environments[k] || FlagEnvironment.new).with(enabled: false) }
166
+ self
167
+ end
168
+
169
+ def clear_rules(environment: nil)
170
+ scoped(environment) do |k|
171
+ @environments[k] = (@environments[k] || FlagEnvironment.new).with(rules: [].freeze)
172
+ end
173
+ self
174
+ end
175
+
176
+ def set_default(value, environment:)
177
+ @environments[environment] = (@environments[environment] || FlagEnvironment.new).with(default: value)
178
+ self
179
+ end
180
+
181
+ def clear_default(environment:)
182
+ @environments[environment] = (@environments[environment] || FlagEnvironment.new).with(default: nil)
183
+ self
184
+ end
185
+
186
+ def add_value(flag_value)
187
+ @values ||= []
188
+ @values << flag_value
189
+ self
190
+ end
191
+
192
+ def remove_value(name)
193
+ return self unless @values
194
+
195
+ @values = @values.reject { |v| v.name == name }
196
+ self
197
+ end
198
+
199
+ def clear_values
200
+ @values = []
201
+ self
202
+ end
203
+
204
+ def _apply(other)
205
+ @id = other.id
206
+ @name = other.name
207
+ @type = other.type
208
+ @default = other.default
209
+ @description = other.description
210
+ @values = other.instance_variable_get(:@values)&.dup
211
+ @environments = other.instance_variable_get(:@environments).dup
212
+ @created_at = other.created_at
213
+ @updated_at = other.updated_at
214
+ end
215
+
216
+ private
217
+
218
+ def scoped(environment, &)
219
+ if environment.nil?
220
+ @environments.each_key(&)
221
+ else
222
+ yield(environment)
223
+ end
224
+ end
225
+ end
226
+
227
+ class BooleanFlag < Flag
228
+ def get(context: nil)
229
+ raw = @client._evaluate_handle(@id, @default, context)
230
+ !!raw
231
+ end
232
+ end
233
+
234
+ class StringFlag < Flag
235
+ def get(context: nil)
236
+ raw = @client._evaluate_handle(@id, @default, context)
237
+ raw.to_s
238
+ end
239
+ end
240
+
241
+ class NumberFlag < Flag
242
+ def get(context: nil)
243
+ @client._evaluate_handle(@id, @default, context)
244
+ end
245
+ end
246
+
247
+ class JsonFlag < Flag
248
+ def get(context: nil)
249
+ @client._evaluate_handle(@id, @default, context)
250
+ end
251
+ end
252
+ end
253
+
254
+ # Top-level re-exports for convenience.
255
+ FlagValue = Flags::FlagValue
256
+ FlagRule = Flags::FlagRule
257
+ FlagEnvironment = Flags::FlagEnvironment
258
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ # Operators supported by +Smplkit::Rule#when+.
5
+ #
6
+ # Customers should prefer +Smplkit::Op::EQ+ etc. over raw strings so the IDE
7
+ # can validate calls. Raw strings are still accepted for backward
8
+ # compatibility.
9
+ module Op
10
+ EQ = "=="
11
+ NEQ = "!="
12
+ LT = "<"
13
+ LTE = "<="
14
+ GT = ">"
15
+ GTE = ">="
16
+ IN = "in"
17
+ CONTAINS = "contains"
18
+
19
+ ALL = [EQ, NEQ, LT, LTE, GT, GTE, IN, CONTAINS].freeze
20
+ end
21
+
22
+ # A typed entity referenced by targeting rules and registered with smplkit.
23
+ #
24
+ # Represents a single entity (user, account, device, etc.). The +type+ and
25
+ # +key+ identify the entity; +attributes+ (provided as a hash and/or keyword
26
+ # arguments) carry the data that targeting rules evaluate against.
27
+ #
28
+ # Used for both authoring (+flag.get(context: [...])+,
29
+ # +client.set_context([...])+, +mgmt.contexts.register([...])+) and reading
30
+ # (+mgmt.contexts.list/get+ return populated +Context+ instances with
31
+ # +save+ / +delete+ ready to call).
32
+ #
33
+ # Examples:
34
+ #
35
+ # Smplkit::Context.new("user", "user-123", plan: "enterprise")
36
+ # Smplkit::Context.new("account", "acme-corp", { "region" => "us" }, employee_count: 500)
37
+ class Context
38
+ CONTEXT_FIELDS = %i[type key name attributes created_at updated_at].freeze
39
+
40
+ attr_reader :type, :key, :attributes
41
+ attr_accessor :name, :created_at, :updated_at
42
+
43
+ def initialize(type, key, attributes = nil, name: nil, created_at: nil, updated_at: nil, **kwargs)
44
+ raise TypeError, "Context type must be a String, got #{type.class}: #{type.inspect}" unless type.is_a?(String)
45
+ unless key.is_a?(String)
46
+ raise TypeError,
47
+ "Context key must be a String, got #{key.class}: #{key.inspect}. " \
48
+ "If your identifier is numeric, stringify it at the SDK boundary."
49
+ end
50
+
51
+ @type = type
52
+ @key = key
53
+ @name = name
54
+ @attributes = stringify_keys(merge_attributes(attributes, kwargs))
55
+ @created_at = created_at
56
+ @updated_at = updated_at
57
+ @client = nil
58
+ end
59
+
60
+ # Composite "{type}:{key}" identifier.
61
+ def id
62
+ "#{@type}:#{@key}"
63
+ end
64
+
65
+ # Bulk-replace attributes. Forces String keys to match the wire format.
66
+ def attributes=(new_attrs)
67
+ @attributes = stringify_keys(new_attrs || {})
68
+ end
69
+
70
+ # Internal: associate a management client with this context so save/delete
71
+ # can route through it.
72
+ def _bind_client(client)
73
+ @client = client
74
+ self
75
+ end
76
+
77
+ def _client
78
+ @client
79
+ end
80
+
81
+ # Persist this context to the server (create or update).
82
+ def save
83
+ raise "Context was constructed without a client; cannot save" if @client.nil?
84
+
85
+ updated = @client._save_context(self)
86
+ _apply(updated)
87
+ self
88
+ end
89
+ alias save! save
90
+
91
+ # Delete this context from the server.
92
+ def delete
93
+ raise "Context was constructed without a client; cannot delete" if @client.nil?
94
+
95
+ @client.delete(id)
96
+ end
97
+ alias delete! delete
98
+
99
+ def to_eval_hash
100
+ { "key" => @key, **@attributes }
101
+ end
102
+
103
+ def ==(other)
104
+ other.is_a?(Context) && other.type == @type && other.key == @key && other.attributes == @attributes
105
+ end
106
+
107
+ def hash
108
+ [@type, @key, @attributes].hash
109
+ end
110
+
111
+ def eql?(other)
112
+ self == other
113
+ end
114
+
115
+ def inspect
116
+ "#<Smplkit::Context type=#{@type.inspect} key=#{@key.inspect} " \
117
+ "name=#{@name.inspect} attributes=#{@attributes.inspect}>"
118
+ end
119
+
120
+ def _apply(other)
121
+ @type = other.type
122
+ @key = other.key
123
+ @name = other.name
124
+ @attributes = other.attributes.dup
125
+ @created_at = other.created_at
126
+ @updated_at = other.updated_at
127
+ end
128
+
129
+ private
130
+
131
+ def merge_attributes(attrs, kwargs)
132
+ base = attrs ? attrs.dup : {}
133
+ kwargs.each { |k, v| base[k] = v }
134
+ base
135
+ end
136
+
137
+ def stringify_keys(hash)
138
+ hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
139
+ end
140
+ end
141
+
142
+ # Describes a flag declaration for buffered registration.
143
+ #
144
+ # Used by +Smplkit::ManagementClient#flags#register+ to queue declarations
145
+ # for bulk registration. +service+ and +environment+ default to +nil+; the
146
+ # runtime client fills them from the active +Smplkit::Client+ when it
147
+ # forwards declarations.
148
+ class FlagDeclaration
149
+ attr_reader :id, :type, :default, :service, :environment
150
+
151
+ def initialize(id:, type:, default:, service: nil, environment: nil)
152
+ @id = id
153
+ @type = type
154
+ @default = default
155
+ @service = service
156
+ @environment = environment
157
+ freeze
158
+ end
159
+
160
+ def ==(other)
161
+ other.is_a?(FlagDeclaration) && id == other.id && type == other.type && default == other.default &&
162
+ service == other.service && environment == other.environment
163
+ end
164
+ alias eql? ==
165
+
166
+ def hash = [id, type, default, service, environment].hash
167
+ end
168
+
169
+ # Fluent builder for flag targeting rules.
170
+ #
171
+ # Smplkit::Rule.new("Enable for enterprise users", environment: "staging")
172
+ # .when("user.plan", Smplkit::Op::EQ, "enterprise")
173
+ # .serve(true)
174
+ #
175
+ # Multiple +.when+ calls are AND'd. +environment:+ is required so the target
176
+ # environment is unambiguous when the rule is passed to +Flag#add_rule+.
177
+ # +.serve+ finalizes the rule and returns the built Hash ready to pass to
178
+ # +add_rule+.
179
+ class Rule
180
+ def initialize(description, environment:)
181
+ @description = description
182
+ @environment = environment
183
+ @conditions = []
184
+ end
185
+
186
+ # Add a condition. Multiple calls are AND'd at the top level.
187
+ #
188
+ # Two forms:
189
+ # - +when(var, op, value)+ - convenience for simple comparisons.
190
+ # +op+ accepts an +Op+ constant (preferred) or a raw string
191
+ # (e.g. +"=="+, +"contains"+).
192
+ # - +when(expr)+ - escape hatch accepting an arbitrary JSON Logic
193
+ # expression (use this for OR, nested AND/OR, +if+, etc.).
194
+ # See https://jsonlogic.com/ for the full expression grammar.
195
+ def when(*args)
196
+ if args.length == 1 && args[0].is_a?(Hash)
197
+ @conditions << args[0]
198
+ return self
199
+ end
200
+
201
+ if args.length == 3
202
+ var, op, value = args
203
+ op_str = op.to_s
204
+ @conditions << if op_str == "contains"
205
+ { "in" => [value, { "var" => var }] }
206
+ else
207
+ { op_str => [{ "var" => var }, value] }
208
+ end
209
+ return self
210
+ end
211
+
212
+ raise ArgumentError,
213
+ "Rule#when takes either (var, op, value) or a single JSON Logic Hash; got args=#{args.inspect}"
214
+ end
215
+
216
+ # Finalize the rule with +value+ served on match and return the built Hash.
217
+ def serve(value)
218
+ logic =
219
+ case @conditions.length
220
+ when 0 then {}
221
+ when 1 then @conditions[0]
222
+ else { "and" => @conditions }
223
+ end
224
+
225
+ {
226
+ "description" => @description,
227
+ "logic" => logic,
228
+ "value" => value,
229
+ "environment" => @environment
230
+ }
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Smplkit
6
+ module Generators
7
+ # Generates +config/initializers/smplkit.rb+ for Rails apps.
8
+ #
9
+ # rails generate smplkit:install
10
+ class InstallGenerator < ::Rails::Generators::Base
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ def create_initializer_file
14
+ create_file "config/initializers/smplkit.rb", initializer_contents
15
+ end
16
+
17
+ def initializer_contents
18
+ <<~RUBY
19
+ # frozen_string_literal: true
20
+
21
+ # smplkit configuration. Anything you don't set here resolves through
22
+ # the standard SMPLKIT_* env vars or the ~/.smplkit profile file.
23
+ Rails.application.configure do
24
+ config.smplkit.environment = Rails.env
25
+ config.smplkit.service = "your-service-name"
26
+ # config.smplkit.api_key = ENV["SMPLKIT_API_KEY"]
27
+
28
+ # Optional: per-request context. The provider receives the Rack env
29
+ # and returns an Array of Smplkit::Context. Returning nil/[] is fine.
30
+ #
31
+ # config.smplkit.context_provider = ->(env) {
32
+ # user = env["warden"]&.user
33
+ # next [] unless user
34
+ #
35
+ # [Smplkit::Context.new("user", user.id.to_s, plan: user.plan)]
36
+ # }
37
+ end
38
+ RUBY
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Helpers
5
+ module_function
6
+
7
+ # Convert a slug-style key to a human-readable display name.
8
+ #
9
+ # key_to_display_name("checkout-v2") # => "Checkout V2"
10
+ # key_to_display_name("user_service") # => "User Service"
11
+ def key_to_display_name(key)
12
+ key.tr("-_", " ").split.map(&:capitalize).join(" ")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ # Log severity levels used by the Smpl Logging service.
5
+ #
6
+ # Acts as a string-valued enum: each constant equals its name when used in
7
+ # string contexts, and supports comparison via the +ordinal+.
8
+ class LogLevel
9
+ NAMES = %w[TRACE DEBUG INFO WARN ERROR FATAL SILENT].freeze
10
+
11
+ attr_reader :name, :ordinal
12
+
13
+ def initialize(name, ordinal)
14
+ @name = name.freeze
15
+ @ordinal = ordinal
16
+ freeze
17
+ end
18
+
19
+ def to_s = @name
20
+ def to_str = @name
21
+ def inspect = "#<Smplkit::LogLevel #{@name}>"
22
+ def ==(other) = other.is_a?(LogLevel) ? @ordinal == other.ordinal : @name == other
23
+ def hash = @ordinal.hash
24
+ def eql?(other) = self == other
25
+ def <=>(other) = other.is_a?(LogLevel) ? @ordinal <=> other.ordinal : nil
26
+
27
+ include Comparable
28
+
29
+ TRACE = new("TRACE", 0)
30
+ DEBUG = new("DEBUG", 1)
31
+ INFO = new("INFO", 2)
32
+ WARN = new("WARN", 3)
33
+ ERROR = new("ERROR", 4)
34
+ FATAL = new("FATAL", 5)
35
+ SILENT = new("SILENT", 6)
36
+
37
+ ALL = [TRACE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT].freeze
38
+
39
+ BY_NAME = ALL.to_h { |lvl| [lvl.name, lvl] }.freeze
40
+
41
+ def self.from_string(value)
42
+ raise ArgumentError, "log level cannot be nil" if value.nil?
43
+
44
+ key = value.to_s.upcase
45
+ level = BY_NAME[key]
46
+ raise ArgumentError, "unknown log level: #{value.inspect}" unless level
47
+
48
+ level
49
+ end
50
+
51
+ def self.coerce(value)
52
+ return value if value.is_a?(LogLevel)
53
+
54
+ from_string(value)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Logging
5
+ module Adapters
6
+ # Contract for pluggable logging framework integration.
7
+ #
8
+ # Adapters bridge the smplkit logging runtime to a specific logging
9
+ # framework (e.g., stdlib +Logger+, +semantic_logger+). Implement this
10
+ # interface to add support for a new logging framework.
11
+ class Base
12
+ # Human-readable adapter name for diagnostics
13
+ # (e.g., +"stdlib-logger"+).
14
+ def name
15
+ raise NotImplementedError
16
+ end
17
+
18
+ # Scan the runtime for existing loggers.
19
+ #
20
+ # Returns an Array of triples
21
+ # +[logger_name, explicit_level_or_nil, effective_level]+ where the
22
+ # levels are +Smplkit::LogLevel+ instances.
23
+ #
24
+ # - +explicit_level_or_nil+: the level the logger was explicitly
25
+ # set to, or +nil+ if it inherits from parent / framework default.
26
+ # - +effective_level+: the resolved level the framework uses for
27
+ # this logger, accounting for inheritance. Always non-nil.
28
+ def discover
29
+ raise NotImplementedError
30
+ end
31
+
32
+ # Set the level on a specific logger.
33
+ #
34
+ # +level+ is a +Smplkit::LogLevel+ instance; the adapter converts to
35
+ # its framework's native level representation.
36
+ def apply_level(_logger_name, _level)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # Install continuous discovery hook.
41
+ #
42
+ # The block is invoked with
43
+ # +(logger_name, explicit_level_or_nil, effective_level)+ whenever a
44
+ # new logger is created in the framework.
45
+ #
46
+ # May be a no-op if the framework doesn't support creation
47
+ # interception.
48
+ def install_hook(&)
49
+ raise NotImplementedError
50
+ end
51
+
52
+ # Remove the hook installed by +install_hook+. Called on
53
+ # +client.logging.close+.
54
+ def uninstall_hook
55
+ raise NotImplementedError
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Top-level re-export.
62
+ LoggingAdapter = Logging::Adapters::Base
63
+ end