Promenade

CI Gem Version

Promenade is a library to simplify instrumenting Ruby applications with Prometheus.

It is currently under development.

Usage

Add promenade to your Gemfle:

gem "promenade"

Built in instrumentation

Promenade includes some built in instrumentation that can be used by requiring it (for example in an initializer).

Currently there is support for ruby-kafka, but I plan to support other things soon.

# Instrument the ruby-kafka libary
require "promenade/kafka"

Instrumentation DSL

Promenade makes recording Prometheus metrics from your own code a little simpler with a DSL of sorts.

Promenade includes some methods for defining your own metrics, and a metric method you can use to record your metrics.

Counter

A counter is a metric that exposes a sum or tally of things.

class WidgetService
  Promenade.counter :widgets_created do
    doc "Records how many widgets are created"
  end

  def create
    # Widget creation code :)
    Promenade.metric(:widgets_created).increment

    # You can also add extra labels as you set increment counters
    Promenade.metric(:widgets_created).increment({ type: "guinness" })
  end

  def batch_create
    You can increment by more than 1 at a time if you need
    Promenade.metric(:widgets_created).increment({ type: "guinness" }, 100)
  end
end

Gauge

A gauge is a metric that exposes an instantaneous value or some snapshot of a changing value.

class Thermometer
  Promenade.gauge :room_temperature_celsius do
    doc "Records room temprature"
  end

  def take_mesurements
    Promenade.metric(:room_temperature_celsius).set({ room: "lounge" }, 22.3)
    Promenade.metric(:room_temperature_celsius).set({ room: "kitchen" }, 25.45)
    Promenade.metric(:room_temperature_celsius).set({ room: "broom_cupboard" }, 15.37)
  end
end

Histogram

A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets. It also provides a sum of all observed values.

class Calculator
  Promenade.histogram :calculator_time_taken do
    doc "Records how long it takes to do the adding"
    # promenade also has some bucket presets like :network and :memory for common usecases
    buckets [0.25, 0.5, 1, 2, 4]
  end

  def add_up
    timing = Benchmark.realtime do
      # Some time consuming addition
    end

    Promenade.metric(:calculator_time_taken).observe({ operation: "addition"}, timing)
  end
end

Summary

Summary is similar to a histogram, but for when you just care about percentile values. Often useful for timings.

class ApiClient
  Promenade.summary :api_client_http_timing do
    doc "record how long requests to the api are taking"
  end

  def get_users
    timing = Benchmark.realtime do
      # Makes a network call
    end

    Promenade.metric(:api_client_http_timing).observe({ method: "get", path: "/api/v1/users" }, timing)
  end
end

Exporter

Because promenade is based on prometheus-client you can add the Prometheus::Client::Rack::Exporter middleware to your rack middleware stack to expose metrics.

There is also a stand alone exporter that can be run with the promenade command.

This is ideal if you are worried about accidentally exposing your metrics, are concerned about the performance impact prometheus scrapes might have on your application, or for applications without a web server (like background processing jobs). It does mean that you have another process to manage on your server though 🤷.

The exporter runs by default on port 9394 and the metrics are available at the standard path of /metrics, the stand-alone exporter is configured to use gzip.

Rails Middleware

Promenade provides custom Rack middleware to track HTTP response times for requests in your Rails application.

This was originally inspired by prometheus-client-mmap.

This middleware is automatically added to your Rack stack if your application is a Ruby on Rails app.

We recommend you add the middleware after ActionDispatch::ShowExceptions in your stack, so you can accurately record the controller and action where an exception was raised.

If you want to change the position, or customise the labels and exception handling behaviour, simply remove the middleware from the stack and re-insert it with your own preferences.

Rails.application.middleware.delete(Promenade::Client::Rack::Collector)
Rails.application.middleware.insert_after(Rails::Rack::Logger, Promenade::Client::Rack::Collector)

Customising the labels recorded for each request

If you would like to collect different labels with each request, you may do so by customising the middleware installation:

label_builder = Proc.new do |env|
  {
    method: env["REQUEST_METHOD"].to_s.downcase,
    host: env["HTTP_HOST"].to_s,
    controller: env.dig("action_dispatch.request.parameters", "controller") || "unknown",
    action: env.dig("action_dispatch.request.parameters", "action") || "unknown"
  }
end
Rails.application.config.middleware.insert_after ActionDispatch::ShowExceptions,
        Promenade::Client::Rack::Collector
        label_builder: label_builder

Customising how the middleware handles exceptions

The default implementation will capture exceptions, count the execption class name (e.g. "StandardError"), and then re-raise the exception.

If you would like to customise this behaviour, you may do so by customising the middleware installation:

exception_handler = Proc.new do |exception, exception_counter, env_hash, request_duration_seconds|
  # This simple example just re-raises the execption
  raise exception
end
Rails.application.config.middleware.insert_after ActionDispatch::ShowExceptions,
        Promenade::Client::Rack::Collector
        exception_handler: exception_handler

Customising the histogram buckets

The default buckets cover a range of latencies from 5 ms to 10s see Promenade::Configuration::DEFAULT_RACK_LATENCY_BUCKETS. This is intended to capture the typical range of latencies for a web application. However, this might not be suitable for your Service-Level Agreements (SLAs), and other bucket size intervals may be required (see histogram bins).

If you would like to customise the histogram buckets, you can do so by configuring Promenade in an initializer:

# config/initializers/promenade.rb

Promenade.configure do |config|
  config.rack_latency_buckets = [0.25, 0.350, 0.5, 1, 1.5, 2.5, 5, 10, 15, 19]
end

Configuration

If you are using rails it should load a railtie and configure promenade.

If are not using rails you should call Promenade.setup after your environment has loaded.

In a typical development environment there should be nothing for you to do. Promenade stores its state files in tmp/promenade and will create that directory if it does not exist.

In a production environment you should try to store the state files on tmpfs for performance, you can configure the path that promenade will write to by setting the PROMETHEUS_MULTIPROC_DIR environment variable.

If you are running the stand-alone exporter, you may also set the PORT environment variable to bind to a port other than the default (9394).

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/errm/promenade.

This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

Acknowledgements

The original code for the Rack middleware collector class was copied from Prometheus Client MMap.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Promenade project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.