[!IMPORTANT] viva_telemetry IS NOT A FRAMEWORK. It is three independent observability surfaces β structured logging, Prometheus-shaped metrics, and statistical benchmarks β that you can pull into any Gleam/BEAM app without dragging in a runtime.
Plays well with Erlang
:logger, Prometheus scrapers, and Grafana.
π― Overview
Observability for Gleam applications on the BEAM. Structured logging, in-memory metrics, Prometheus export, BEAM memory visibility, and small statistical benchmarks β without forcing a large framework into your application.
The three surfaces (log, metrics, bench) are deliberately independent:
import only what you need, configure each per process.
| Property | Value |
|---|---|
| Language | Pure Gleam (type-safe functional) |
| Runtime | BEAM / OTP 27+ |
| Logging backend |
Erlang :logger, console, JSON file |
| Metrics backend | ETS-backed counters, gauges, histograms |
| Export format |
Prometheus text (# HELP / # TYPE) |
| Tests | 41 passing |
| Public API | viva_telemetry/{log,metrics,bench} |
β‘ Quick Start
gleam add viva_telemetry@1Log, count, time β in one file
import viva_telemetry/bench
import viva_telemetry/log
import viva_telemetry/metrics
pub fn main() {
log.configure_erlang(log.info_level)
log.info("Server started", [#("port", "8080")])
let requests = metrics.counter("http_requests_total")
metrics.inc(requests)
bench.run("my_function", fn() { heavy_work() })
|> bench.print()
}π Prerequisites
| Tool | Version | Required for | | :--------- | :--------- | :--------------- | | Gleam | `>= 1.11` | Build / runtime | | Erlang/OTP | `>= 27` | BEAM runtime | Zero NIFs. Zero C dependencies. Pure BEAM.ποΈ Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Gleam application code β
β viva_telemetry/log Β· /metrics Β· /bench β
ββββββββββ¬ββββββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββ
β β β
ββββββββββΌββββββ ββββββββββΌβββββββββ ββββββββΌββββββββββ
β Logging β β Metrics β β Benchmarks β
β β β β β β
β levels β β counter β β warmup β
β handlers β β gauge β β samples β
β context β β histogram β β percentiles β
β sampling β β Prometheus β β ips / speedup β
β named logger β β BEAM memory β β JSON / MD β
ββββββββ¬ββββββββ ββββββββββ¬βββββββββ ββββββββββββββββββ
β β
ββββββββΌββββββ βββββββββΌβββββββ
β Erlang β β ETS-backed β
β :logger β β atomic ops β
ββββββββββββββ ββββββββββββββββπ Core Modules
| Module | Description | | :-------------------------------- | :--------------------------------------------------------- | | `viva_telemetry/log` | Structured logs, named loggers, context, sampling, handlers | | `viva_telemetry/log/level` | RFC 5424 levels (trace β emergency) | | `viva_telemetry/log/entry` | Internal log record type | | `viva_telemetry/log/handler` | Console / JSON file / custom handler dispatch | | `viva_telemetry/metrics` | Counter, gauge, histogram, BEAM memory, Prometheus export | | `viva_telemetry/bench` | Warmup, samples, percentiles, IPS, JSON/Markdown export | | `viva_telemetry_ffi.erl` | Erlang FFI: process dict, time, JSON glue | | `viva_telemetry_metrics_ffi.erl` | ETS metric storage + atomic counters |𧬠Design Principles
| Principle | Description |
|---|---|
| Three independent surfaces | log, metrics, bench are import-only-what-you-need |
| BEAM-native |
ETS for metrics, :logger for logs, no external runtime needed |
| Process-local config |
Handler config & with_context data live per process |
| Prometheus-shaped metrics | Output drops straight into any scraper, no adapters |
| No surprises | Negative counter increments ignored; gauge updates serialized |
π Modules Walkthrough
Logging
import viva_telemetry/log
// Recommended on the BEAM
log.configure_erlang(log.info_level)
log.info("User logged in", [
#("user_id", "42"),
#("ip", "192.168.1.1"),
])Handler options
```gleam log.configure_erlang(log.info_level) log.configure_erlang_with_name(log.info_level, "my_app") log.configure_console(log.debug_level) log.configure_json("app.jsonl", log.info_level) log.configure_full(log.debug_level, "app.jsonl", log.info_level) ```Named loggers, context, sampling, lazy logs
```gleam import gleam/int import gleam/option.{Some} let logger = log.logger("app.http") |> log.with_field("request_id", "abc123") |> log.with_int("attempt", 1) |> log.with_option("user_id", Some(42), int.to_string) logger |> log.logger_info_with("Request completed", [#("status", "200")]) log.with_context([#("request_id", "abc123")], fn() { log.debug("Processing request", []) }) log.debug_lazy(fn() { "expensive: " <> expensive_to_string(data) }, []) log.sampled(log.trace_level, 0.01, "Hot path", []) ```Metrics
import viva_telemetry/metrics
let requests = metrics.counter("http_requests_total")
metrics.inc(requests)
metrics.inc_by(requests, 5)
let connections = metrics.gauge("active_connections")
metrics.set(connections, 42.0)
let latency =
metrics.histogram_with_labels_and_description(
"request_duration_seconds",
[0.1, 0.5, 1.0],
[#("route", "/users")],
"Request duration in seconds.",
)
let result = metrics.time_ms(latency, fn() { do_work() })
io.println(metrics.to_prometheus())Prometheus output
```text # HELP request_duration_seconds Request duration in seconds. # TYPE request_duration_seconds histogram request_duration_seconds_bucket{le="0.5",route="/users"} 1 request_duration_seconds_bucket{le="+Inf",route="/users"} 1 request_duration_seconds_sum{route="/users"} 0.25 request_duration_seconds_count{route="/users"} 1 # TYPE beam_memory_total_bytes gauge beam_memory_total_bytes 12345678 ```Benchmarks
import viva_telemetry/bench
bench.run("fib_recursive", fn() { fib(30) })
|> bench.print()
let slow = bench.run("v1", fn() { algo_v1() })
let fast = bench.run("v2", fn() { algo_v2() })
bench.compare(slow, fast)
|> bench.print_comparison()
bench.to_json(result)
bench.to_markdown_table([slow, fast])Warmup, percentiles (p50/p95/p99), 95% confidence intervals, IPS, and optional JSON/Markdown export β all in memory, no external profiler.
πΊοΈ Roadmap
| Phase | Status |
|---|---|
| Structured logging (console / JSON / Erlang) | β |
| Named loggers + context propagation | β |
| Lazy logs + sampling | β |
| ETS-backed counters / gauges / histograms | β |
Prometheus text export with HELP / TYPE | β |
| BEAM memory metrics | β |
| Statistical benchmarks (warmup + percentiles) | β |
| Bench comparison + JSON / Markdown export | β |
| OpenTelemetry OTLP exporter | β³ |
| Distributed tracing primitives | β³ |
| Exemplars on histograms | β³ |
Built-in /metrics HTTP handler | β³ |
π€ Contributing
git checkout -b feature/your-feature
gleam test # 41 tests
gleam format --check src test
gleam docs buildSee CONTRIBUTING.md for guidelines.
π Documentation
- Hex.pm package
- HexDocs reference
- CHANGELOG β release history.
- CONTRIBUTING β guidelines.
- SECURITY β vulnerability reporting.
Local development
make test # run tests
make bench # run benchmark example
make log # run logging example
make metrics # run metrics example
make docs # generate HexDocs locallyπ VIVA Ecosystem
| Package | Purpose |
|---|---|
viva_math | Mathematical foundations |
viva_emotion | PAD emotional dynamics |
viva_tensor | FP8 LLM inference on the BEAM |
viva_aion | Time perception |
viva_glyph | Symbolic language |
viva_telemetry | Observability (this package) |
π‘ Inspiration
- Logging: Erlang
:logger, glimt, glog, structlog, zap, tracing - Metrics: Prometheus + BEAM telemetry conventions
- Benchmarking: criterion, benchee, hyperfine