Themis

Prometheus client in pure Gleam!

Please remember that Themis is still in early development.

Only Erlang target supported currently.

Package Version Hex Docs

gleam add themis

Quick Start

import gleam/dict
import gleam/io
import gleam/set
import themis
import themis/counter
import themis/gauge
import themis/histogram
import themis/number

pub fn main() {
  // initialize the metrics store
  let metrics_store = themis.init()

  // Gauge

  // This can fail if the metric name is invalid
  let assert Ok(_) =
    gauge.new(
      metrics_store,
      "my_first_metric",
      "A gauge Prometheus metric",
    )

  let labels = dict.from_list([#("foo", "bar")])
  let value = number.integer(10)
  let assert Ok(_) =
    gauge.observe(metrics_store, "my_first_metric", labels, value)

  // Counter

  let assert Ok(_) =
    counter.new(
      metrics_store,
      "my_second_metric",
      "A counter Prometheus metric",
    )

  let labels = dict.from_list([#("wibble", "wobble")])
  let other_labels = dict.from_list([#("wii", "woo")])
  let assert Ok(_) =
    counter.new(metrics_store, "my_second_metric", "A counter Prometheus metric")
  let assert Ok(_) =
    counter.increment(metrics_store, "my_second_metric", labels)
  let assert Ok(_) =
    counter.increment_by(
      metrics_store,
      "my_second_metric",
      other_labels,
      number.decimal(1.2),
    )

  // Histogram

  // Histograms work with buckets. Each bucket needs an upper boundary.
  // Read more about histograms here https://prometheus.io/docs/practices/histograms/
  let buckets =
    set.from_list([
      number.decimal(0.05),
      number.decimal(0.1),
      number.decimal(0.25),
      number.decimal(0.5),
      number.integer(1),
    ])
  let assert Ok(_) =
    histogram.new(
      metrics_store,
      "my_third_metric",
      "A histogram Prometheus metric",
      buckets,
    )

  let value = number.integer(20)
  let other_value = number.decimal(1.5)
  let labels = dict.from_list([#("toto", "tata")])
  let other_labels = dict.from_list([#("toto", "titi")])
  let assert Ok(_) =
    histogram.observe(metrics_store, "my_third_metric", labels, value)
  // When incrementing a histogram with new labels, a new histogram will automatically be initialized
  let assert Ok(_) =
    histogram.observe(
      metrics_store,
      "my_third_metric",
      other_labels,
      other_value,
    )

  // Printing all the metrics as a Prometheus-scrapable String
  let assert Ok(prometheus_string) = themis.print(metrics_store)
  io.println(prometheus_string)
}

This code will print the following prometheus-compatible metrics:

# HELP my_first_metric A gauge Prometheus metric
# TYPE my_first_metric gauge
my_first_metric{foo="bar"} 10

# HELP my_second_metric_total A counter Prometheus metric
# TYPE my_second_metric_total counter
my_second_metric_total{wibble="wobble"} 1
my_second_metric_total{wii="woo"} 1.2

# HELP my_third_metric A histogram Prometheus metric
# TYPE my_third_metric histogram
my_third_metric_bucket{le="0.05",toto="tata"} 0
my_third_metric_bucket{le="0.1",toto="tata"} 0
my_third_metric_bucket{le="0.25",toto="tata"} 0
my_third_metric_bucket{le="0.5",toto="tata"} 0
my_third_metric_bucket{le="1",toto="tata"} 0
my_third_metric_bucket{le="+Inf",toto="tata"} 1
my_third_metric_sum{toto="tata"} 1
my_third_metric_count{toto="tata"} 1

my_third_metric_bucket{le="0.05",toto="titi"} 0
my_third_metric_bucket{le="0.1",toto="titi"} 0
my_third_metric_bucket{le="0.25",toto="titi"} 0
my_third_metric_bucket{le="0.5",toto="titi"} 0
my_third_metric_bucket{le="1",toto="titi"} 0
my_third_metric_bucket{le="+Inf",toto="titi"} 1
my_third_metric_sum{toto="titi"} 1
my_third_metric_count{toto="titi"} 1

Further documentation can be found at https://hexdocs.pm/themis.

Usage

Working with Different Numeric Types

Themis metric values are set using the dedicated Number type. There are 5 number types available:

import themis/number

// Integer values
let integer = number.integer(1_234_567)

// Decimal (float) values
let decimal = number.decimal(23.5)

// Special values
let positive_infinity = number.positive_infinity()
let negative_infinity = number.negative_infinity()
let not_a_number = number.not_a_number()

Metric Types

Gauges

Gauges are metrics that represent a single numerical value that can arbitrarily go up and down. They are typically used for measured values like temperatures, current memory usage, or number of active connections.

import themis/gauge

// Create a new gauge metric
let assert Ok(_) = 
  gauge.new(
    metrics_store,
    "process_memory_bytes",
    "Current memory usage in bytes",
  )

// Set a gauge value with labels
let labels = dict.from_list([#("process", "web_server")])
let value = number.integer(52_428_800)  // 50MB in bytes
let assert Ok(_) =
  gauge.observe(metrics_store, "process_memory_bytes", labels, value)

Counters

Counters are cumulative metrics that can only increase or be reset to zero. They are typically used to count requests served, tasks completed, errors occurred, or other countable occurrences.

import themis/counter

// Create a new counter metric
let assert Ok(_) =
  counter.new(
    metrics_store,
    "http_requests_total",
    "Total number of HTTP requests made",
  )

// Currently, counters cannot be initialized.
// To have a counter of value 0, you must increment it by 0:
let labels = dict.from_list([#("method", "GET"), #("path", "/api/users")])
let assert Ok(_) =
  counter.increment_by(metrics_store, "http_requests_total", labels, number.integer(0))

// Increment counter by 1
let assert Ok(_) =
  counter.increment(metrics_store, "http_requests_total", labels)

// Increment counter by specific amount
let assert Ok(_) =
  counter.increment_by(
    metrics_store,
    "http_requests_total",
    labels,
    number.decimal(5.0),
  )

Histograms

Histograms sample observations (usually duration or response size) and count them in configurable buckets. They also provide a sum of all observed values and a count of observations.

import themis/histogram

// Define histogram buckets (upper bounds of observation buckets in seconds)
let buckets =
  set.from_list([
    number.decimal(0.005),  // 5ms
    number.decimal(0.01),   // 10ms
    number.decimal(0.025),  // 25ms
    number.decimal(0.05),   // 50ms
    number.decimal(0.1),    // 100ms
    number.decimal(0.25),   // 250ms
    number.decimal(0.5),    // 500ms
    number.decimal(1.0),    // 1s
  ])

// Create a new histogram metric
let assert Ok(_) =
  histogram.new(
    metrics_store,
    "http_request_duration_seconds",
    "HTTP request duration in seconds",
    buckets,
  )

// Record an observation
let labels = dict.from_list([#("method", "POST"), #("path", "/api/users")])
let duration = number.decimal(0.157)  // 157ms
let assert Ok(_) =
  histogram.observe(
    metrics_store,
    "http_request_duration_seconds",
    labels,
    duration,
  )

Each histogram observation is counted in all buckets with upper bounds greater than the observation value. The +Inf bucket is automatically added and counts all observations. Additionally, histograms track the sum of all observed values and the total count of observations.

Summaries

Summaries have not yet been implemented, because at first glance it seems an accurate summary must keep a complete history of all the observed values, which will be a huge memory hog. This means I would have to implement some algorithm that goes way above my head to instead derive an approximation. I ain’t doin’ that (maybe one day if I need summaries but don’t hold your breath).
If you’re feeling adventurous, feel free to open a PR.

Known issues

License

MIT

Search Document