Class: AceMQ::AMQP::Telemetry::Registry

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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

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