defmodule TelemetryUI do
use Supervisor
defmodule Page do
@moduledoc false
defstruct id: nil, title: nil, metrics: [], ui_options: []
def cast_all(pages = [{_, _} | _]), do: Enum.map(pages, &cast/1)
def cast_all(pages = [{_, _, _} | _]), do: Enum.map(pages, &cast/1)
def cast_all(metrics), do: [cast({"", metrics})]
defp cast({title, metrics}), do: cast({title, metrics, []})
defp cast({title, metrics, options}) do
%__MODULE__{
id: cast_id(title),
title: title,
metrics: List.wrap(metrics),
ui_options: Keyword.get(options, :ui_options, [])
}
end
defp cast_id(title) do
TelemetryUI.Slug.slugify(title)
end
end
def start_link(opts) do
opts[:metrics] || raise ArgumentError, "the :metrics option is required by #{inspect(__MODULE__)}"
pages = Page.cast_all(opts[:metrics])
name = Keyword.get(opts, :name, :default)
theme = struct!(TelemetryUI.Theme, opts[:theme] || %{})
scale = Enum.uniq([theme.primary_color] ++ theme.scale)
theme = %{theme | scale: scale}
state = %{
name: name,
backend: opts[:backend],
theme: theme,
pages: pages
}
validate_pages!(state.pages)
validate_theme!(state.theme)
Supervisor.start_link(__MODULE__, state, name: Module.concat(__MODULE__, name))
end
def child_spec(opts) do
id = Keyword.get(opts, :name)
Supervisor.child_spec(super(opts), id: id)
end
@impl Supervisor
def init(state) do
children = [
{TelemetryUI.Config, config: state, name: config_name(state.name)}
]
children =
children ++
if state.backend do
metrics =
state.pages
|> Enum.flat_map(& &1.metrics)
|> Enum.map(&Map.get(&1, :telemetry_metric))
|> Enum.reject(&is_nil/1)
|> Enum.uniq_by(&{&1.name, &1.tags})
[
{TelemetryUI.WriteBuffer, backend: state.backend, name: writer_buffer_name(state.name)},
{TelemetryUI.Reporter, metrics: metrics, write_buffer: writer_buffer_name(state.name)}
]
else
[]
end
children =
children ++
if state.backend && state.backend.pruner_interval_ms do
[
{TelemetryUI.Pruner, backend: state.backend}
]
else
[]
end
children =
if Application.get_env(:telemetry_ui, :disabled, false) do
[]
else
children
end
Supervisor.init(children, strategy: :one_for_one)
end
def metric_data(name, metric, filters) do
TelemetryUI.Scraper.metric(backend(name), metric, filters)
end
def insert_metric_data(name, event) do
TelemetryUI.WriteBuffer.insert(name, event)
end
def pages(name), do: config(name, :pages)
def theme(name), do: config(name, :theme)
def backend(name), do: config(name, :backend)
def valid_share_key?(share_key), do: is_binary(share_key) and String.length(share_key) <= 15
def valid_share_url?(url), do: is_binary(URI.parse(url).host)
def page_by_id(name, id), do: Enum.find(pages(name), &(&1.id === id))
def page_by_title(name, title), do: Enum.find(pages(name), &(&1.title === title))
def metric_by_id(name, id) do
name
|> pages()
|> Enum.flat_map(& &1.metrics)
|> Enum.reject(&is_nil(Map.get(&1, :id)))
|> Enum.find(&(&1.id === id))
end
def writer_buffer_name(name), do: Module.concat(TelemetryUI.WriteBuffer, name)
def config_name(name), do: Module.concat(TelemetryUI.Config, name)
defp config(name, key), do: GenServer.call(config_name(name), key)
defp validate_pages!(pages) do
pages
|> Enum.flat_map(& &1.metrics)
|> Enum.each(fn metric ->
unless TelemetryUI.Web.Component.impl_for(metric) do
raise TelemetryUI.InvalidMetricWebComponentError.exception({metric})
end
end)
end
defp validate_theme!(theme) do
if not is_nil(theme.share_key) and not valid_share_key?(theme.share_key) do
raise TelemetryUI.InvalidThemeShareKeyError.exception(theme.share_key)
end
end
end