New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add custom metrics and metric history fork to LiveDashboard #613
Changes from 6 commits
91be7ac
4425ca4
935da76
b9917f1
621b06e
28061f8
2391afd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
defmodule CompaniesWeb.HistoricalData do | ||
@moduledoc """ | ||
Single source of truth to gather all individual modules that provide historical | ||
data to live dashboard and merge their seperate history maps into one. If adding | ||
a new source of metric history for LiveDashboard, it only needs to implement | ||
it's own signatures/0 method as the other modules here do, and be added the the | ||
list of signatures below. | ||
""" | ||
alias CompaniesWeb.{RepoMetricsHistory, ViewingStats} | ||
|
||
def signatures do | ||
for module <- [RepoMetricsHistory, ViewingStats], reduce: %{} do | ||
acc -> Map.merge(acc, apply(module, :signatures, [])) | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
defmodule CompaniesWeb.RepoMetricsHistory do | ||
@moduledoc """ | ||
GenServer to handle historical data for Ecto queries and rebroadcast to | ||
LiveDashboard under seperate history-focused telemetry namespace. | ||
""" | ||
|
||
use GenServer | ||
|
||
@telemetry_event [:companies, :repo, :query] | ||
@historic_metric [:ecto, :dashboard, :query] | ||
@history_buffer_size 500 | ||
|
||
def signatures do | ||
%{ | ||
@historic_metric => {__MODULE__, :data, []} | ||
} | ||
end | ||
|
||
def telemetry_event, do: @telemetry_event | ||
|
||
def start_link([]) do | ||
GenServer.start_link(__MODULE__, %{}, name: __MODULE__) | ||
end | ||
|
||
def init(_state) do | ||
{:ok, %{history: CircularBuffer.new(@history_buffer_size)}} | ||
end | ||
|
||
def data(%{event_name: event_name} = metric) do | ||
if List.starts_with?(event_name, @historic_metric) do | ||
GenServer.call(__MODULE__, {:data, metric}) | ||
else | ||
[] | ||
end | ||
end | ||
|
||
def setup_handlers do | ||
:telemetry.attach( | ||
"aggregation-handler-#{__MODULE__}", | ||
@telemetry_event, | ||
&__MODULE__.handle_event/4, | ||
nil | ||
) | ||
end | ||
|
||
def handle_event(@telemetry_event, metric_map, metadata, config) do | ||
GenServer.cast(__MODULE__, {:telemetry_metric, metric_map, metadata, config}) | ||
end | ||
|
||
def handle_call({:data, _metric}, _from, %{history: history}) do | ||
{:reply, CircularBuffer.to_list(history), %{history: history}} | ||
end | ||
|
||
def handle_cast({:telemetry_metric, metric_map, metadata, _config}, %{history: history}) do | ||
time = System.system_time(:second) | ||
:telemetry.execute(@historic_metric, metric_map, metadata) | ||
|
||
new_history = CircularBuffer.insert(history, %{data: metric_map, time: time, metadata: pruned_metadata(metadata)}) | ||
|
||
{:noreply, %{history: new_history}} | ||
end | ||
|
||
defp pruned_metadata(metadata) do | ||
# for now keep it all, reminder to either keep or drop selected fields based on dashboard usage to conserve memory, | ||
# ideally via some published source of truth hook from dahsboard module to ensure correctness | ||
metadata | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. José expressed concern about memory, and I think this will be the main way we address that concern in practice, though this method will likely need to consider the metric as well as the metadata to decide what to save vs discard... but I think for now we can either leave this as is, or kill, and look at memory usage without discarding anything before deciding if it's a practical issue for us, though we may want to tackle even if it's not a big issue here so that visibility is in place in LiveDashboard to support hooks doing this pruning intelligently. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On newer versions of my fork, I'm actually just leaving out the metadata entirely and most of the metrics seem to work exactly the same, so possibly we can do the same here, though I'm curious to see the difference in memory impact both ways/it's entirely possible updates to LiveDashboard may start making use of this metadata in new releases that it's not using now. |
||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
defmodule CompaniesWeb.ViewingStats do | ||
@moduledoc """ | ||
GenServer to handle aggregating simple app stats and history for telemetry_poller | ||
to pass on to LiveDashboard. | ||
""" | ||
|
||
use GenServer | ||
|
||
@telemetry_event [:page_views, :count_events] | ||
@historic_metrics [:page_views, :companies_web] | ||
@history_buffer_size 500 | ||
|
||
def signatures do | ||
%{ | ||
@historic_metrics => {__MODULE__, :data, []} | ||
} | ||
end | ||
|
||
def telemetry_event, do: @telemetry_event | ||
|
||
def start_link([]) do | ||
GenServer.start_link(__MODULE__, %{}, name: __MODULE__) | ||
end | ||
|
||
def init(_state) do | ||
{:ok, %{history: CircularBuffer.new(@history_buffer_size), current: %{}}} | ||
end | ||
|
||
def data(%{event_name: event_name} = metric) do | ||
if List.starts_with?(event_name, @historic_metrics) do | ||
GenServer.call(__MODULE__, {:data, metric}) | ||
else | ||
[] | ||
end | ||
end | ||
|
||
def setup_handlers do | ||
:telemetry.attach( | ||
"aggregation-handler-#{__MODULE__}", | ||
@telemetry_event, | ||
&__MODULE__.handle_event/4, | ||
nil | ||
) | ||
end | ||
|
||
def handle_event(@telemetry_event, map, metadata, config) do | ||
GenServer.cast(__MODULE__, {:telemetry_metric, map, metadata, config}) | ||
end | ||
|
||
def emit do | ||
GenServer.cast(__MODULE__, :emit_telemetry) | ||
end | ||
|
||
def handle_call({:raw_data, _metric}, _from, %{history: history} = state) do | ||
{:reply, history, state} | ||
end | ||
|
||
def handle_call({:data, metric}, _from, %{history: history} = state) do | ||
local_metric = List.last(metric.name) | ||
|
||
reply = | ||
for {time, time_metrics} <- history, | ||
{^local_metric, data} <- time_metrics do | ||
%{data: %{local_metric => data}, time: time} | ||
end | ||
|
||
{:reply, reply, state} | ||
end | ||
|
||
def handle_cast(:emit_telemetry, %{history: history, current: current}) do | ||
time = System.system_time(:second) | ||
|
||
for {key, value} <- current do | ||
:telemetry.execute(@historic_metrics, %{key => value}) | ||
end | ||
|
||
{:noreply, %{history: CircularBuffer.insert(history, {time, current}), current: %{}}} | ||
end | ||
|
||
def handle_cast({:telemetry_metric, metric_map, _metadata, _config}, %{current: current} = state) do | ||
updated_current = | ||
for {key, value} <- metric_map, reduce: current do | ||
acc -> Map.put_new(acc, key, 0) |> update_in([key], &(&1 + value)) | ||
end | ||
|
||
{:noreply, %{state | current: updated_current}} | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is technically out of scope, but without this I accidentally got some errors submitted to my work AppSignal account, because I guess I set the env var for APPSIGNAL_PUSH_API_KEY at some point and it was still set... very confusing seeing errors from an unrelated project show up in work project 🙃