class Smplkit::Logging::LoggingClient
actually moved; the deleted key itself emits nothing.
listener invocations for dependents whose computed effective level
deletion-flavored events — logger / group deletions only emit
listener for that id. There are no batch / summary events and no
per logger), each invocation also fires every matching key-scoped
loggers’ effective levels invokes the global listener N times (once
corresponds to exactly one adapter apply. A trigger that moves N
listener notification for that logger, and every notification
+adapter.apply_level(logger_id, new_level)+ is paired with exactly one
Change-listener contract — every call the SDK makes to
level. See {Smplkit::Logging::Resolution}.
dot-notation ancestry) to compute each managed logger’s effective
and the SDK walks the chain (env override → base → group chain →
Level resolution is client-side: the server stores raw configuration
CRUD has moved to mgmt.loggers.* / mgmt.log_groups.*.
application for a customer’s logging frameworks via pluggable adapters.
Obtained via +Smplkit::Client#logging+. Manages the discovery and level
Synchronous logging runtime namespace.
def _close
def _close @adapters.each(&:uninstall_hook) if @installed @installed = false end
def adapters
def adapters @adapters.dup end
def apply_levels(source: "websocket")
listener notification, and no notification fires without a
key-scoped listener. No adapter push happens without a paired
value: push to adapters, then fire each global + matching
freshly-computed effective level differs from the last applied
notifications. For every locally-tracked managed logger whose
Apply newly-resolved levels in lockstep with listener
def apply_levels(source: "websocket") @name_map.each do |raw_name, normalized_id| entry = @loggers_cache[normalized_id] next if entry.nil? next unless entry["managed"] resolved_string = Resolution.resolve_level( normalized_id, @parent._environment, @loggers_cache, @groups_cache ) previous = @resolved_levels[normalized_id] next if previous == resolved_string coerced = LogLevel.coerce(resolved_string) @resolved_levels[normalized_id] = resolved_string push_to_adapters(raw_name, coerced) fire_change_event(normalized_id, coerced, source: source) end end
def auto_load_adapters
def auto_load_adapters @adapters << Adapters::StdlibLoggerAdapter.new begin require "semantic_logger" require_relative "adapters/semantic_logger_adapter" @adapters << Adapters::SemanticLoggerAdapter.new rescue LoadError Smplkit.debug("registration", "semantic_logger gem not installed; semantic-logger adapter skipped") end return unless @adapters.empty? # Defensive log — unreachable in practice because stdlib +Logger+ # is always present, so +StdlibLoggerAdapter+ is always # constructible. # :nocov: Smplkit.debug("registration", "no logging adapters loaded; runtime features disabled") # :nocov: end
def delete(name)
def delete(name) @manage.loggers.delete(name) end
def fetch_and_apply(trigger:, source: "websocket")
def fetch_and_apply(trigger:, source: "websocket") Smplkit.debug("resolution", "full resolution pass starting (trigger: #{trigger})") loggers = @manage.loggers.list_logger_entries groups = @manage.log_groups.list_group_entries @loggers_cache = loggers @groups_cache = groups apply_levels(source: source) rescue StandardError => e Smplkit.debug("resolution", "fetch_and_apply failed (trigger: #{trigger}): #{e.class}: #{e.message}") end
def fire_change_event(normalized_id, level, source:)
def fire_change_event(normalized_id, level, source:) event = LoggerChangeEvent.new(name: normalized_id, level: level, source: source) (@global_listeners + @key_listeners[normalized_id]).each do |cb| cb.call(event) rescue StandardError => e Smplkit.debug("logging", "listener raised: #{e.class}: #{e.message}") end end
def flush_initial_registration
def flush_initial_registration @manage.loggers.flush rescue StandardError => e Smplkit.debug("registration", "initial logger flush failed: #{e.class}: #{e.message}") end
def get(name)
def get(name) @manage.loggers.get(name) end
def handle_group_changed(data)
def handle_group_changed(data) key = data["id"] || data["key"] || "" return if key.to_s.empty? begin entry_id, entry = @manage.log_groups.get_group_entry(key) @groups_cache[entry_id || key] = entry rescue StandardError => e Smplkit.debug("websocket", "group_changed fetch failed for #{key.inspect}: #{e.class}: #{e.message}") return end apply_levels(source: "websocket") end
def handle_group_deleted(data)
def handle_group_deleted(data) key = data["id"] || data["key"] || "" return if key.to_s.empty? @groups_cache.delete(key) apply_levels(source: "websocket") end
def handle_logger_changed(data)
def handle_logger_changed(data) key = data["id"] || data["name"] || "" normalized = Normalize.normalize_logger_name(key) return if normalized.empty? begin entry_id, entry = @manage.loggers.get_logger_entry(normalized) @loggers_cache[entry_id || normalized] = entry rescue StandardError => e Smplkit.debug("websocket", "logger_changed fetch failed for #{normalized.inspect}: #{e.class}: #{e.message}") return end apply_levels(source: "websocket") end
def handle_logger_deleted(data)
nothing; dependents whose effective level moves fire through the
Deletion is a pure cache eviction. The deleted key itself fires
def handle_logger_deleted(data) key = data["id"] || data["name"] || "" normalized = Normalize.normalize_logger_name(key) return if normalized.empty? @loggers_cache.delete(normalized) apply_levels(source: "websocket") end
def handle_loggers_changed(_data)
def handle_loggers_changed(_data) fetch_and_apply(trigger: "loggers_changed WS event") end
def initialize(parent, manage:, metrics:, logging_base_url:, app_base_url:)
def initialize(parent, manage:, metrics:, logging_base_url:, app_base_url:) @parent = parent @manage = manage @metrics = metrics @logging_base_url = logging_base_url @app_base_url = app_base_url @adapters = [] @installed = false @global_listeners = [] @key_listeners = Hash.new { |h, k| h[k] = [] } @lock = Mutex.new # original_name → normalized_id for every adapter-discovered logger. # We keep originals so adapter.apply_level receives whatever the # framework's registry indexes by. @name_map = {} # normalized_id → resolution-cache entry. @loggers_cache = {} # group id → resolution-cache entry. Without this, any managed # logger with +level=null+ that inherits from a group silently # keeps whatever level its adapter had at startup. @groups_cache = {} # normalized_id → last-applied resolved level (string). Drives the # lockstep between adapter.apply_level and listener notifications: # we only push (and fire) when the freshly-resolved level differs # from what's recorded here. @resolved_levels = {} end
def install
+semantic-logger+ adapter (when the gem is available). Customer
Auto-loads the +stdlib-logger+ adapter (always) and the
Install the logging integration.
def install return self if @installed auto_load_adapters if @adapters.empty? @adapters.each do |adapter| discovered = adapter.discover discovered.each { |(name, _explicit, effective)| observe_logger(adapter, name, effective) } adapter.install_hook { |name, _explicit, effective| observe_logger(adapter, name, effective) } end flush_initial_registration fetch_and_apply(trigger: "install", source: "manual") @ws_manager = @parent._ensure_ws @ws_manager.on("logger_changed") { |data| handle_logger_changed(data) } @ws_manager.on("logger_deleted") { |data| handle_logger_deleted(data) } @ws_manager.on("group_changed") { |data| handle_group_changed(data) } @ws_manager.on("group_deleted") { |data| handle_group_deleted(data) } @ws_manager.on("loggers_changed") { |data| handle_loggers_changed(data) } @installed = true self end
def list(page_number: nil, page_size: nil)
def list(page_number: nil, page_size: nil) @manage.loggers.list(page_number: page_number, page_size: page_size) end
def observe_logger(_adapter, raw_name, level)
def observe_logger(_adapter, raw_name, level) normalized = Normalize.normalize_logger_name(raw_name) @name_map[raw_name] = normalized @manage.loggers.register(LoggerSource.new( name: normalized, resolved_level: level, level: nil, service: @parent._service, environment: @parent._environment )) end
def on_change(name = nil, &block)
def on_change(name = nil, &block) raise ArgumentError, "on_change requires a block" unless block if name.nil? @global_listeners << block else @key_listeners[Normalize.normalize_logger_name(name)] << block end block end
def push_to_adapters(raw_name, coerced_level)
def push_to_adapters(raw_name, coerced_level) @adapters.each do |a| a.apply_level(raw_name, coerced_level) rescue StandardError => e Smplkit.debug("logging", "adapter apply_level raised: #{e.class}: #{e.message}") end end
def refresh
Re-fetch all loggers and groups and re-apply resolved levels. Fires
def refresh fetch_and_apply(trigger: "refresh", source: "manual") end
def register_adapter(adapter)
def register_adapter(adapter) unless adapter.is_a?(Adapters::Base) raise ArgumentError, "adapter must implement Smplkit::Logging::Adapters::Base" end @adapters << adapter self end