diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f40936..05641a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +**0.1.3** + +- Typo corrections in README.md + **0.1.2** - bug fixes around `Threat Status`, choosing to not display when no IUCU data is available diff --git a/lib/application.ex b/lib/application.ex index 2a9cb89..061f431 100644 --- a/lib/application.ex +++ b/lib/application.ex @@ -4,8 +4,10 @@ defmodule PlantIdDiscordBot.Application do @impl true def start(_type, _args) do children = [ + PlantIdDiscordBot.ProcessRegistry, PlantIdDiscordBot.Consumer, PlantIdDiscordBot.RateLimiter, + {PlantIdDiscordBot.Metrics, []}, PlantIdDiscordBot.Scheduler, {Plug.Cowboy, scheme: :http, diff --git a/lib/metrics.ex b/lib/metrics.ex new file mode 100644 index 0000000..6e4faf6 --- /dev/null +++ b/lib/metrics.ex @@ -0,0 +1,96 @@ +defmodule PlantIdDiscordBot.Metrics do + use Agent + alias PlantIdDiscordBot.Metrics + + @file_path "priv/metrics" + + defstruct [ + :guild_id, + :guild_name, + :total_requests + ] + + def start_link(_opts) do + initial_state = + case File.exists?(Path.join(@file_path, "requests")) do + true -> read() + false -> %{} + end + + Agent.start_link(fn -> initial_state end, name: via_tuple(:metrics)) + end + + defp via_tuple(key) do + PlantIdDiscordBot.ProcessRegistry.via_tuple({__MODULE__, key}) + end + + defp get_pid do + [{pid, _value}] = Registry.lookup(PlantIdDiscordBot.ProcessRegistry, {__MODULE__, :metrics}) + pid + end + + def get(key) do + get_pid() + |> Agent.get(&Map.get(&1, key)) + end + + def get_all() do + get_pid() + |> Agent.get(& &1) + end + + def put(guild_id, guild_name) do + get_pid() + |> Agent.update(fn state -> + case Map.get(state, guild_id) do + nil -> + Map.put(state, guild_id, %Metrics{ + guild_id: guild_id, + guild_name: guild_name, + total_requests: 1 + }) + + existing -> + Map.put(state, guild_id, %Metrics{ + existing + | total_requests: existing.total_requests + 1 + }) + end + end) + end + + @doc """ + Reset metrics state and remove the saved file from disk. + """ + def reset() do + get_pid() + |> Agent.update(fn _ -> %{} end) + + file = Path.join(@file_path, "requests") + + if File.exists?(file) do + File.rm!(Path.join(@file_path, "requests")) + end + end + + @doc """ + Write the current state to disk. To be consumed by a CRON job. + """ + def write() do + File.mkdir_p!("priv/metrics") + + data = + get_all() + |> :erlang.term_to_binary() + + File.write!(Path.join(@file_path, "requests"), data) + end + + @doc """ + Read the state from disk. To be loaded into state on process start. + """ + def read() do + File.read!(Path.join(@file_path, "requests")) + |> :erlang.binary_to_term() + end +end diff --git a/lib/plantid_discord_bot/process_registry.ex b/lib/plantid_discord_bot/process_registry.ex new file mode 100644 index 0000000..e97c683 --- /dev/null +++ b/lib/plantid_discord_bot/process_registry.ex @@ -0,0 +1,17 @@ +defmodule PlantIdDiscordBot.ProcessRegistry do + def start_link do + Registry.start_link(keys: :unique, name: __MODULE__) + end + + def via_tuple(key) do + {:via, Registry, {__MODULE__, key}} + end + + def child_spec(_) do + Supervisor.child_spec( + Registry, + id: __MODULE__, + start: {__MODULE__, :start_link, []} + ) + end +end diff --git a/test/metrics_test.exs b/test/metrics_test.exs new file mode 100644 index 0000000..5fca5a6 --- /dev/null +++ b/test/metrics_test.exs @@ -0,0 +1,43 @@ +defmodule PlantIdDiscordBot.MetricsTest do + use ExUnit.Case + doctest PlantIdDiscordBot.Metrics + + test "put multiple entries into state" do + PlantIdDiscordBot.Metrics.reset() + PlantIdDiscordBot.Metrics.put(123, "Test Guild 1") + PlantIdDiscordBot.Metrics.put(321, "Test Guild 2") + + keys = + PlantIdDiscordBot.Metrics.get_all() + |> Map.keys() + + assert length(keys) == 2 + + assert %PlantIdDiscordBot.Metrics{ + guild_id: 123, + guild_name: "Test Guild 1", + total_requests: 1 + } == + PlantIdDiscordBot.Metrics.get(123) + + assert %PlantIdDiscordBot.Metrics{ + guild_id: 321, + guild_name: "Test Guild 2", + total_requests: 1 + } == + PlantIdDiscordBot.Metrics.get(321) + end + + test "update requests for a guild" do + PlantIdDiscordBot.Metrics.reset() + PlantIdDiscordBot.Metrics.put(123, "Test Guild 1") + PlantIdDiscordBot.Metrics.put(123, "Test Guild 1") + + assert %PlantIdDiscordBot.Metrics{ + guild_id: 123, + guild_name: "Test Guild 1", + total_requests: 2 + } == + PlantIdDiscordBot.Metrics.get(123) + end +end