TimelessMetrics User’s Guide
Getting Started
TimelessMetrics is an embedded time-series database for Elixir applications. Add the dependency, start the supervisor, and optionally enable the HTTP API.
Add the Dependency
# mix.exs
defp deps do
[
{:timeless_metrics, "~> 1.2"}
]
end
Start the Supervisor
Add TimelessMetrics to your application supervision tree:
# lib/my_app/application.ex
children = [
{TimelessMetrics, name: :metrics, data_dir: "/tmp/metrics_data"}
]
Supervisor.start_link(children, strategy: :one_for_one)
Configuration Options
All options are passed to the supervisor child spec:
| Option | Default | Description |
|---|---|---|
:name |
required | Atom identifier for this store instance |
:data_dir |
required | Directory for persisted data |
:compression |
:zstd |
Compression algorithm for data blocks |
:max_blocks |
100 |
Maximum data blocks in memory per series |
:block_size |
1000 |
Points per data block |
:flush_interval |
60_000 |
Flush to disk interval (ms) |
:raw_retention_seconds |
604_800 |
Raw data retention (default 7 days) |
:daily_retention_seconds |
31_536_000 |
Daily rollup retention (default 1 year) |
:rollup_interval |
300_000 |
Daily rollup computation interval (ms) |
:retention_interval |
3_600_000 |
Retention cleanup interval (ms) |
:alert_interval |
60_000 |
Alert evaluation interval (ms) |
:self_monitor |
true |
Emit internal performance metrics |
:scraping |
true |
Enable Prometheus scrape target subsystem |
Full Configuration Example
# lib/my_app/application.ex
children = [
{TimelessMetrics,
name: :metrics,
data_dir: "/var/data/metrics",
compression: :zstd,
max_blocks: 200,
block_size: 2000,
flush_interval: 30_000,
raw_retention_seconds: 30 * 86_400,
daily_retention_seconds: 365 * 86_400,
rollup_interval: :timer.minutes(5),
retention_interval: :timer.hours(1),
alert_interval: :timer.seconds(30),
self_monitor: true,
scraping: true}
]
Start the HTTP Server
The HTTP server is a separate Plug-based application. Add it to your supervision tree alongside the store:
# lib/my_app/application.ex
children = [
{TimelessMetrics, name: :metrics, data_dir: "/tmp/metrics_data"},
{Bandit, plug: {TimelessMetrics.HTTP, store: :metrics}, port: 8428}
]
The HTTP API is now available at http://localhost:8428.
Writing Metrics (Elixir)
Single Point
Write a single metric data point with write/4. The current timestamp is used by default.
TimelessMetrics.write(:metrics, "cpu_usage", %{"host" => "web-1"}, 73.2)
To specify an explicit timestamp (Unix seconds):
TimelessMetrics.write(:metrics, "cpu_usage", %{"host" => "web-1"}, 68.5,
timestamp: 1_700_000_000
)
Batch Write
Write many points at once with write_batch/2. Each entry is a 3-tuple
{metric, labels, value} or a 4-tuple {metric, labels, value, timestamp}:
TimelessMetrics.write_batch(:metrics, [
{"cpu_usage", %{"host" => "web-1"}, 73.2},
{"cpu_usage", %{"host" => "web-2"}, 61.8},
{"mem_usage", %{"host" => "web-1"}, 4_200_000_000},
{"mem_usage", %{"host" => "web-2"}, 3_800_000_000, 1_700_000_000}
])
Writing Metrics (HTTP)
Three ingest protocols are supported. All return 204 No Content on success.
Prometheus Text Format
curl -X POST http://localhost:8428/api/v1/import/prometheus \
-H "Content-Type: text/plain" \
--data-binary '
cpu_usage{host="web-1"} 73.2
cpu_usage{host="web-2"} 61.8
mem_usage{host="web-1"} 4200000000
'
VictoriaMetrics JSON Lines
Each line is a JSON object with metric, values, and timestamps arrays:
curl -X POST http://localhost:8428/api/v1/import \
-H "Content-Type: application/json" \
--data-binary '
{"metric":{"__name__":"cpu_usage","host":"web-1"},"values":[73.2,74.1],"timestamps":[1700000000,1700000060]}
{"metric":{"__name__":"cpu_usage","host":"web-2"},"values":[61.8],"timestamps":[1700000000]}
'
InfluxDB Line Protocol
curl -X POST http://localhost:8428/write \
--data-binary '
cpu_usage,host=web-1 value=73.2 1700000000000000000
cpu_usage,host=web-2 value=61.8 1700000000000000000
'
Timestamps in InfluxDB line protocol are nanoseconds.
Querying Data (Elixir)
Raw Points — Single Series
query/4 returns all points for an exact label match within a time range:
{:ok, points} =
TimelessMetrics.query(:metrics, "cpu_usage", %{"host" => "web-1"},
from: System.os_time(:second) - 3600,
to: System.os_time(:second)
)
Raw Points — Multiple Series
query_multi/4 matches all series whose labels are a superset of the filter.
Pass an empty map to match all series for that metric:
{:ok, series_list} =
TimelessMetrics.query_multi(:metrics, "cpu_usage", %{},
from: System.os_time(:second) - 3600
)
Filter by specific labels to narrow results:
{:ok, series_list} =
TimelessMetrics.query_multi(:metrics, "cpu_usage", %{"host" => "web-1"},
from: System.os_time(:second) - 3600
)
Aggregated Query — Single Series
query_aggregate/4 buckets points in time and applies an aggregate function.
Supported aggregates: :avg, :min, :max, :sum, :count, :last, :first.
Supported buckets: :minute, :hour, :day, or {n, :seconds}:
{:ok, data} =
TimelessMetrics.query_aggregate(:metrics, "cpu_usage", %{"host" => "web-1"},
from: System.os_time(:second) - 86_400,
to: System.os_time(:second),
bucket: :hour,
aggregate: :avg
)
Aggregated Query — Multiple Series
query_aggregate_multi/4 aggregates across all matching series:
{:ok, series_list} =
TimelessMetrics.query_aggregate_multi(:metrics, "cpu_usage", %{},
from: System.os_time(:second) - 86_400,
bucket: {300, :seconds},
aggregate: :avg
)
Grouped Aggregation
query_aggregate_grouped/4 aggregates across series and groups results by a label key.
This is useful for dashboards that show one line per group:
{:ok, groups} =
TimelessMetrics.query_aggregate_grouped(:metrics, "cpu_usage", %{},
from: System.os_time(:second) - 86_400,
bucket: :hour,
aggregate: :avg,
group_by: "host"
)
Latest Value
latest/3 returns the most recent data point for a single series:
{:ok, {timestamp, value}} =
TimelessMetrics.latest(:metrics, "cpu_usage", %{"host" => "web-1"})
latest_multi/3 returns the latest value for all matching series:
{:ok, results} = TimelessMetrics.latest_multi(:metrics, "cpu_usage")
Top N Helper
top_n/3 sorts query results and returns the top N entries.
It works on results from query_aggregate_multi or latest_multi:
{:ok, series} =
TimelessMetrics.query_aggregate_multi(:metrics, "cpu_usage", %{},
from: System.os_time(:second) - 3600,
bucket: :hour,
aggregate: :avg
)
top = TimelessMetrics.top_n(series, 5)
A custom ordering function can be provided as the third argument:
# Sort by maximum value instead of last value
{:ok, series} =
TimelessMetrics.query_aggregate_multi(:metrics, "cpu_usage", %{},
from: System.os_time(:second) - 3600,
bucket: :hour,
aggregate: :avg
)
top = TimelessMetrics.top_n(series, 5, fn %{data: data} ->
data |> Enum.map(&elem(&1, 1)) |> Enum.max(fn -> 0 end)
end)
Querying Data (HTTP)
Raw Export
Export raw points in VictoriaMetrics JSON lines format:
curl "http://localhost:8428/api/v1/export?metric=cpu_usage&host=web-1&from=-1h"
Response (one JSON object per line):
{"metric":{"__name__":"cpu_usage","host":"web-1"},"values":[73.2,74.1],"timestamps":[1700000000,1700000060]}
Latest Value
curl "http://localhost:8428/api/v1/query?metric=cpu_usage&host=web-1"
Response:
{"labels":{"host":"web-1"},"timestamp":1700000120,"value":74.1}
Without a label filter, all matching series are returned:
curl "http://localhost:8428/api/v1/query?metric=cpu_usage"
{"data":[
{"labels":{"host":"web-1"},"timestamp":1700000120,"value":74.1},
{"labels":{"host":"web-2"},"timestamp":1700000120,"value":62.3}
]}
Query Range
Query with time bucketing, aggregation, and optional grouping:
curl "http://localhost:8428/api/v1/query_range?\
metric=cpu_usage&\
start=-1h&\
end=now&\
step=60&\
aggregate=avg"
Response:
{
"metric": "cpu_usage",
"series": [
{"labels":{"host":"web-1"},"data":[[1700000000,73.2],[1700000060,74.1]]},
{"labels":{"host":"web-2"},"data":[[1700000000,61.8],[1700000060,62.3]]}
]
}
With group_by:
curl "http://localhost:8428/api/v1/query_range?\
metric=cpu_usage&\
start=-6h&\
step=300&\
aggregate=avg&\
group_by=host"
{
"metric": "cpu_usage",
"groups": [
{"group":{"host":"web-1"},"data":[[1700000000,73.2],[1700000300,71.5]]},
{"group":{"host":"web-2"},"data":[[1700000000,61.8],[1700000300,63.1]]}
]
}
Time parameters accept Unix seconds or relative durations: -1h, -30m, -5d, now.
PromQL (Grafana Compatible)
The Prometheus-compatible endpoint supports PromQL queries for use with Grafana:
curl "http://localhost:8428/prometheus/api/v1/query_range?\
query=cpu_usage\{host=\"web-1\"\}&\
start=1700000000&\
end=1700003600&\
step=60"
To configure Grafana, add a Prometheus data source pointing at
http://localhost:8428/prometheus.
Series Discovery
Elixir API
List all metric names in the store:
{:ok, metrics} = TimelessMetrics.list_metrics(:metrics)
List all series for a given metric:
{:ok, series} = TimelessMetrics.list_series(:metrics, "cpu_usage")
Get distinct values for a label key:
{:ok, values} = TimelessMetrics.label_values(:metrics, "cpu_usage", "host")
HTTP API
List all metric names:
curl "http://localhost:8428/api/v1/label/__name__/values"
{"status":"success","data":["cpu_usage","mem_usage","disk_io"]}
List distinct values for a label:
curl "http://localhost:8428/api/v1/label/host/values?metric=cpu_usage"
{"status":"success","data":["web-1","web-2"]}
List all series for a metric:
curl "http://localhost:8428/api/v1/series?metric=cpu_usage"
{"status":"success","data":[{"host":"web-1"},{"host":"web-2"}]}
Metric Metadata
Register type, unit, and description metadata for a metric. Metadata is also
auto-inferred from Prometheus naming conventions (e.g. _total → counter,
_bytes → gauge with unit “bytes”).
Elixir API
TimelessMetrics.register_metric(:metrics, "cpu_usage", :gauge,
unit: "%",
description: "CPU utilization percentage"
)
{:ok, meta} = TimelessMetrics.get_metadata(:metrics, "cpu_usage")
HTTP API
Register metadata:
curl -X POST http://localhost:8428/api/v1/metadata \
-H "Content-Type: application/json" \
-d '{
"metric": "cpu_usage",
"type": "gauge",
"unit": "%",
"description": "CPU utilization percentage"
}'
Retrieve metadata:
curl "http://localhost:8428/api/v1/metadata?metric=cpu_usage"
{"metric":"cpu_usage","type":"gauge","unit":"%","description":"CPU utilization percentage"}
Annotations
Annotations are event markers (deploys, incidents, config changes) that can be overlaid on charts.
Elixir API
Create an annotation:
{:ok, id} =
TimelessMetrics.annotate(:metrics, System.os_time(:second), "v2.1.0 deployed",
description: "Rolling deploy to production cluster",
tags: ["deploy", "production"]
)
Query annotations in a time range:
{:ok, annotations} =
TimelessMetrics.annotations(
:metrics,
System.os_time(:second) - 86_400,
System.os_time(:second)
)
Filter by tags:
{:ok, deploys} =
TimelessMetrics.annotations(
:metrics,
System.os_time(:second) - 86_400,
System.os_time(:second),
tags: ["deploy"]
)
Delete an annotation:
TimelessMetrics.delete_annotation(:metrics, 1)
HTTP API
Create an annotation:
curl -X POST http://localhost:8428/api/v1/annotations \
-H "Content-Type: application/json" \
-d '{
"title": "v2.1.0 deployed",
"description": "Rolling deploy to production cluster",
"tags": ["deploy", "production"]
}'
{"id":1,"status":"created"}
Query annotations:
curl "http://localhost:8428/api/v1/annotations?from=-24h&to=now&tags=deploy"
Delete an annotation:
curl -X DELETE "http://localhost:8428/api/v1/annotations/1"
Alerts
Alert rules monitor metric values against thresholds. When a condition is met for the configured duration, the alert fires and optionally sends a webhook.
Alert State Machine
An alert rule progresses through these states:
stateDiagram-v2
[*] --> ok
ok --> pending : condition met
pending --> ok : condition cleared
pending --> firing : duration elapsed
firing --> resolved : condition cleared
resolved --> ok : next evaluation
resolved --> pending : condition met again
- ok — condition not met
-
pending — condition met, waiting for
:durationseconds - firing — condition persisted, webhook sent
- resolved — condition cleared after firing, webhook sent
Elixir API
Create an alert rule:
{:ok, rule_id} =
TimelessMetrics.create_alert(:metrics,
name: "High CPU",
metric: "cpu_usage",
condition: :above,
threshold: 90.0,
labels: %{"host" => "web-1"},
duration: 300,
aggregate: :avg,
webhook_url: "https://hooks.example.com/alerts"
)
List all alert rules with their current state:
{:ok, alerts} = TimelessMetrics.list_alerts(:metrics)
Update an alert rule (partial update — only specified fields change):
{:ok, alerts} = TimelessMetrics.list_alerts(:metrics)
[first | _] = alerts
TimelessMetrics.update_alert(:metrics, first.id,
threshold: 95.0,
duration: 600
)
Delete an alert rule:
{:ok, alerts} = TimelessMetrics.list_alerts(:metrics)
[first | _] = alerts
TimelessMetrics.delete_alert(:metrics, first.id)
View and acknowledge alert history:
{:ok, history} = TimelessMetrics.alert_history(:metrics, limit: 10)
Manually trigger evaluation (normally runs on :alert_interval):
TimelessMetrics.evaluate_alerts(:metrics)
Webhook Payload
When an alert fires or resolves, a JSON POST is sent to the configured webhook_url:
{
"rule_id": 1,
"name": "High CPU",
"metric": "cpu_usage",
"labels": {"host": "web-1"},
"condition": "above",
"threshold": 90.0,
"value": 93.5,
"state": "firing",
"timestamp": 1700000300
}
HTTP API
Create an alert:
curl -X POST http://localhost:8428/api/v1/alerts \
-H "Content-Type: application/json" \
-d '{
"name": "High CPU",
"metric": "cpu_usage",
"condition": "above",
"threshold": 90.0,
"labels": {"host": "web-1"},
"duration": 300,
"aggregate": "avg",
"webhook_url": "https://hooks.example.com/alerts"
}'
{"id":1,"status":"created"}
List alerts:
curl "http://localhost:8428/api/v1/alerts"
Delete an alert:
curl -X DELETE "http://localhost:8428/api/v1/alerts/1"
Scrape Targets
TimelessMetrics can scrape Prometheus-compatible /metrics endpoints on a
configurable interval, similar to Prometheus itself. Scraping is enabled by
default (:scraping option).
Scrape targets are managed through the TimelessMetrics.Scraper GenServer or
the HTTP API. The scraper process is named :"#{store_name}_scraper".
Elixir API
scraper = :metrics_scraper
# Add a scrape target
{:ok, id} =
TimelessMetrics.Scraper.add_target(scraper, %{
"job_name" => "my_app",
"address" => "localhost:4000",
"scheme" => "http",
"metrics_path" => "/metrics",
"scrape_interval" => 30,
"scrape_timeout" => 10,
"labels" => %{"env" => "production"},
"honor_labels" => false,
"honor_timestamps" => true
})
# List all targets
{:ok, targets} = TimelessMetrics.Scraper.list_targets(:metrics_scraper)
# Delete a target (use an ID from list_targets above)
TimelessMetrics.Scraper.delete_target(:metrics_scraper, 1)
Scrape Target Options
| Field | Default | Description |
|---|---|---|
job_name |
required | Identifier for the scrape job |
address |
required | Host and port to scrape |
scheme |
"http" |
"http" or "https" |
metrics_path |
"/metrics" |
Path to the metrics endpoint |
scrape_interval |
30 |
Seconds between scrapes |
scrape_timeout |
10 |
Timeout per scrape request (seconds) |
labels |
%{} |
Extra labels added to all scraped metrics |
honor_labels |
false |
If true, scraped labels take precedence |
honor_timestamps |
true |
If true, use timestamps from scraped data |
relabel_configs |
nil |
Pre-scrape relabeling rules |
metric_relabel_configs |
nil |
Post-scrape metric relabeling rules |
enabled |
true |
Enable or disable this target |
Relabel Configs
Relabel configs follow the Prometheus relabeling model. Each config is a map
with a "regex" key:
{:ok, id} = TimelessMetrics.Scraper.add_target(scraper, %{
"job_name" => "my_app",
"address" => "localhost:4000",
"metric_relabel_configs" => [
%{"regex" => "^go_.*"}
]
})
HTTP API
Create a scrape target:
curl -X POST http://localhost:8428/api/v1/scrape_targets \
-H "Content-Type: application/json" \
-d '{
"job_name": "my_app",
"address": "localhost:4000",
"scrape_interval": 30,
"labels": {"env": "production"}
}'
{"id":1,"status":"created"}
List all targets with health status:
curl "http://localhost:8428/api/v1/scrape_targets"
Update a target:
curl -X PUT "http://localhost:8428/api/v1/scrape_targets/1" \
-H "Content-Type: application/json" \
-d '{"scrape_interval": 15}'
Delete a target:
curl -X DELETE "http://localhost:8428/api/v1/scrape_targets/1"
Operations
Store Info
TimelessMetrics.info(:metrics)
Flush to Disk
Force all buffered data to be written to disk immediately:
TimelessMetrics.flush(:metrics)
Backup
Create a consistent online backup of all data. The backup is a server-side snapshot — files are written to the target directory and the response tells you what was created:
{:ok, result} = TimelessMetrics.backup(:metrics, "/tmp/metrics_backup")
# result => %{path: "/tmp/metrics_backup", files: ["timeless.db", ...], total_bytes: 12345678}
Via HTTP (if no path is provided, a timestamped subdirectory under
/backups/ is created automatically):
curl -X POST http://localhost:8428/api/v1/backup \
-H "Content-Type: application/json" \
-d '{"path": "/tmp/metrics_backup"}'
{"status":"ok","path":"/tmp/metrics_backup","files":["timeless.db"],"total_bytes":12345678}
To download the backup, archive it from the server filesystem:
tar czf metrics_backup.tar.gz -C /tmp/metrics_backup .
To restore, stop the store, replace the data_dir contents with the backup
files, and restart.
Daily Rollup
Force a daily rollup computation (normally runs on :rollup_interval):
TimelessMetrics.rollup(:metrics)
Retention Enforcement
Force retention cleanup now (normally runs on :retention_interval):
TimelessMetrics.enforce_retention(:metrics)
Health Endpoint
curl "http://localhost:8428/health"
{
"status": "ok",
"series": 142,
"points": 1580000,
"storage_bytes": 12345678,
"buffer_points": 340,
"bytes_per_point": 7.81
}
Self-Monitoring
When :self_monitor is true (the default), TimelessMetrics emits its own
performance metrics. The /metrics endpoint exposes these in Prometheus format,
allowing you to scrape TimelessMetrics with itself or any external Prometheus instance:
curl "http://localhost:8428/metrics"
# HELP timeless_metrics_series_count Number of active series
# TYPE timeless_metrics_series_count gauge
timeless_metrics_series_count 142
# HELP timeless_metrics_total_points Total stored points
# TYPE timeless_metrics_total_points gauge
timeless_metrics_total_points 1580000
...
Embedded SVG Charts
The /chart endpoint renders a metric directly as an SVG image. This is useful
for embedding in dashboards, Slack messages, or documentation:
curl "http://localhost:8428/chart?\
metric=cpu_usage&\
from=-6h&\
step=300&\
aggregate=avg&\
width=800&\
height=300&\
theme=dark"
Query parameters:
| Parameter | Default | Description |
|---|---|---|
metric |
required | Metric name |
from |
-1h |
Start time |
to |
now |
End time |
step |
auto | Bucket size in seconds |
aggregate |
avg |
Aggregation function |
width |
800 |
SVG width in pixels |
height |
300 |
SVG height in pixels |
theme |
auto |
dark, light, or auto |
forecast |
— | Forecast horizon duration |
anomalies |
— |
Anomaly sensitivity: low, medium, high |
Label filters can be added as additional query parameters (e.g. &host=web-1).
Embed in HTML:

API Documentation
An interactive OpenAPI docs UI is available at /api/docs, and the raw
OpenAPI 3.0 spec at /api/openapi.json.
Authentication
All endpoints (except /health) support optional Bearer token authentication.
Pass the token via header or query parameter:
# Via header
curl -H "Authorization: Bearer my-secret-token" \
"http://localhost:8428/api/v1/query?metric=cpu_usage"
# Via query parameter (useful for browser/img tags)
curl "http://localhost:8428/chart?metric=cpu_usage&token=my-secret-token"