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
-
.install!(meter_provider) ⇒ Object
Registers the asynchronous instruments on
meter_provider(foam's own — init gates this on foam owning the metrics slot). - .reset_for_tests! ⇒ Object
-
.rss_bytes ⇒ Object
Current RSS in bytes from /proc/self/statm, or nil off-Linux / on any read failure (fail-to-dark).
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.}") end @installed_provider_id = meter_provider.object_id true rescue StandardError => e Diagnostics.warn("runtime metrics failed to install: #{e.class}: #{e.}") 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_bytes ⇒ Object
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 |