Module: Foam::Otel::RuntimeMetrics

Defined in:
lib/foam/otel/runtime_metrics.rb

Overview

Hand-written Ruby runtime + GC metrics (raised-floor ruling 2026-07-26). Ruby has NO drop-in upstream runtime-metrics gem (the js/java floors ride @opentelemetry/host-metrics / runtime-telemetry-java8; the contrib registry has no Ruby equivalent), so foam ships its own collector: asynchronous instruments whose callbacks read GC.stat / Thread.list / /proc at COLLECT time, on the PeriodicMetricReader's schedule and thread — no extra timer thread, nothing to restart after a fork beyond the reader foam's fork hook already restarts (GOTCHAS R1).

Names follow the process.runtime.runtime pattern the fleet's python package emits (process.runtime.cpython.* from the official system-metrics instrumentation). Every callback is fail-to-dark: a missing GC.stat key or an unreadable /proc returns nil (the SDK ignores non-numeric callback results), never a raise into the collect loop.

Constant Summary collapse

INSTRUMENTS =

[instrument kind, name, unit, GC.stat key or reader proc]

[
  [:observable_counter, "process.runtime.ruby.gc.count", "{collection}",
   -> { GC.count }].freeze,
  [:observable_counter, "process.runtime.ruby.gc.minor_count", "{collection}",
   -> { GC.stat(:minor_gc_count) }].freeze,
  [:observable_counter, "process.runtime.ruby.gc.major_count", "{collection}",
   -> { GC.stat(:major_gc_count) }].freeze,
  # GC total time: milliseconds, present when GC.measure_total_time is
  # on (the Ruby >= 3.1 default). Absent → the gauge stays dark.
  [:observable_counter, "process.runtime.ruby.gc.duration", "ms",
   -> { GC.stat[:time] }].freeze,
  [:observable_gauge, "process.runtime.ruby.gc.heap_live_slots", "{slot}",
   -> { GC.stat(:heap_live_slots) }].freeze,
  [:observable_gauge, "process.runtime.ruby.gc.heap_free_slots", "{slot}",
   -> { GC.stat(:heap_free_slots) }].freeze,
  [:observable_counter, "process.runtime.ruby.gc.allocated_objects", "{object}",
   -> { GC.stat(:total_allocated_objects) }].freeze,
  [:observable_counter, "process.runtime.ruby.gc.freed_objects", "{object}",
   -> { GC.stat(:total_freed_objects) }].freeze,
  [:observable_gauge, "process.runtime.ruby.threads.count", "{thread}",
   -> { Thread.list.size }].freeze,
  # RSS via /proc (stable semconv name — process.memory.usage). Linux
  # only; elsewhere the callback returns nil and the stream stays dark.
  [:observable_gauge, "process.memory.usage", "By",
   -> { RuntimeMetrics.rss_bytes }].freeze,
].freeze

Class Method Summary collapse

Class Method Details

.install!(meter_provider) ⇒ Object

Registers the asynchronous instruments on meter_provider (foam's own — init gates this on foam owning the metrics slot). Idempotent per provider: a post-fork re-init builds a NEW provider and re-registers; a duplicate init on the SAME provider is a no-op (re-creating an instrument re-registers it under the SDK mutex and warns — the same reason metrics.rb caches).



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/foam/otel/runtime_metrics.rb', line 57

def install!(meter_provider)
  return false if meter_provider.nil?
  return true if @installed_provider_id == meter_provider.object_id

  meter = meter_provider.meter("foam-runtime-metrics")
  INSTRUMENTS.each do |kind, name, unit, reader|
    callback = guarded_callback(reader)
    case kind
    when :observable_counter
      meter.create_observable_counter(name, unit: unit, callback: callback)
    when :observable_gauge
      meter.create_observable_gauge(name, unit: unit, callback: callback)
    end
  rescue StandardError => e
    Diagnostics.warn("runtime metric #{name} failed to register: #{e.class}: #{e.message}")
  end
  @installed_provider_id = meter_provider.object_id
  true
rescue StandardError => e
  Diagnostics.warn("runtime metrics failed to install: #{e.class}: #{e.message}")
  false
end

.reset_for_tests!Object



80
81
82
# File 'lib/foam/otel/runtime_metrics.rb', line 80

def reset_for_tests!
  @installed_provider_id = nil
end

.rss_bytesObject

Current RSS in bytes from /proc/self/statm, or nil off-Linux / on any read failure (fail-to-dark).



86
87
88
89
90
91
# File 'lib/foam/otel/runtime_metrics.rb', line 86

def rss_bytes
  pages = File.read("/proc/self/statm").split[1].to_i
  pages * page_size
rescue StandardError
  nil
end