Class: Smplkit::Logging::LoggingClient

Inherits:
Object
  • Object
show all
Defined in:
lib/smplkit/logging/client.rb

Overview

The Smpl Logging client (sync).

One client exposes the full surface, reachable as client.logging (Smplkit::Client) or constructed directly:

logging = Smplkit::LoggingClient.new(environment: "production", service: "my-svc")
logging.loggers.new("sqlalchemy.engine").save
logging.install

The CRUD surface (loggers / log_groups sub-clients) works immediately. register_adapter is a pre-install configuration call. The live surface (install / on_change / refresh) requires install first; calling on_change / refresh earlier raises NotInstalledError.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key = nil, environment: nil, base_url: nil, profile: nil, base_domain: nil, scheme: nil, debug: nil, extra_headers: nil, parent: nil, transport: nil, metrics: nil) ⇒ LoggingClient

Returns a new instance of LoggingClient.



426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/smplkit/logging/client.rb', line 426

def initialize(api_key = nil, environment: nil, base_url: nil, profile: nil,
               base_domain: nil, scheme: nil, debug: nil, extra_headers: nil,
               parent: nil, transport: nil, metrics: nil)
  @parent = parent
  @metrics = metrics
  @environment = parent.nil? ? environment : parent._environment
  @service = parent&._service
  @standalone_api_key = nil
  if transport.nil?
    @logging_http, _app_http, @app_base_url, @standalone_api_key = Logging.logging_transport(
      api_key: api_key, base_url: base_url, profile: profile,
      base_domain: base_domain, scheme: scheme, debug: debug, extra_headers: extra_headers
    )
  else
    @logging_http = transport
    @app_base_url = nil
  end

  # Discovery buffer is owned by this client; the loggers sub-client shares
  # it so discovery and explicit registration drain together.
  @buffer = LoggerRegistrationBuffer.new
  @loggers = LoggersClient.new(@logging_http, buffer: @buffer)
  @log_groups = LogGroupsClient.new(@logging_http)

  # Live-surface state.
  @connected = false
  @name_map = {}        # original_name → normalized_id
  @loggers_cache = {}   # id → logger data
  @groups_cache = {}    # id → group data
  @global_listeners = []
  @key_listeners = Hash.new { |h, k| h[k] = [] }
  @adapters = []
  @explicit_adapters = false
  @ws_manager = nil
  @owns_ws = false
  @lock = Mutex.new
end

Instance Attribute Details

#log_groupsObject (readonly)

Returns the value of attribute log_groups.



424
425
426
# File 'lib/smplkit/logging/client.rb', line 424

def log_groups
  @log_groups
end

#loggersObject (readonly)

Returns the value of attribute loggers.



424
425
426
# File 'lib/smplkit/logging/client.rb', line 424

def loggers
  @loggers
end

Class Method Details

.open(**kwargs) {|client| ... } ⇒ Object

Construct a LoggingClient, yield it to the block, and close it on exit.

Mirrors Ruby’s File.open block form: the client is closed automatically when the block returns or raises, so a standalone client’s owned transports and WebSocket are always torn down.

Smplkit::LoggingClient.open(environment: "production") do |logging|
  logging.loggers.new("sqlalchemy.engine").save
  logging.install
end

Parameters:

  • kwargs (Hash)

    keyword arguments forwarded to new.

Yield Parameters:

Returns:

  • (Object)

    the block’s return value.



625
626
627
628
629
630
631
632
# File 'lib/smplkit/logging/client.rb', line 625

def self.open(**kwargs)
  client = new(**kwargs)
  begin
    yield client
  ensure
    client.close
  end
end

Instance Method Details

#adaptersArray<Adapters::Base>

Registered logging adapters.

Returns:

  • (Array<Adapters::Base>)

    a copy of the adapters this client uses to discover loggers and apply levels.



486
487
488
# File 'lib/smplkit/logging/client.rb', line 486

def adapters
  @adapters.dup
end

#closeObject Also known as: _close

Release resources — only those this client owns.

Uninstalls the adapter hooks, unsubscribes from the WebSocket, and tears down the owned WebSocket (standalone install). A wired client borrows the parent’s transport and WebSocket and closes neither.



587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
# File 'lib/smplkit/logging/client.rb', line 587

def close
  Smplkit.debug("lifecycle", "LoggingClient.close() called")
  @adapters.each do |adapter|
    adapter.uninstall_hook
  rescue StandardError => e
    Smplkit.debug("logging", "adapter #{adapter.name} uninstall_hook failed: #{e.class}: #{e.message}")
  end
  if @ws_manager
    ws_handlers.each { |event, handler| @ws_manager.off(event, handler) }
    if @owns_ws
      @ws_manager.stop
      @owns_ws = false
    end
    @ws_manager = nil
  end
  @connected = false
end

#installObject

Hook smplkit into the application’s logging machinery.

Loads adapters, scans existing loggers, applies levels from the smplkit server, and wires WebSocket handlers for live updates. This IS the explicit consent gate — on_change / refresh require it first.

Idempotent — safe to call multiple times.



499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
# File 'lib/smplkit/logging/client.rb', line 499

def install
  Smplkit.debug("lifecycle", "LoggingClient.install() called")
  @parent&._ensure_started
  return self if @connected

  # 0. Load adapters
  @adapters = Logging.auto_load_adapters if @adapters.empty?

  # 1. Discover existing loggers from all adapters (keep discovery and hook
  # installation as two passes — discover every adapter before any hook is
  # live, mirroring the Python SDK).
  # rubocop:disable Style/CombinableLoops
  @adapters.each do |adapter|
    existing = adapter.discover
    existing.each do |name, explicit_level, effective_level|
      @name_map[name] = Normalize.normalize_logger_name(name)
      @loggers.register(loggersource_for(name, explicit_level, effective_level))
    end
  rescue StandardError => e
    Smplkit.debug("logging", "adapter #{adapter.name} discover failed: #{e.class}: #{e.message}")
  end

  # 2. Install continuous discovery hooks
  @adapters.each do |adapter|
    adapter.install_hook { |name, explicit, effective| on_new_logger(name, explicit, effective) }
  rescue StandardError => e
    Smplkit.debug("logging", "adapter #{adapter.name} install_hook failed: #{e.class}: #{e.message}")
  end
  # rubocop:enable Style/CombinableLoops

  # 3. Flush initial batch
  begin
    @loggers.flush
  rescue StandardError => e
    Smplkit.debug("registration", "bulk logger registration failed: #{e.class}: #{e.message}")
  end

  # 4-6. Fetch, resolve, apply
  begin
    fetch_and_apply(trigger: "install()")
  rescue StandardError => e
    Smplkit.debug("resolution",
                  "failed to fetch/apply logging levels during connect " \
                  "(logging: #{@logging_http&.config&.host}): #{e.class}: #{e.message}")
  end

  # 7. Register WebSocket event handlers for real-time level updates
  @ws_manager = ensure_ws
  ws_handlers.each { |event, handler| @ws_manager.on(event, &handler) }

  @connected = true
  self
end

#on_change(name = nil, &block) ⇒ Object

Register a change listener.

client.logging.on_change { |event| ... }                 # global
client.logging.on_change("sqlalchemy.engine") { |e| ... } # key-scoped

Requires install first; raises NotInstalledError otherwise.

Raises:

  • (ArgumentError)


561
562
563
564
565
566
567
568
569
570
571
# File 'lib/smplkit/logging/client.rb', line 561

def on_change(name = nil, &block)
  require_installed
  raise ArgumentError, "on_change requires a block" unless block

  if name.nil?
    @global_listeners << block
  else
    @key_listeners[name] << block
  end
  block
end

#refreshObject

Re-fetch all loggers and groups and fire listener events for any deltas.

Requires install first; raises NotInstalledError otherwise.



576
577
578
579
580
# File 'lib/smplkit/logging/client.rb', line 576

def refresh
  require_installed
  Smplkit.debug("resolution", "refresh() called, triggering full resolution pass")
  fetch_and_apply_deltas(trigger: "refresh()", source: "manual")
end

#register_adapter(adapter) ⇒ Object

Register a logging adapter. Must be called before install().

If called at least once, auto-loading is disabled — only explicitly registered adapters are used. This is a pre-install configuration call: it is intentionally NOT gated by install.



471
472
473
474
475
476
477
478
479
480
# File 'lib/smplkit/logging/client.rb', line 471

def register_adapter(adapter)
  raise "Cannot register adapters after install()" if @connected
  unless adapter.is_a?(Adapters::Base)
    raise ArgumentError, "adapter must implement Smplkit::Logging::Adapters::Base"
  end

  @explicit_adapters = true
  @adapters << adapter
  self
end