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@1
Log, 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
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
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
# 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 build
See 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