Class: AceMQ::AMQP::Telemetry::Registry
- Inherits:
-
Object
- Object
- AceMQ::AMQP::Telemetry::Registry
- Defined in:
- lib/acemq/amqp/telemetry.rb
Overview
Keeps the numbers in memory.
Enough to serve from a health endpoint, assert on in a test, or print on a signal. It is not a substitute for a real metrics system: there are no percentiles and nothing is exported anywhere on its own.
metrics = Telemetry::Registry.new
mq = Connection.open(url, telemetry: metrics)
...
puts metrics.to_prometheus
Defined Under Namespace
Classes: Timing
Class Method Summary collapse
-
.key(metric, labels) ⇒ Object
private
A metric and its labels, flattened into one key.
-
.legal(name) ⇒ Object
private
A metric or label name Prometheus will accept.
Instance Method Summary collapse
-
#[](metric, **labels) ⇒ Object
One counter, for a test or an endpoint that wants a single number: the sum of every series of
metricwhose labels include the ones given. - #count(metric, delta = 1, **labels) ⇒ Object
-
#counts ⇒ Object
Every counter, keyed by metric and labels.
- #gauge(metric, value, **labels) ⇒ Object
-
#gauges ⇒ Object
Every gauge.
-
#initialize ⇒ Registry
constructor
A new instance of Registry.
- #observe(metric, value, **labels) ⇒ Object
-
#timings ⇒ Object
Every timing, as Timing.
-
#to_prometheus ⇒ Object
The Prometheus text exposition format, as a string.
Constructor Details
#initialize ⇒ Registry
Returns a new instance of Registry.
349 350 351 352 353 354 |
# File 'lib/acemq/amqp/telemetry.rb', line 349 def initialize @lock = Mutex.new @counts = Hash.new(0) @gauges = {} @timings = {} end |
Class Method Details
.key(metric, labels) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
A metric and its labels, flattened into one key.
Sorted, because the same labels given in a different order have to produce the same key — otherwise one counter quietly becomes several and the total is wrong in a way nobody notices.
423 424 425 426 427 428 |
# File 'lib/acemq/amqp/telemetry.rb', line 423 def self.key(metric, labels) return metric.to_s if labels.nil? || labels.empty? rendered = labels.map { |name, value| "#{name}=#{value}" }.sort.join(",") "#{metric}{#{rendered}}" end |
.legal(name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
A metric or label name Prometheus will accept.
A colon is legal in a metric name and reserved for recording rules, and is not legal in a label name at all, so it goes the way of the dots: nothing AceMQ writes contains one, and the one rule that is right for both beats two that differ where it never matters.
438 439 440 |
# File 'lib/acemq/amqp/telemetry.rb', line 438 def self.legal(name) name.to_s.gsub(/[^a-zA-Z0-9_]/, "_") end |
Instance Method Details
#[](metric, **labels) ⇒ Object
One counter, for a test or an endpoint that wants a single number:
the sum of every series of metric whose labels include the ones
given. Naming fewer labels than were recorded adds the rest up, so a
lookup written before a tag was added keeps reading the same total.
388 389 390 391 392 393 394 395 396 397 |
# File 'lib/acemq/amqp/telemetry.rb', line 388 def [](metric, **labels) wanted = labels.map { |name, value| "#{name}=#{value}" } @lock.synchronize do @counts.sum do |key, value| name, rest = key.split("{", 2) have = rest.to_s.delete_suffix("}").split(",") name == metric.to_s && (wanted - have).empty? ? value : 0 end end end |
#count(metric, delta = 1, **labels) ⇒ Object
356 357 358 359 |
# File 'lib/acemq/amqp/telemetry.rb', line 356 def count(metric, delta = 1, **labels) key = self.class.key(metric, labels) @lock.synchronize { @counts[key] += delta } end |
#counts ⇒ Object
Every counter, keyed by metric and labels.
376 |
# File 'lib/acemq/amqp/telemetry.rb', line 376 def counts = @lock.synchronize { @counts.dup } |
#gauge(metric, value, **labels) ⇒ Object
370 371 372 373 |
# File 'lib/acemq/amqp/telemetry.rb', line 370 def gauge(metric, value, **labels) key = self.class.key(metric, labels) @lock.synchronize { @gauges[key] = value } end |
#gauges ⇒ Object
Every gauge.
379 |
# File 'lib/acemq/amqp/telemetry.rb', line 379 def gauges = @lock.synchronize { @gauges.dup } |
#observe(metric, value, **labels) ⇒ Object
361 362 363 364 365 366 367 368 |
# File 'lib/acemq/amqp/telemetry.rb', line 361 def observe(metric, value, **labels) key = self.class.key(metric, labels) @lock.synchronize do timing = @timings[key] @timings[key] = timing.nil? ? Timing.new(1, value, value, value) : timing.plus(value) end end |
#timings ⇒ Object
Every timing, as Timing.
382 |
# File 'lib/acemq/amqp/telemetry.rb', line 382 def timings = @lock.synchronize { @timings.dup } |
#to_prometheus ⇒ Object
The Prometheus text exposition format, as a string.
A string rather than a Rack application: this library has no web framework and should not choose one. Serve it from whatever already answers HTTP in the process, on a port the ingress does not publish — what a service publishes and how long its handlers take is more than an anonymous caller should be able to learn.
The path every AceMQ library serves this at is /acemq-metrics, and
health at /acemq-health, so a scrape configuration written for one
works against another.
410 411 412 413 414 |
# File 'lib/acemq/amqp/telemetry.rb', line 410 def to_prometheus (counts.sort.map { |key, value| render(key, value, "counter") } + gauges.sort.map { |key, value| render(key, value, "gauge") } + timings.sort.flat_map { |key, timing| render_timing(key, timing) }).join end |