diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index 97b9ed2..37b0af7 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -53,6 +53,28 @@ Assign the handle from the registry to an instance variable (or replace it when Each call to `registry.metric(:field)` returns the **same** cached instance for that field. Setting `registry.observer = …` updates every metric the registry already holds, so you normally keep using the same handle. Fetch a new metric only when you use a different registry or a different field name. +## Namespaces + +Use namespaces when several components expose the same generic field names but need distinct fields in the shared registry: + +```ruby +registry = Async::Utilization::Registry.new + +socket_accept = registry.namespace(:socket_accept) +long_task = registry.namespace(:long_task) + +socket_accept.metric(:acquired_count).increment +long_task.metric(:acquired_count).increment + +registry.values +# => { +# socket_accept_acquired_count: 1, +# long_task_acquired_count: 1 +# } +``` + +A namespace is registry-like: pass it to code that expects an object responding to `metric(name)`. Nested namespaces compose names with underscores. + ## With Shared Memory Observer When you need to share metrics with other processes (like a supervisor monitoring worker health), you can set up a shared memory observer: diff --git a/lib/async/utilization.rb b/lib/async/utilization.rb index 1ed47c4..5f1569c 100644 --- a/lib/async/utilization.rb +++ b/lib/async/utilization.rb @@ -5,6 +5,7 @@ require_relative "utilization/version" require_relative "utilization/schema" +require_relative "utilization/namespace" require_relative "utilization/registry" require_relative "utilization/observer" require_relative "utilization/metric" diff --git a/lib/async/utilization/metric.rb b/lib/async/utilization/metric.rb index 717b257..34667da 100644 --- a/lib/async/utilization/metric.rb +++ b/lib/async/utilization/metric.rb @@ -38,6 +38,9 @@ def initialize(name) # @attribute [Symbol] The field name for this metric. attr :name + # Get the current in-memory metric value. + # + # @returns [Numeric] The last value written to this metric. def value @guard.synchronize do @value diff --git a/lib/async/utilization/namespace.rb b/lib/async/utilization/namespace.rb new file mode 100644 index 0000000..81c0421 --- /dev/null +++ b/lib/async/utilization/namespace.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +module Async + module Utilization + # A registry-like view that prefixes metric names. + # + # Namespaces let components use generic metric names while applications decide + # how those names are composed in the shared registry. + class Namespace + # Initialize a new namespace. + # + # @parameter registry [Registry] The underlying registry. + # @parameter name [Symbol] The namespace name. + def initialize(registry, name) + @registry = registry + @name = name.to_sym + end + + # @attribute [Registry] The underlying registry. + attr :registry + + # @attribute [Symbol] The namespace name. + attr :name + + # Get a metric in this namespace. + # + # @parameter name [Symbol] The metric name. + # @returns [Metric] A metric instance for the namespaced field. + def metric(name) + @registry.metric(metric_name(name)) + end + + # Get a nested namespace. + # + # @parameter name [Symbol] The nested namespace name. + # @returns [Namespace] A namespace view with the composed name. + def namespace(name) + self.class.new(@registry, metric_name(name)) + end + + private + + def metric_name(name) + :"#{@name}_#{name}" + end + end + end +end diff --git a/lib/async/utilization/registry.rb b/lib/async/utilization/registry.rb index 7e24394..cb07df9 100644 --- a/lib/async/utilization/registry.rb +++ b/lib/async/utilization/registry.rb @@ -4,6 +4,7 @@ # Copyright, 2026, by Samuel Williams. require "console" +require_relative "namespace" module Async module Utilization @@ -93,6 +94,14 @@ def metric(field) @metrics[field] ||= Metric.for(field, @observer) end end + + # Get a namespace view of this registry. + # + # @parameter name [Symbol] The namespace name. + # @returns [Namespace] A registry-like namespace view. + def namespace(name) + Namespace.new(self, name) + end end end end diff --git a/releases.md b/releases.md index be10626..bcc3815 100644 --- a/releases.md +++ b/releases.md @@ -2,6 +2,7 @@ ## Unreleased + - Add `Async::Utilization::Namespace` for composing registry metric names. - `Async::Utilization::Metric` is the primary interface, remove `#set`, `#increment`, `#decrement` and `#track` from `Registry`. ## v0.3.2 diff --git a/test/async/utilization/namespace.rb b/test/async/utilization/namespace.rb new file mode 100644 index 0000000..514221f --- /dev/null +++ b/test/async/utilization/namespace.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "sus" +require "async/utilization" + +describe Async::Utilization::Namespace do + let(:registry) {Async::Utilization::Registry.new} + let(:namespace) {registry.namespace(:socket_accept)} + + it "uses namespaced metric names" do + metric = namespace.metric(:acquired_count) + + expect(metric).to be_a(Async::Utilization::Metric) + expect(metric.name).to be == :socket_accept_acquired_count + + metric.set(2) + expect(registry.values).to have_keys(socket_accept_acquired_count: be == 2) + end + + it "returns the same metric instance for the same namespaced field" do + metric1 = namespace.metric(:waiting_count) + metric2 = namespace.metric(:waiting_count) + + expect(metric1).to be == metric2 + end + + it "supports nested namespaces" do + metric = namespace.namespace(:long_task).metric(:waiting_count) + + expect(metric.name).to be == :socket_accept_long_task_waiting_count + + metric.increment + expect(registry.values).to have_keys(socket_accept_long_task_waiting_count: be == 1) + end + + it "writes namespaced metrics to an observer" do + schema = Async::Utilization::Schema.build(socket_accept_acquired_count: :u64) + buffer = IO::Buffer.new(8) + + observer = Object.new + observer.define_singleton_method(:schema){schema} + observer.define_singleton_method(:buffer){buffer} + + registry.observer = observer + + namespace.metric(:acquired_count).set(5) + + expect(buffer.get_value(:u64, 0)).to be == 5 + end +end diff --git a/test/async/utilization/registry.rb b/test/async/utilization/registry.rb index 97e116c..9f05152 100644 --- a/test/async/utilization/registry.rb +++ b/test/async/utilization/registry.rb @@ -84,6 +84,14 @@ expect(registry.values).to have_keys(module_test: be == 2) end + it "can create a namespace" do + namespace = registry.namespace(:socket_accept) + + expect(namespace).to be_a(Async::Utilization::Namespace) + expect(namespace.registry).to be == registry + expect(namespace.name).to be == :socket_accept + end + it "can use metric for decrement" do registry.metric(:module_decrement_test).increment registry.metric(:module_decrement_test).increment