Skip to content

Instantly share code, notes, and snippets.

@alg
Last active July 30, 2018 09:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alg/6cf0d0309185c3cdb40deffbde6e8220 to your computer and use it in GitHub Desktop.
Save alg/6cf0d0309185c3cdb40deffbde6e8220 to your computer and use it in GitHub Desktop.
ETS metrics
defmodule Metrics do
use GenServer
@type metric_pid :: pid()
@type server :: term()
@type time_tag :: :daily | :hourly | :minutely
@type metric :: {term(), term()}
@type server_metric :: {:metric, {server(), time_tag(), term()}, metric()}
@spec start_link() :: metric_pid()
def start_link() do
GenServer.start_link(__MODULE__, [])
end
@doc """
Writes a timed metric for the given server and time. Creates the number of time-tagged records for
daily, hourly and minutely metrics. Overwrites previous values for seamless round-robin.
"""
@spec write(metric_pid(), server(), DateTime.t(), {term(), term()}) :: :ok
def write(metrics, server, time, metric = {_, _}) do
GenServer.cast(metrics, {:write, server, time, metric})
end
@doc """
Reads time-tagged metrics for the given server.
"""
@spec read(metric_pid(), server(), time_tag()) :: [server_metric()]
def read(metrics, server, time_tag) do
GenServer.call(metrics, {:read, server, time_tag})
end
@doc """
Reads all metrics for the given server.
"""
@spec read(metric_pid(), server()) :: [server_metric()]
def read(metrics, server) do
GenServer.call(metrics, {:read, server})
end
@doc """
Prints statistics for the metrics storage.
"""
@spec info(metric_pid()) :: :ok
def info(metrics) do
GenServer.call(metrics, :info)
end
def init(_) do
db = :ets.new(:metrics, [:set, {:keypos, 2}])
{:ok, db}
end
def handle_call(:info, _from, db) do
IO.inspect :ets.info(db)
{:reply, :ok, db}
end
def handle_call({:read, server}, _from, db) do
matches =
:ets.match_object(db, {:metric, {server, :_, :_}, :_})
{:reply, matches, db}
end
def handle_call({:read, server, time_tag}, _from, db) do
matches =
:ets.match_object(db, {:metric, {server, time_tag, :_}, :_})
{:reply, matches, db}
end
def handle_cast({:write, server, time, metric = {_, _}}, db) do
now = Timex.now()
if Timex.diff(now, time, :days) < 31 do
h = if Timex.diff(now, time, :hours) < 24 do
m = if Timex.diff(now, time, :minutes) < 60 do
[{:metric, {server, :minutely, time.minute}, metric}]
else
[]
end
[{:metric, {server, :hourly, time.hour}, metric}] ++ m
else
[]
end
recs = [{:metric, {server, :daily, time.day}, metric}] ++ h
:ets.insert(db, recs)
end
{:noreply, db}
end
end
defmodule MetricsTest do
use ExUnit.Case
alias Timex.Duration, as: D
setup do
{:ok, m} = GenServer.start_link(Metrics, [])
{:ok, metrics: m}
end
test "Storing metrics", %{metrics: m} do
met = {:foo, 1}
t = Timex.now()
Metrics.write(m, 123, t, met)
assert Enum.sort(Metrics.read(m, 123)) == [
{:metric, {123, :daily, t.day}, met},
{:metric, {123, :hourly, t.hour}, met},
{:metric, {123, :minutely, t.minute}, met}
]
assert Metrics.read(m, 111) == []
end
test "Overwriting metrics", %{metrics: m} do
now = Timex.now()
t = Timex.subtract(now, D.from_minutes(1))
Metrics.write(m, 123, t, {:foo, :bar})
Metrics.write(m, 123, now, {:foo, :baz})
assert Enum.sort(Metrics.read(m, 123)) == [
{:metric, {123, :daily, now.day}, {:foo, :baz}},
{:metric, {123, :hourly, now.hour}, {:foo, :baz}},
{:metric, {123, :minutely, t.minute}, {:foo, :bar}},
{:metric, {123, :minutely, now.minute}, {:foo, :baz}}
]
assert Enum.sort(Metrics.read(m, 123, :minutely)) == [
{:metric, {123, :minutely, t.minute}, {:foo, :bar}},
{:metric, {123, :minutely, now.minute}, {:foo, :baz}}
]
end
test "Not storing out-of-scope minute metrics", %{metrics: m} do
now = Timex.now()
time = Timex.subtract(now, D.from_minutes(61))
metric = {:foo, :bar}
Metrics.write(m, :sid, time, metric)
assert Enum.sort(Metrics.read(m, :sid)) == [
{:metric, {:sid, :daily, time.day}, metric},
{:metric, {:sid, :hourly, time.hour}, metric}
]
end
test "Not storing out-of-scope hour metrics", %{metrics: m} do
now = Timex.now()
time = Timex.subtract(now, D.from_minutes(24 * 60 + 1))
metric = {:foo, :bar}
Metrics.write(m, :sid, time, metric)
assert Metrics.read(m, :sid) == [
{:metric, {:sid, :daily, time.day}, metric}
]
end
test "Not storing out-of-scope day metrics", %{metrics: m} do
now = Timex.now()
time = Timex.subtract(now, D.from_days(32))
metric = {:foo, :bar}
Metrics.write(m, :sid, time, metric)
assert Metrics.read(m, :sid) == []
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment