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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +105 -0
- data/lib/smplkit/client.rb +218 -0
- data/lib/smplkit/config/client.rb +238 -0
- data/lib/smplkit/config/helpers.rb +108 -0
- data/lib/smplkit/config/models.rb +192 -0
- data/lib/smplkit/config_resolution.rb +202 -0
- data/lib/smplkit/context.rb +68 -0
- data/lib/smplkit/debug.rb +50 -0
- data/lib/smplkit/errors.rb +114 -0
- data/lib/smplkit/flags/client.rb +480 -0
- data/lib/smplkit/flags/helpers.rb +76 -0
- data/lib/smplkit/flags/models.rb +258 -0
- data/lib/smplkit/flags/types.rb +233 -0
- data/lib/smplkit/generators/install_generator.rb +42 -0
- data/lib/smplkit/helpers.rb +15 -0
- data/lib/smplkit/log_level.rb +57 -0
- data/lib/smplkit/logging/adapters/base.rb +63 -0
- data/lib/smplkit/logging/adapters/semantic_logger_adapter.rb +88 -0
- data/lib/smplkit/logging/adapters/stdlib_logger_adapter.rb +143 -0
- data/lib/smplkit/logging/client.rb +142 -0
- data/lib/smplkit/logging/helpers.rb +69 -0
- data/lib/smplkit/logging/levels.rb +86 -0
- data/lib/smplkit/logging/models.rb +124 -0
- data/lib/smplkit/logging/normalize.rb +16 -0
- data/lib/smplkit/logging/sources.rb +44 -0
- data/lib/smplkit/management/buffer.rb +111 -0
- data/lib/smplkit/management/client.rb +623 -0
- data/lib/smplkit/management/models.rb +133 -0
- data/lib/smplkit/management/types.rb +65 -0
- data/lib/smplkit/metrics.rb +78 -0
- data/lib/smplkit/railtie.rb +48 -0
- data/lib/smplkit/version.rb +5 -0
- data/lib/smplkit/ws.rb +92 -0
- data/lib/smplkit.rb +43 -0
- data/sig/smplkit.rbs +141 -0
- 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
|