# Themis
Prometheus client in pure Gleam!
Please remember that Themis is still in early development.
Only Erlang target supported currently.
[![Package Version](https://img.shields.io/hexpm/v/themis)](https://hex.pm/packages/themis)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/themis/)
```sh
gleam add themis
```
## Quick Start
```gleam
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:
```gleam
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.
```gleam
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.
```gleam
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.
```gleam
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).<br>
If you're feeling adventurous, feel free to open a PR.
## Known issues
- Javascript target is not supported. While it is not *impossible* to implement, I am not a Javascript developer. To make Themis compatible with the Javascript target, a replacement for the Erlang ETS tables (which store all the metrics) must be used. If you're interested in implementing a Javascript-compatible metrics store for Themis, please open an issue.
## License
MIT