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,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is loaded conditionally by the LoggingClient — only when the
4
+ # customer's app has +require "semantic_logger"+. The +require+ at the top is
5
+ # safe because the file itself is only loaded after a successful
6
+ # +require "semantic_logger"+ elsewhere.
7
+ require "semantic_logger"
8
+ require "concurrent"
9
+
10
+ module Smplkit
11
+ module Logging
12
+ module Adapters
13
+ # Adapter for the +semantic_logger+ gem.
14
+ #
15
+ # SemanticLogger has its own internal logger registry and its own level
16
+ # system that natively includes TRACE — a 1-to-1 map across all seven
17
+ # smplkit canonical levels.
18
+ class SemanticLoggerAdapter < Base
19
+ def initialize
20
+ super
21
+ @loggers = Concurrent::Hash.new
22
+ @on_new = nil
23
+ @uninstalled = false
24
+ end
25
+
26
+ def name
27
+ "semantic-logger"
28
+ end
29
+
30
+ def track(name, logger)
31
+ @loggers[name] = logger
32
+ end
33
+
34
+ def discover
35
+ rows = []
36
+ # Default named loggers SemanticLogger creates: itself + the global
37
+ # one. Customers add more via +SemanticLogger[ClassOrName]+.
38
+ all_loggers.each do |name, logger|
39
+ level = logger.respond_to?(:level) ? logger.level : nil
40
+ smpl_level = Levels.semantic_level_to_smpl(level)
41
+ rows << [name, smpl_level, smpl_level]
42
+ end
43
+
44
+ rows.uniq { |row| row[0] }
45
+ end
46
+
47
+ def apply_level(logger_name, level)
48
+ logger = @loggers[logger_name]
49
+ return unless logger
50
+ return unless logger.respond_to?(:level=)
51
+
52
+ logger.level = Levels.smpl_level_to_semantic(level)
53
+ end
54
+
55
+ def install_hook(&on_new_logger)
56
+ @on_new = on_new_logger
57
+ @uninstalled = false
58
+ # SemanticLogger's API for new-logger interception varies across
59
+ # versions. The Ruby SDK initial release relies on +discover+ being
60
+ # called periodically — full prepend-based interception will be
61
+ # filled in once tested against the targeted +semantic_logger+
62
+ # version pinned in dev deps. (See ISSUES.md.)
63
+ end
64
+
65
+ def uninstall_hook
66
+ @uninstalled = true
67
+ end
68
+
69
+ private
70
+
71
+ def all_loggers
72
+ loggers = @loggers.dup
73
+ if defined?(::SemanticLogger::Logger) && ::SemanticLogger::Logger.respond_to?(:processors)
74
+ # No-op probe to keep this method tolerant of the live API.
75
+ end
76
+
77
+ if defined?(::SemanticLogger) &&
78
+ ::SemanticLogger.respond_to?(:default_level) &&
79
+ ::SemanticLogger.respond_to?(:[])
80
+ loggers["root"] ||= ::SemanticLogger["root"]
81
+ end
82
+
83
+ loggers
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "concurrent"
5
+
6
+ module Smplkit
7
+ module Logging
8
+ module Adapters
9
+ # Adapter for Ruby stdlib +Logger+ (which +ActiveSupport::Logger+
10
+ # subclasses, so Rails is covered automatically).
11
+ #
12
+ # Ruby has no global logger registry like Python's
13
+ # +logging.Logger.manager.loggerDict+. Instead this adapter:
14
+ #
15
+ # 1. Always reports +"root"+ at the framework default level.
16
+ # 2. Reports +"rails"+ if +Rails.logger+ is defined.
17
+ # 3. Reports any logger explicitly registered via +track+.
18
+ # 4. Uses +Module#prepend+ on +::Logger+ to catch new logger
19
+ # creation. The hook fires on every +Logger.new+ call.
20
+ class StdlibLoggerAdapter < Base
21
+ @hook_module = nil
22
+ @global_lock = Mutex.new
23
+
24
+ class << self
25
+ attr_reader :hook_module
26
+ end
27
+
28
+ def initialize
29
+ super
30
+ @loggers = Concurrent::Hash.new
31
+ track_root!
32
+ @on_new = nil
33
+ @uninstalled = false
34
+ end
35
+
36
+ def name
37
+ "stdlib-logger"
38
+ end
39
+
40
+ # Register an additional logger with the adapter so its level can be
41
+ # discovered and applied.
42
+ def track(logger_name, logger)
43
+ @loggers[logger_name] = logger
44
+ end
45
+
46
+ def discover
47
+ rows = []
48
+ @loggers.each_pair do |logger_name, logger|
49
+ level = logger.level
50
+ smpl_level = Levels.stdlib_level_to_smpl(level)
51
+ rows << [logger_name, smpl_level, smpl_level]
52
+ end
53
+
54
+ if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
55
+ track("rails", ::Rails.logger) unless @loggers.key?("rails")
56
+ level = ::Rails.logger.level
57
+ smpl_level = Levels.stdlib_level_to_smpl(level)
58
+ rows << ["rails", smpl_level, smpl_level]
59
+ end
60
+
61
+ rows.uniq { |row| row[0] }
62
+ end
63
+
64
+ def apply_level(logger_name, level)
65
+ logger = @loggers[logger_name]
66
+ return unless logger
67
+
68
+ native = Levels.smpl_level_to_stdlib(level)
69
+ logger.level = native
70
+ end
71
+
72
+ def install_hook(&on_new_logger)
73
+ @on_new = on_new_logger
74
+ @uninstalled = false
75
+
76
+ self.class.global_lock.synchronize do
77
+ unless self.class.hook_installed?
78
+ hook = self.class.build_hook
79
+ ::Logger.prepend(hook)
80
+ self.class.instance_variable_set(:@hook_module, hook)
81
+ end
82
+ self.class.adapters << self
83
+ end
84
+ end
85
+
86
+ def uninstall_hook
87
+ @uninstalled = true
88
+ self.class.global_lock.synchronize do
89
+ self.class.adapters.delete(self)
90
+ end
91
+ end
92
+
93
+ # Called by the prepended hook when a new Logger is created.
94
+ def on_new_logger_created(logger, name)
95
+ return if @uninstalled
96
+
97
+ track(name, logger)
98
+ smpl_level = Levels.stdlib_level_to_smpl(logger.level)
99
+ @on_new&.call(name, smpl_level, smpl_level)
100
+ end
101
+
102
+ class << self
103
+ def adapters
104
+ @adapters ||= Concurrent::Array.new
105
+ end
106
+
107
+ def global_lock
108
+ @global_lock ||= Mutex.new
109
+ end
110
+
111
+ def hook_installed?
112
+ !@hook_module.nil?
113
+ end
114
+
115
+ def build_hook
116
+ Module.new do
117
+ def initialize(*args, **kwargs)
118
+ super
119
+ StdlibLoggerAdapter.adapters.each do |adapter|
120
+ adapter.on_new_logger_created(self, "logger.#{object_id}")
121
+ rescue StandardError
122
+ # Swallow to keep Logger.new robust under the hook.
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ def reset_hook!
129
+ @hook_module = nil
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def track_root!
136
+ @loggers["root"] ||= ::Logger.new($stdout)
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ StdlibLoggerAdapter = Logging::Adapters::StdlibLoggerAdapter
143
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Smplkit
6
+ module Logging
7
+ # Synchronous logging runtime namespace.
8
+ #
9
+ # Obtained via +Smplkit::Client#logging+. Manages the discovery and level
10
+ # application for a customer's logging frameworks via pluggable adapters.
11
+ # CRUD has moved to +mgmt.loggers.*+ / +mgmt.log_groups.*+.
12
+ class LoggingClient
13
+ def initialize(parent, manage:, metrics:, logging_base_url:, app_base_url:)
14
+ @parent = parent
15
+ @manage = manage
16
+ @metrics = metrics
17
+ @logging_base_url = logging_base_url
18
+ @app_base_url = app_base_url
19
+ @adapters = []
20
+ @installed = false
21
+ @global_listeners = []
22
+ @key_listeners = Hash.new { |h, k| h[k] = [] }
23
+ @lock = Mutex.new
24
+ end
25
+
26
+ # Install the logging integration.
27
+ #
28
+ # Auto-loads the +stdlib-logger+ adapter (always) and the
29
+ # +semantic-logger+ adapter (when the gem is available). Customer
30
+ # explicit registration via +register_adapter+ wins over auto-load.
31
+ def install
32
+ return self if @installed
33
+
34
+ auto_load_adapters if @adapters.empty?
35
+
36
+ @adapters.each do |adapter|
37
+ discovered = adapter.discover
38
+ discovered.each { |(name, _explicit, effective)| observe_logger(adapter, name, effective) }
39
+ adapter.install_hook { |name, _explicit, effective| observe_logger(adapter, name, effective) }
40
+ end
41
+
42
+ @ws_manager = @parent._ensure_ws
43
+ @ws_manager.on("logger_changed") { |data| handle_logger_changed(data) }
44
+ @installed = true
45
+ self
46
+ end
47
+ alias start install
48
+
49
+ def register_adapter(adapter)
50
+ unless adapter.is_a?(Adapters::Base)
51
+ raise ArgumentError, "adapter must implement Smplkit::Logging::Adapters::Base"
52
+ end
53
+
54
+ @adapters << adapter
55
+ self
56
+ end
57
+
58
+ def adapters
59
+ @adapters.dup
60
+ end
61
+
62
+ def get(name)
63
+ @manage.loggers.get(name)
64
+ end
65
+
66
+ def list
67
+ @manage.loggers.list
68
+ end
69
+
70
+ def delete(name)
71
+ @manage.loggers.delete(name)
72
+ end
73
+
74
+ def on_change(name = nil, &block)
75
+ raise ArgumentError, "on_change requires a block" unless block
76
+
77
+ if name.nil?
78
+ @global_listeners << block
79
+ else
80
+ @key_listeners[Normalize.normalize_logger_name(name)] << block
81
+ end
82
+ block
83
+ end
84
+
85
+ def _close
86
+ @adapters.each(&:uninstall_hook) if @installed
87
+ @installed = false
88
+ end
89
+
90
+ private
91
+
92
+ def auto_load_adapters
93
+ @adapters << Adapters::StdlibLoggerAdapter.new
94
+
95
+ begin
96
+ require "semantic_logger"
97
+ require_relative "adapters/semantic_logger_adapter"
98
+ @adapters << Adapters::SemanticLoggerAdapter.new
99
+ rescue LoadError
100
+ Smplkit.debug("registration", "semantic_logger gem not installed; semantic-logger adapter skipped")
101
+ end
102
+
103
+ return unless @adapters.empty?
104
+
105
+ Smplkit.debug("registration", "no logging adapters loaded; runtime features disabled")
106
+ end
107
+
108
+ def observe_logger(_adapter, raw_name, level)
109
+ normalized = Normalize.normalize_logger_name(raw_name)
110
+ @manage.loggers.register(LoggerSource.new(
111
+ name: normalized,
112
+ resolved_level: level,
113
+ level: nil,
114
+ service: @parent._service,
115
+ environment: @parent._environment
116
+ ))
117
+ end
118
+
119
+ def handle_logger_changed(data)
120
+ name = Normalize.normalize_logger_name(data["name"] || data["id"] || "")
121
+ return if name.empty?
122
+
123
+ level = data["resolved_level"] || data["level"]
124
+ coerced = level && LogLevel.coerce(level)
125
+ @adapters.each { |a| a.apply_level(name, coerced) } if coerced
126
+
127
+ event = LoggerChangeEvent.new(name: name, level: coerced, source: "websocket")
128
+ (@global_listeners + @key_listeners[name]).each do |cb|
129
+ cb.call(event)
130
+ rescue StandardError => e
131
+ Smplkit.debug("logging", "listener raised: #{e.class}: #{e.message}")
132
+ end
133
+ end
134
+ end
135
+
136
+ LoggerChangeEvent = Struct.new(:name, :level, :source, keyword_init: true) do
137
+ def ==(other)
138
+ other.is_a?(LoggerChangeEvent) && name == other.name && level == other.level && source == other.source
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Logging
5
+ module Helpers
6
+ module_function
7
+
8
+ def logger_resource_to_model(client, resource)
9
+ attrs = resource["attributes"] || {}
10
+ SmplLogger.new(
11
+ client,
12
+ id: resource["id"] || attrs["id"],
13
+ name: attrs["name"],
14
+ resolved_level: attrs["resolved_level"] && LogLevel.coerce(attrs["resolved_level"]),
15
+ level: attrs["level"] && LogLevel.coerce(attrs["level"]),
16
+ service: attrs["service"],
17
+ environment: attrs["environment"],
18
+ log_group_id: attrs["log_group_id"],
19
+ managed: attrs.fetch("managed", true),
20
+ description: attrs["description"],
21
+ created_at: attrs["created_at"],
22
+ updated_at: attrs["updated_at"]
23
+ )
24
+ end
25
+
26
+ def log_group_resource_to_model(client, resource)
27
+ attrs = resource["attributes"] || {}
28
+ SmplLogGroup.new(
29
+ client,
30
+ id: resource["id"] || attrs["id"],
31
+ key: attrs["key"] || resource["id"],
32
+ name: attrs["name"],
33
+ level: attrs["level"] && LogLevel.coerce(attrs["level"]),
34
+ description: attrs["description"],
35
+ parent_id: attrs["parent_id"],
36
+ environments: attrs["environments"] || {},
37
+ created_at: attrs["created_at"],
38
+ updated_at: attrs["updated_at"]
39
+ )
40
+ end
41
+
42
+ def build_logger_body(logger)
43
+ attributes = {
44
+ "name" => logger.name,
45
+ "resolved_level" => logger.resolved_level&.to_s,
46
+ "level" => logger.level&.to_s,
47
+ "service" => logger.service,
48
+ "environment" => logger.environment,
49
+ "log_group_id" => logger.log_group_id,
50
+ "managed" => logger.managed,
51
+ "description" => logger.description
52
+ }.compact
53
+ { "data" => { "type" => "logger", "id" => logger.id, "attributes" => attributes } }
54
+ end
55
+
56
+ def build_log_group_body(group)
57
+ attributes = {
58
+ "key" => group.key,
59
+ "name" => group.name,
60
+ "level" => group.level&.to_s,
61
+ "description" => group.description,
62
+ "parent_id" => group.parent_id,
63
+ "environments" => group.environments
64
+ }.compact
65
+ { "data" => { "type" => "log_group", "id" => group.key, "attributes" => attributes } }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require_relative "../log_level"
5
+
6
+ module Smplkit
7
+ module Logging
8
+ # Bidirectional mapping between Ruby stdlib +Logger+ levels and smplkit
9
+ # canonical levels (per ADR-046 §2.3).
10
+ #
11
+ # Stdlib +Logger+ has DEBUG/INFO/WARN/ERROR/FATAL/UNKNOWN — no TRACE. The
12
+ # +stdlib-logger+ adapter maps smplkit TRACE to stdlib DEBUG when
13
+ # applying levels, and maps stdlib DEBUG to smplkit DEBUG when
14
+ # discovering — there is no way to distinguish smplkit-TRACE-mapped-to-
15
+ # DEBUG from genuine DEBUG, which is consistent with how the Python
16
+ # +stdlib-logging+ adapter handles the same gap.
17
+ module Levels
18
+ STDLIB_TO_SMPL = {
19
+ ::Logger::DEBUG => LogLevel::DEBUG,
20
+ ::Logger::INFO => LogLevel::INFO,
21
+ ::Logger::WARN => LogLevel::WARN,
22
+ ::Logger::ERROR => LogLevel::ERROR,
23
+ ::Logger::FATAL => LogLevel::FATAL,
24
+ ::Logger::UNKNOWN => LogLevel::SILENT
25
+ }.freeze
26
+
27
+ SMPL_TO_STDLIB = {
28
+ LogLevel::TRACE => ::Logger::DEBUG,
29
+ LogLevel::DEBUG => ::Logger::DEBUG,
30
+ LogLevel::INFO => ::Logger::INFO,
31
+ LogLevel::WARN => ::Logger::WARN,
32
+ LogLevel::ERROR => ::Logger::ERROR,
33
+ LogLevel::FATAL => ::Logger::FATAL,
34
+ LogLevel::SILENT => ::Logger::UNKNOWN
35
+ }.freeze
36
+
37
+ module_function
38
+
39
+ def stdlib_level_to_smpl(level)
40
+ return LogLevel::DEBUG if level.nil?
41
+
42
+ STDLIB_TO_SMPL[level] || nearest_smpl_for(level)
43
+ end
44
+
45
+ def smpl_level_to_stdlib(level)
46
+ coerced = LogLevel.coerce(level)
47
+ SMPL_TO_STDLIB.fetch(coerced)
48
+ end
49
+
50
+ # SemanticLogger's level system natively includes TRACE — a 1-to-1 map.
51
+ SEMANTIC_TO_SMPL = {
52
+ trace: LogLevel::TRACE,
53
+ debug: LogLevel::DEBUG,
54
+ info: LogLevel::INFO,
55
+ warn: LogLevel::WARN,
56
+ error: LogLevel::ERROR,
57
+ fatal: LogLevel::FATAL
58
+ }.freeze
59
+
60
+ SMPL_TO_SEMANTIC = SEMANTIC_TO_SMPL.invert.freeze
61
+
62
+ def semantic_level_to_smpl(level)
63
+ return LogLevel::INFO if level.nil?
64
+
65
+ SEMANTIC_TO_SMPL[level.to_sym] || LogLevel::INFO
66
+ end
67
+
68
+ def smpl_level_to_semantic(level)
69
+ coerced = LogLevel.coerce(level)
70
+ # SemanticLogger has no SILENT — closest equivalent is :fatal.
71
+ SMPL_TO_SEMANTIC[coerced] || :fatal
72
+ end
73
+
74
+ def nearest_smpl_for(stdlib_level)
75
+ sorted = STDLIB_TO_SMPL.keys.sort
76
+ best = sorted.first
77
+ sorted.each do |bp|
78
+ break if bp > stdlib_level
79
+
80
+ best = bp
81
+ end
82
+ STDLIB_TO_SMPL[best]
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Logging
5
+ # A logger resource managed by the smplkit Logging service.
6
+ #
7
+ # Attributes:
8
+ # - id, name: identity
9
+ # - resolved_level: effective level computed by the platform from
10
+ # environment overrides + log group inheritance
11
+ # - level: explicit override (nil means inherit)
12
+ # - service, environment: provenance
13
+ # - log_group_id: parent log group, if any
14
+ # - managed: whether the SDK should apply server-driven level changes
15
+ class SmplLogger
16
+ attr_accessor :id, :name, :resolved_level, :level, :service, :environment,
17
+ :log_group_id, :managed, :created_at, :updated_at, :description
18
+
19
+ def initialize(client = nil, name:, resolved_level:, id: nil, level: nil,
20
+ service: nil, environment: nil, log_group_id: nil,
21
+ managed: true, description: nil, created_at: nil, updated_at: nil)
22
+ @client = client
23
+ @id = id
24
+ @name = name
25
+ @resolved_level = resolved_level
26
+ @level = level
27
+ @service = service
28
+ @environment = environment
29
+ @log_group_id = log_group_id
30
+ @managed = managed
31
+ @description = description
32
+ @created_at = created_at
33
+ @updated_at = updated_at
34
+ end
35
+
36
+ def managed? = !!@managed
37
+
38
+ def save
39
+ raise "SmplLogger was constructed without a client; cannot save" if @client.nil?
40
+
41
+ updated = @client._update_logger(self)
42
+ _apply(updated)
43
+ self
44
+ end
45
+ alias save! save
46
+
47
+ def delete
48
+ raise "SmplLogger was constructed without a client; cannot delete" if @client.nil?
49
+
50
+ @client.delete(@id || @name)
51
+ end
52
+ alias delete! delete
53
+
54
+ def _apply(other)
55
+ @id = other.id
56
+ @name = other.name
57
+ @resolved_level = other.resolved_level
58
+ @level = other.level
59
+ @service = other.service
60
+ @environment = other.environment
61
+ @log_group_id = other.log_group_id
62
+ @managed = other.managed
63
+ @description = other.description
64
+ @created_at = other.created_at
65
+ @updated_at = other.updated_at
66
+ end
67
+ end
68
+
69
+ # A log group resource — a hierarchical bag of loggers with a shared
70
+ # configured level.
71
+ class SmplLogGroup
72
+ attr_accessor :id, :key, :name, :level, :description, :parent_id, :environments,
73
+ :created_at, :updated_at
74
+
75
+ def initialize(client = nil, key:, id: nil, name: nil, level: nil,
76
+ description: nil, parent_id: nil, environments: nil,
77
+ created_at: nil, updated_at: nil)
78
+ @client = client
79
+ @id = id
80
+ @key = key
81
+ @name = name
82
+ @level = level
83
+ @description = description
84
+ @parent_id = parent_id
85
+ @environments = environments || {}
86
+ @created_at = created_at
87
+ @updated_at = updated_at
88
+ end
89
+
90
+ def save
91
+ raise "SmplLogGroup was constructed without a client; cannot save" if @client.nil?
92
+
93
+ updated =
94
+ if @created_at.nil?
95
+ @client._create_log_group(self)
96
+ else
97
+ @client._update_log_group(self)
98
+ end
99
+ _apply(updated)
100
+ self
101
+ end
102
+ alias save! save
103
+
104
+ def delete
105
+ raise "SmplLogGroup was constructed without a client; cannot delete" if @client.nil?
106
+
107
+ @client.delete(@key)
108
+ end
109
+ alias delete! delete
110
+
111
+ def _apply(other)
112
+ @id = other.id
113
+ @key = other.key
114
+ @name = other.name
115
+ @level = other.level
116
+ @description = other.description
117
+ @parent_id = other.parent_id
118
+ @environments = other.environments
119
+ @created_at = other.created_at
120
+ @updated_at = other.updated_at
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Logging
5
+ # Logger name normalization per ADR-034 §5.
6
+ #
7
+ # Replace +/+ with +.+, replace +:+ with +.+, lowercase everything.
8
+ module Normalize
9
+ module_function
10
+
11
+ def normalize_logger_name(name)
12
+ name.to_s.tr("/:", "..").downcase
13
+ end
14
+ end
15
+ end
16
+ end