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 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 different different times mobius ran. A good example of this might 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 and 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