defmodule Mobius do
@moduledoc """
Localized metrics reporter
"""
use Supervisor
alias Mobius.{MetricsTable, RemoteReporter, Scraper, Summary}
alias Telemetry.Metrics
@default_args [mobius_instance: :mobius, persistence_dir: "/data", autosave_interval: nil]
@type time_unit() :: :second | :minute | :hour | :day
@typedoc """
Arguments to Mobius
* `:name` - the name of the mobius instance (defaults to `:mobius`)
* `:metrics` - list of telemetry metrics for Mobius to track
* `:persistence_dir` - the top level directory where mobius will persist
* `:autosave_interval` - time in seconds between automatic writes of the
persistence data (default disabled) metric information
* `:database` - the `Mobius.RRD.t()` to use. This will default to the the default
values found in `Mobius.RRD`
"""
@type arg() ::
{:mobius_instance, instance()}
| {:metrics, [Metrics.t()]}
| {:persistence_dir, binary()}
| {:database, Mobius.RRD.t()}
| {:remote_reporter, RemoteReporter.t() | {RemoteReporter.t(), term()}}
| {:remote_report_interval, non_neg_integer()}
@typedoc """
The name of the Mobius instance
This is used to store data for a particular set of mobius metrics.
"""
@type instance() :: atom()
@type metric_type() :: :counter | :last_value | :sum | :summary
@typedoc """
The name of the metric
Example: `"vm.memory.total"`
"""
@type metric_name() :: binary()
@typedoc """
A single metric data point
* `:type` - the type of the metric
* `:value` - the value of the measurement for the metric
* `:tags` - a map of the tags for the metric
* `:timestamp` - the naive time in seconds the metric was sampled
* `:name` - the name of the metric
"""
@type metric() :: %{
type: metric_type(),
value: term(),
tags: map(),
timestamp: integer(),
name: binary()
}
@type timestamp() :: integer()
@doc """
Start Mobius
"""
def start_link(args) do
Supervisor.start_link(__MODULE__, ensure_args(args), name: name(args[:mobius_instance]))
end
defp name(instance) do
Module.concat(__MODULE__.Supervisor, instance)
end
@impl Supervisor
def init(args) do
mobius_persistence_path = Path.join(args[:persistence_dir], to_string(args[:mobius_instance]))
case ensure_mobius_persistence_dir(mobius_persistence_path) do
:ok ->
args =
args
|> Keyword.put(:persistence_dir, mobius_persistence_path)
|> Keyword.put_new(:database, Mobius.RRD.new())
MetricsTable.init(args)
children =
[
{Mobius.MetricsTable.Monitor, args},
{Mobius.Registry, args},
{Mobius.Scraper, args}
]
|> maybe_enable_autosave(args)
|> maybe_start_remote_reporter(args)
Supervisor.init(children, strategy: :one_for_one)
{:error, :enoent} ->
raise("persistence_path does not exist: #{mobius_persistence_path}")
{:error, msg} ->
raise("could not start mobius: #{msg}")
end
end
defp ensure_args(args) do
Keyword.merge(@default_args, args)
end
defp ensure_mobius_persistence_dir(persistence_path) do
case File.mkdir_p(persistence_path) do
:ok ->
:ok
{:error, :eexist} ->
:ok
error ->
error
end
end
defp maybe_enable_autosave(children, args) do
if is_number(args[:autosave_interval]) and args[:autosave_interval] > 0 do
children ++ [{Mobius.AutoSave, args}]
else
children
end
end
defp maybe_start_remote_reporter(children, args) do
case args[:remote_reporter] do
nil ->
children
_config ->
children ++ [{Mobius.RemoteReporterServer, make_remote_report_server_args(args)}]
end
end
defp make_remote_report_server_args(mobius_args) do
reporter = Keyword.fetch!(mobius_args, :remote_reporter)
report_interval = mobius_args[:remote_report_interval]
[
reporter: reporter,
report_interval: report_interval,
mobius_instance: mobius_args[:mobius_instance]
]
end
@doc """
Get the current metric information
If you configured Mobius to use a different name then you can pass in your
custom name to ensure Mobius requests the metrics from the right place.
"""
@spec info(Mobius.instance() | nil) :: :ok
def info() do
info(@default_args[:mobius_instance])
end
def info(instance) do
instance
|> MetricsTable.get_entries()
|> Enum.group_by(fn {metric_name, _type, _value, meta} -> {metric_name, meta} end)
|> Enum.each(fn {{metric_name, meta}, metrics} ->
reports =
Enum.map(metrics, fn {_metric_name, type, value, _meta} ->
"#{to_string(type)}: #{inspect(format_value(type, value))}\n"
end)
[
"Metric Name: ",
metric_name,
"\n",
"Tags: #{inspect(meta)}\n",
reports
]
|> IO.puts()
end)
end
defp format_value(:summary, summary_data) do
Summary.calculate(summary_data)
end
defp format_value(_, value) do
value
end
@doc """
Persist the metrics to disk
"""
@spec save(instance()) :: :ok | {:error, reason :: term()}
def save(), do: save(@default_args[:mobius_instance])
def save(instance) do
start_t = System.monotonic_time()
prefix = [:mobius, :save]
:telemetry.execute(prefix ++ [:start], %{system_time: System.system_time()}, %{
instance: instance
})
with :ok <- Scraper.save(instance),
:ok <- MetricsTable.Monitor.save(instance) do
duration = System.monotonic_time() - start_t
:telemetry.execute(prefix ++ [:stop], %{duration: duration}, %{instance: instance})
:ok
else
error ->
duration = System.monotonic_time() - start_t
:telemetry.execute(
prefix ++ [:exception],
%{reason: inspect(error), duration: duration},
%{instance: instance}
)
error
end
end
end