195 lines
5.3 KiB
Elixir
195 lines
5.3 KiB
Elixir
defmodule MusicLibraryWeb.Telemetry.StorageTest do
|
|
use ExUnit.Case, async: false
|
|
|
|
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
|