Files
2026-05-22 07:42:54 +01:00

195 lines
5.3 KiB
Elixir

defmodule MusicLibraryWeb.Telemetry.StorageTest do
use ExUnit.Case, async: true
import ExUnit.CaptureLog
import Telemetry.Metrics, only: [summary: 2]
alias MusicLibrary.TelemetryRepo
alias MusicLibraryWeb.Telemetry.Storage
setup do
id = System.unique_integer([:positive])
metric = summary("music_library.test.metric_#{id}.duration", tags: [])
pid = start_supervised!({Storage, {[], [flush_interval_ms: 60_000]}})
on_exit(fn ->
TelemetryRepo.query!(
"DELETE FROM telemetry_datapoints WHERE metric_key LIKE ?1",
["%metric_#{id}%"]
)
end)
%{pid: pid, metric: metric, id: id}
end
describe "handle_cast/2" do
test "buffers datapoints in-memory without persisting", %{pid: pid, metric: metric} do
cast_point(pid, metric, 1.0, 100)
cast_point(pid, metric, 2.0, 200)
%{buffer: buffer} = :sys.get_state(pid)
key = metric_key(metric)
assert [%{measurement: 2.0, time: 200}, %{measurement: 1.0, time: 100}] =
Map.get(buffer, key)
assert db_count(key) == 0
end
test "many casts return quickly (no sync I/O on cast path)", %{pid: pid, metric: metric} do
{time_us, :ok} =
:timer.tc(fn ->
for _ <- 1..500, do: cast_point(pid, metric, 1.0, 0)
_ = :sys.get_state(pid)
:ok
end)
assert time_us < 200_000, "500 casts took #{time_us}us"
end
end
describe "handle_call({:data, metric}, ...)" do
test "force-flushes the requested metric and returns persisted rows", %{
pid: pid,
metric: metric
} do
cast_point(pid, metric, 42.0, 1000)
result = GenServer.call(pid, {:data, metric})
assert [%{measurement: m, time: 1000, label: nil}] = result
assert_in_delta m, 42.0, 0.001
assert db_count(metric_key(metric)) == 1
end
test "only flushes the requested metric, leaves others buffered", %{
pid: pid,
metric: metric_a,
id: id
} do
metric_b = summary("music_library.test.metric_#{id}_b.duration", tags: [])
cast_point(pid, metric_a, 1.0, 1)
cast_point(pid, metric_b, 2.0, 2)
_ = GenServer.call(pid, {:data, metric_a})
assert db_count(metric_key(metric_a)) == 1
assert db_count(metric_key(metric_b)) == 0
%{buffer: buffer} = :sys.get_state(pid)
refute Map.has_key?(buffer, metric_key(metric_a))
assert [%{measurement: 2.0}] = Map.get(buffer, metric_key(metric_b))
end
test "returns empty list when nothing has been cast", %{pid: pid, metric: metric} do
assert GenServer.call(pid, {:data, metric}) == []
end
end
describe "periodic :flush" do
test "drains the full buffer to SQLite", %{pid: pid, metric: metric, id: id} do
metric_b = summary("music_library.test.metric_#{id}_b.duration", tags: [])
cast_point(pid, metric, 1.5, 10)
cast_point(pid, metric_b, 2.5, 20)
send(pid, :flush)
_ = :sys.get_state(pid)
assert db_count(metric_key(metric)) == 1
assert db_count(metric_key(metric_b)) == 1
%{buffer: buffer} = :sys.get_state(pid)
assert buffer == %{}
end
test "prunes rows above retention_limit per metric", %{id: id} do
metric = summary("music_library.test.metric_#{id}_r.duration", tags: [])
pid =
start_supervised!({Storage, {[], [retention_limit: 3, flush_interval_ms: 60_000]}},
id: :storage_retention
)
for n <- 1..10, do: cast_point(pid, metric, n / 1.0, n)
send(pid, :flush)
_ = :sys.get_state(pid)
key = metric_key(metric)
assert db_count(key) == 3
{:ok, %{rows: rows}} =
TelemetryRepo.query(
"SELECT time FROM telemetry_datapoints WHERE metric_key = ?1 ORDER BY time ASC",
[key]
)
assert rows == [[8], [9], [10]]
end
end
describe "terminate/2" do
test "flushes the buffer on shutdown", %{pid: pid, metric: metric} do
cast_point(pid, metric, 7.0, 5000)
:ok = stop_supervised(MusicLibraryWeb.Telemetry.Storage)
{:ok, %{rows: rows}} =
TelemetryRepo.query(
"SELECT measurement, time FROM telemetry_datapoints WHERE metric_key = ?1",
[metric_key(metric)]
)
assert [[_, 5000]] = rows
refute Process.alive?(pid)
end
end
describe "error handling" do
test "logs a warning and keeps running when the flush raises", %{pid: pid, metric: metric} do
bad = %{measurement: {:not_a_number}, time: 1}
GenServer.cast(pid, {:telemetry_metric, bad, metric})
log =
capture_log(fn ->
send(pid, :flush)
_ = :sys.get_state(pid)
end)
assert log =~ "[Telemetry.Storage] flush"
assert Process.alive?(pid)
%{buffer: buffer} = :sys.get_state(pid)
assert buffer == %{}
end
end
defp cast_point(pid, metric, measurement, time) do
GenServer.cast(
pid,
{:telemetry_metric, %{measurement: measurement, time: time}, metric}
)
end
defp db_count(key) do
{:ok, %{rows: [[count]]}} =
TelemetryRepo.query(
"SELECT COUNT(*) FROM telemetry_datapoints WHERE metric_key = ?1",
[key]
)
count
end
defp metric_key(metric) do
Enum.join(
[
inspect(metric.__struct__),
Enum.join(metric.name, "."),
Enum.join(metric.tags, ".")
],
":"
)
end
end