Class: Frankenstein::CollectedMetric

Inherits:
Prometheus::Client::Metric
  • Object
show all
Defined in:
lib/frankenstein/collected_metric.rb

Overview

Populate metric data at scrape time

The usual implementation of a Prometheus registry is to create and register a suite of metrics at program initialization, and then instrument the running code by setting/incrementing/decrementing the metrics and their label sets as the program runs.

Sometimes, however, your program itself doesn't actually interact with the values that you want to return in your metrics, such as the counts of some external resource. You can hack around this by running something periodically in a thread to poll the external resource and update the value, but that's icky.

Instead, this class provides you with a way to say, "whenever we're scraped, run this block of code to generate the label sets and current values, and return that as part of the scrape data". This allows you to do away with ugly polling threads, and instead just write a simple "gather some data and return some numbers" block.

The block to run is passed to the Frankenstein::CollectedMetric constructor, and must return a hash, containing the labelsets and associated numeric values you want to return for the scrape. If your block doesn't send back a hash, or raises an exception during execution, no values will be returned for the metric, an error will be logged (if a logger was specified), and the value of the <metric>_collection_errors_total counter, labelled by the exception class, will be incremented.

Performance & Concurrency

Bear in mind that the code that you specify for the collection action will be run on every scrape; if you've got two Prometheus servers, with a scrape interval of 30 seconds, you'll be running this code once every 15 seconds, forever. Also, Prometheus scrapes have a default timeout of five seconds. So, whatever your collection code does, make it snappy and low-overhead.

On a related note, remember that scrapes can arrive in parallel, so your collection code could potentially be running in parallel, too (depending on your metrics server). Thus, it must be thread-safe -- preferably, it should avoid mutating shared state at all.

Examples:

Returning a database query


Frankenstein::CollectedMetric.new(:my_db_query, "The results of a DB query") do
  ActiveRecord::Base.connection.execute("SELECT name,class,value FROM some_table").each_with_object do |row, h|
    h[name: row['name'], class: row['class']] = row['value']
  end
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, docstring:, labels: [], type: :gauge, logger: Logger.new('/dev/null'), registry: Prometheus::Client.registry, &collector) ⇒ CollectedMetric

Returns a new instance of CollectedMetric.

Parameters:

  • name (Symbol)

    the name of the metric to collect for. This must follow all the normal rules for a Prometheus metric name, and should meet the guidelines for metric naming, unless you like being shunned at parties.

  • docstring (#to_s)

    the descriptive help text for the metric.

  • labels (Array<Symbol>) (defaults to: [])

    the labels which all time series for this metric must possess.

  • type (Symbol) (defaults to: :gauge)

    what type of metric you're returning. It's uncommon to want anything other than :gauge here (the default), because when you're collecting external data it's unlikely you'll be able to trust that your external data source will behave like a proper counter (or histogram or summary), but if you want the flexibility, it's there for you. If you do decide to try your hand at collecting a histogram or summary, bear in mind that the value that you need to return is not a number, or even a hash -- it's a Prometheus-internal class instance, and dealing with the intricacies of that is entirely up to you.

  • logger (Logger) (defaults to: Logger.new('/dev/null'))

    if you want to know what's going on inside your metric, you can pass a logger and see what's going on. Otherwise, you'll be blind if anything goes badly wrong. Up to you.

  • registry (Prometheus::Client::Registry) (defaults to: Prometheus::Client.registry)

    the registry in which this metric will reside. The <metric>_collection_errors_total metric will also be registered here, so you'll know if a collection fails.

  • collector (Proc)

    the code to run on every scrape request.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/frankenstein/collected_metric.rb', line 93

def initialize(name, docstring:, labels: [], type: :gauge, logger: Logger.new('/dev/null'), registry: Prometheus::Client.registry, &collector)
  @validator = Prometheus::Client::LabelSetValidator.new(expected_labels: labels)

  validate_name(name)
  validate_docstring(docstring)

  @name = name
  @docstring = docstring
  @base_labels = {}

  validate_type(type)

  @type      = type
  @logger    = logger
  @registry  = registry
  @collector = collector

  @errors_metric = @registry.counter(:"#{@name}_collection_errors_total", docstring: "Errors encountered while collecting for #{@name}")
  @registry.register(self)
end

Instance Attribute Details

#typeObject (readonly)

The type of the metric being collected.



59
60
61
# File 'lib/frankenstein/collected_metric.rb', line 59

def type
  @type
end

Instance Method Details

#get(labels = {}) ⇒ Object

Retrieve the value for the given labelset.



116
117
118
119
120
# File 'lib/frankenstein/collected_metric.rb', line 116

def get(labels = {})
  @validator.validate_labelset!(labels)

  values[labels]
end

#valuesObject

Retrieve a complete set of labels and values for the metric.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/frankenstein/collected_metric.rb', line 124

def values
  begin
    @collector.call(self).tap do |results|
      unless results.is_a?(Hash)
        @logger.error(progname) { "Collector proc did not return a hash, got #{results.inspect}" }
        @errors_metric.increment(class: "NotAHashError")
        return {}
      end
      results.keys.each { |labelset| @validator.validate_labelset!(labelset) }
    end
  rescue StandardError => ex
    @logger.error(progname) { (["Exception in collection: #{ex.message} (#{ex.class})"] + ex.backtrace).join("\n  ") }
    @errors_metric.increment(class: ex.class.to_s)

    {}
  end
end