defmodule Mobius do
@moduledoc """
Localized metrics reporter
"""
use Supervisor
alias Mobius.{Event, EventLog, MetricsTable, ReportServer, Scraper, Summary}
alias Telemetry.Metrics
@default_args [mobius_instance: :mobius, persistence_dir: "/data", autosave_interval: nil]
@type time_unit() :: :second | :minute | :hour | :day
@typedoc """
A function to process an event's measurements
This will be called on each measurement and will receive a tuple where the
first element is the name of the measurement and the second element is the
value. This function can process the value and return a new one.
"""
@type event_measurement_values() :: ({atom(), term()} -> term())
@typedoc """
Options you can pass an event
These options only apply to the `:event` argument to Mobius. If you want
to track metrics please see the `:metrics` argument to Mobius.
* `:tags` - list of tag names to save with the event
* `:measurement_values` - a function that will receive each measurement that
allows for data processing before storing the event in the event log
* `:group` - an atom that defines the event group, this will allow for filtering
on particular types of events for example: `:network`. Default is `:default`
"""
@type event_opt() ::
{:measurement_values, event_measurement_values()} | {:tags, [atom()]} | {:group, atom()}
@type event_def() :: [binary() | {binary(), keyword()}]
@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 default
values found in `Mobius.RRD`
* `:events` - a list of events for mobius to store in the event log
* `:event_log_size` - number of events to store (defaults to 500)
* `:clock` - module that implements the `Mobius.Clock` behaviour
* `:session` - a unique id to distinguish between different ties Mobius has ran
Mobius sessions allow you collect events to analyze across the different times
mobius ran. A good example of this might be measuring how fast an interface
makes its first connection. You can build averages over run times and measure
connection performance. This will allow you to know on average how fast a
device connects so you can check for increased or decreased performance between
runs.
By default Mobius will generate an UUID for each run.
"""
@type arg() ::
{:mobius_instance, instance()}
| {:metrics, [Metrics.t()]}
| {:persistence_dir, binary()}
| {:database, Mobius.RRD.t()}
| {:events, [event_def()]}
| {:event_log_size, integer()}
| {:clock, module()}
| {:session, session()}
@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
@type session() :: binary()
@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]))
args = Keyword.put_new(args, :session, UUID.uuid4())
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.TimeServer, args},
{Mobius.MetricsTable.Monitor, args},
{Mobius.EventsServer, args},
{Mobius.Registry, args},
{Mobius.Scraper, args},
{Mobius.ReportServer, args}
]
|> maybe_enable_autosave(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
@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),
:ok <- EventLog.save(instance: 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
@doc """
Get the latest metrics
The latest metrics are the metrics recorded between the last query for the
metrics and the query for the metrics that is being called.
"""
@spec get_latest_metrics(Mobius.instance()) :: [metric()]
def get_latest_metrics(instance \\ :mobius) do
ReportServer.get_latest_metrics(instance)
end
@doc """
Get the latest events
The latest events are the events recorded between the last query for the
events and the query for the events that is being called.
"""
@spec get_latest_events(Mobius.instance()) :: [Event.t()]
def get_latest_events(instance \\ :mobius) do
ReportServer.get_latest_events(instance)
end
end