Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions examples/expected/prometheus_metrics.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# TYPE http_requests_total counter
http_requests_total 0
http_requests_total{route="/api",status="200"} 3
http_requests_total{route="/health",status="200"} 1
# TYPE inflight_requests gauge
inflight_requests 2
# TYPE request_latency_ms histogram
request_latency_ms_bucket{le="1"} 0
request_latency_ms_bucket{le="5"} 0
request_latency_ms_bucket{le="10"} 0
request_latency_ms_bucket{le="25"} 0
request_latency_ms_bucket{le="50"} 0
request_latency_ms_bucket{le="100"} 0
request_latency_ms_bucket{le="250"} 0
request_latency_ms_bucket{le="500"} 0
request_latency_ms_bucket{le="1000"} 0
request_latency_ms_bucket{le="5000"} 0
request_latency_ms_bucket{le="+Inf"} 0
request_latency_ms_count 0
request_latency_ms_sum 0
request_latency_ms_bucket{le="1",route="/api"} 0
request_latency_ms_bucket{le="5",route="/api"} 0
request_latency_ms_bucket{le="10",route="/api"} 0
request_latency_ms_bucket{le="25",route="/api"} 1
request_latency_ms_bucket{le="50",route="/api"} 1
request_latency_ms_bucket{le="100",route="/api"} 2
request_latency_ms_bucket{le="250",route="/api"} 2
request_latency_ms_bucket{le="500",route="/api"} 2
request_latency_ms_bucket{le="1000",route="/api"} 2
request_latency_ms_bucket{le="5000",route="/api"} 2
request_latency_ms_bucket{le="+Inf",route="/api"} 2
request_latency_ms_count{route="/api"} 2
request_latency_ms_sum{route="/api"} 97
request_latency_ms_min{route="/api"} 12
request_latency_ms_max{route="/api"} 85
23 changes: 23 additions & 0 deletions examples/prometheus_metrics.pith
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# expose pith metrics for prometheus.
#
# in a real app you spawn the endpoint and let prometheus scrape it:
# import std.prometheus as prometheus
# spawn prometheus.serve("0.0.0.0", 9464)
# prometheus then GETs http://host:9464/metrics on its scrape interval. here we
# just register a few labeled metrics and print exactly what a scrape returns.
import std.metrics as metrics

fn main() -> Int!:
reqs := metrics.counter("http_requests_total")
reqs.labels(["route", "/api", "status", "200"]).add(3)
reqs.labels(["route", "/health", "status", "200"]).inc()

metrics.gauge("inflight_requests").set(2)

lat := metrics.histogram("request_latency_ms")
lat.labels(["route", "/api"]).observe(12)
lat.labels(["route", "/api"]).observe(85)

# the text prometheus would receive from GET /metrics
print(metrics.snapshot_text())
return 0
53 changes: 53 additions & 0 deletions std/prometheus.pith
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# std.prometheus - expose pith metrics for prometheus to scrape.
#
# spawn serve() alongside your app to answer GET /metrics with the text
# exposition of every registered std.metrics series (labels included):
#
# import std.metrics as metrics
# import std.prometheus as prometheus
#
# fn main():
# spawn prometheus.serve("0.0.0.0", 9464)
# metrics.counter("requests_total").labels(["route", "/api"]).inc()
# ... run your app ...
#
# the endpoint is read-only and pull-based: prometheus scrapes it on an interval,
# so serve() just renders the current snapshot on each request.

import std.metrics as metrics
import std.net.http as http

# prometheus's text-exposition format content type.
PROMETHEUS_CONTENT_TYPE := "text/plain; version=0.0.4; charset=utf-8"

# the /metrics response for the current metric snapshot.
pub fn metrics_response() -> http.HttpResponse:
body := metrics.snapshot_text() + chr(10)
return http.response(200).with_body_text(body).header("Content-Type", PROMETHEUS_CONTENT_TYPE)

# handle one scrape connection: answer GET /metrics, 404 for anything else. a
# request that fails to parse is dropped so it can't wedge the accept loop.
fn handle_scrape(fd: Int) -> Int!:
tcp_set_timeout(fd, 2000)
req := http.read_request_bytes(fd)!
if req.is_get("/metrics")!:
return http.send(fd, metrics_response())
return http.send(fd, http.not_found_response())

# serve the /metrics endpoint on host:port. this blocks in an accept loop, so run
# it in the background: `spawn prometheus.serve("0.0.0.0", 9464)`.
pub fn serve(host: String, port: Int) -> Int!:
fd := tcp_listen(host, port)!
while true:
client := tcp_accept(fd)!
handle_scrape(client) catch 0
return 0

test "metrics_response carries the prometheus content type and current snapshot":
metrics.reset()
metrics.counter("http_requests_total").labels(["route", "/api"]).inc()
resp := metrics_response()
rendered := resp.to_bytes()!.to_string_utf8()!
assert(rendered.contains("Content-Type: text/plain; version=0.0.4"))
assert(rendered.contains("# TYPE http_requests_total counter"))
assert(rendered.contains("http_requests_total" + chr(123) + "route=" + chr(34) + "/api" + chr(34) + chr(125) + " 1"))
Loading