Skip to content

sjqtentacles/sml-metrics

Repository files navigation

sml-metrics

CI

Pure Standard ML implementation of Prometheus-style metrics — counters, gauges and histograms with the text exposition format — and zero dependencies. Builds and tests cleanly under both MLton and Poly/ML, producing byte-identical output.

Overview

Prometheus scrapes metrics from targets in a simple line-oriented text format. This library lets an SML program build that output:

  • Counter — a monotonically increasing integer (inc, incBy, value). A negative incBy raises NonMonotonic.
  • Gauge — a real value that can go up or down (set, inc, dec, incBy, decBy, value).
  • Histogram — observations bucketed by configurable upper bounds (le), with cumulative _bucket counts (per Prometheus convention), an implicit +Inf bucket, plus _sum and _count.
  • Registry — collects metric families and renders the exposition format via Registry.expose.

Each metric is a family with a name, help text and zero or more child series keyed by label name/value pairs, so one family can expose many series (for example one per HTTP status code).

Format and determinism

The renderer follows the Prometheus text format exactly:

# HELP <name> <help text>
# TYPE <name> <counter|gauge|histogram>
<name>{label="v",...} <value>
  • One # HELP and # TYPE line per family, followed by all of its series.
  • Families are emitted in registration order; the label pairs of a series are emitted in the order they were supplied (a series' identity is independent of pair order).
  • Label values escape \, " and newline; help text escapes \ and newline.
  • Histograms emit cumulative _bucket{le="..."} lines (including le="+Inf"), then _sum and _count.

Counter values and histogram counts are integers. All real-valued numbers (gauge values, histogram sums and bucket bounds) are rendered with a forced decimal point and a leading - for negatives, so output is byte-identical across MLton and Poly/ML. As a result an integer bucket bound of 1 renders as le="1.0", and a gauge of 21.5 renders as 21.5.

Why

A small, readable, dependency-free metrics library is handy for instrumenting SML programs that are scraped by Prometheus, for teaching the exposition format, and as a reference for the counter/gauge/histogram model. The output is deterministic and identical under both compilers, which makes it trivial to golden-test.

Install

Using smlpkg:

smlpkg add github.com/sjqtentacles/sml-metrics
smlpkg sync

Then add the library to your MLB file:

$(SML_LIB)/basis/basis.mlb
lib/github.com/sjqtentacles/sml-metrics/sources.mlb

Usage

open Metrics

val requests =
    Counter.new {name = "http_requests_total",
                 help = "The total number of HTTP requests."}
val () = Counter.incBy requests [("method", "post"), ("code", "200")] 1027
val () = Counter.incBy requests [("method", "post"), ("code", "400")] 3

val temperature =
    Gauge.new {name = "temperature_celsius",
               help = "Current temperature in Celsius."}
val () = Gauge.set temperature [] 21.5

val latency =
    Histogram.new {name = "request_latency_seconds",
                   help = "Request latency in seconds.",
                   buckets = [0.1, 0.5, 1.0]}
val () = List.app (Histogram.observe latency [])
           [0.05, 0.3, 0.5, 0.7, 1.0, 2.0]

val reg = Registry.new ()
val () = Registry.counter reg requests
val () = Registry.gauge reg temperature
val () = Registry.histogram reg latency

val () = print (Registry.expose reg)

The signature exposes:

signature METRICS =
sig
  type labels = (string * string) list
  exception NonMonotonic

  structure Counter :
    sig
      type t
      val new   : {name : string, help : string} -> t
      val inc   : t -> labels -> unit
      val incBy : t -> labels -> int -> unit
      val value : t -> labels -> int
    end

  structure Gauge :
    sig
      type t
      val new   : {name : string, help : string} -> t
      val set   : t -> labels -> real -> unit
      val inc   : t -> labels -> unit
      val dec   : t -> labels -> unit
      val incBy : t -> labels -> real -> unit
      val decBy : t -> labels -> real -> unit
      val value : t -> labels -> real
    end

  structure Histogram :
    sig
      type t
      val new          : {name : string, help : string, buckets : real list} -> t
      val observe      : t -> labels -> real -> unit
      val count        : t -> labels -> int
      val sum          : t -> labels -> real
      val bucketCounts : t -> labels -> (real * int) list
    end

  structure Registry :
    sig
      type t
      val new       : unit -> t
      val counter   : t -> Counter.t -> unit
      val gauge     : t -> Gauge.t -> unit
      val histogram : t -> Histogram.t -> unit
      val expose    : t -> string
    end
end

Running make example prints:

# HELP http_requests_total The total number of HTTP requests.
# TYPE http_requests_total counter
http_requests_total{method="post",code="200"} 1027
http_requests_total{method="post",code="400"} 3
# HELP temperature_celsius Current temperature in Celsius.
# TYPE temperature_celsius gauge
temperature_celsius 21.5
# HELP request_latency_seconds Request latency in seconds.
# TYPE request_latency_seconds histogram
request_latency_seconds_bucket{le="0.1"} 1
request_latency_seconds_bucket{le="0.5"} 3
request_latency_seconds_bucket{le="1.0"} 5
request_latency_seconds_bucket{le="+Inf"} 6
request_latency_seconds_sum 4.55
request_latency_seconds_count 6

Testing

The test suite is written strict-TDD (golden exposition vectors first, then the implementation). It covers counter monotonicity and per-label series, gauge inc/dec/set, histogram bucket boundaries (le is inclusive — a value equal to a bound falls into that bucket) and cumulative counts, label-value and help-text escaping, and full Registry.expose golden output.

make test        # MLton
make test-poly   # Poly/ML

Both compilers report 30 passed, 0 failed, with byte-identical output.

License

MIT.

About

Prometheus-style counters, gauges, histograms + text exposition in pure Standard ML (MLton + Poly/ML)

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors