defmodule Wobserver.Util.Metrics.Formatter do
@moduledoc ~S"""
Formatter.
"""
alias Wobserver.Util.Node.Discovery
@doc ~S"""
Format a set of `data` with a `label`.
The `data` must be given as a `list` of tuples with the following format: `{value, labels}`, where `labels` is a keyword list with labels and their values.
The following options can also be given:
- `type`, the type of the metric. The following values are currently supported: `:gauge`, `:counter`.
- `help`, a single line text description of the metric.
"""
@callback format_data(
name :: String.t,
data :: [{integer | float, keyword}],
type :: :atom,
help :: String.t
) :: String.t
@doc ~S"""
Combines formatted metrics together.
Arguments:
- `metrics`, a list of formatted metrics for one node.
"""
@callback combine_metrics(
metrics :: list[String.t]
) :: String.t
@doc ~S"""
Merges formatted sets of metrics from different nodes together.
The merge should prevent double declarations of help and type.
Arguments:
- `metrics`, a list of formatted sets metrics for multiple node.
"""
@callback merge_metrics(
metrics :: list[String.t]
) :: String.t
@doc ~S"""
Format a set of `data` with a `label` for a metric parser/aggregater.
The following options can also be given:
- `type`, the type of the metric. The following values are currently supported: `:gauge`, `:counter`.
- `help`, a single line text description of the metric.
- `formatter`, a module implementing the `Formatter` behaviour to format metrics.
"""
@spec format(
data :: any,
label :: String.t,
type :: :gauge | :counter | nil,
help :: String.t | nil,
formatter :: atom | nil
) :: String.t | :error
def format(data, label, type \\ nil, help \\ nil, formatter \\ nil)
def format(data, label, type, help, nil) do
format(data, label, type, help, Application.get_env(
:wobserver,
:metric_format,
Wobserver.Util.Metrics.Prometheus
))
end
def format(data, label, type, help, formatter) when is_binary(formatter) do
{new_formatter, []} = Code.eval_string formatter
format(data, label, type, help, new_formatter)
end
def format(data, label, type, help, formatter)
when is_integer(data) or is_float(data) do
format([{data, []}], label, type, help, formatter)
end
def format(data, label, type, help, formatter) when is_map(data) do
data
|> map_to_data()
|> format(label, type, help, formatter)
end
def format(data, label, type, help, formatter) when is_binary(data) do
{new_data, []} = Code.eval_string data
format(new_data, label, type, help, formatter)
catch
:error, _ -> :error
end
def format(data, label, type, help, formatter) when is_function(data) do
data.()
|> format(label, type, help, formatter)
end
def format(data, label, type, help, formatter) do
cond do
Keyword.keyword?(data) ->
data
|> list_to_data()
|> format(label, type, help, formatter)
is_list(data) ->
data =
data
|> Enum.map(fn {value, labels} ->
{value, Keyword.merge([node: Discovery.local.name], labels)}
end)
formatter.format_data(label, data, type, help)
true ->
:error
end
end
@doc ~S"""
Formats a keyword list of metrics using a given `formatter`.
**Metrics**
The key is the name of the metric and the value can be given in the following formats:
- `data`
- `{data, type}`
- `{data, type, help}`
The different fields are:
- `data`, the actual metrics information.
- `type`, the type of the metric.
Possible values: `:gauge`, `:counter`.
- `help`, a one line text description of the metric.
The `data` can be given in the following formats:
- `integer` | `float`, just a single value.
- `map`, where every key will be turned into a type value.
- `keyword` list, where every key will be turned into a type value
- `list` of tuples with the following format: `{value, labels}`, where `labels` is a keyword list with labels and their values.
- `function` | `string`, a function or String that can be evaluated to a function, which, when called, returns one of the above data-types.
Example:
```elixir
iex> Wobserver.Util.Metrics.Formatter.format_all [simple: 5]
"simple{node=\"10.74.181.35\"} 5\n"
```
```elixir
iex> Wobserver.Util.Metrics.Formatter.format_all [simple: {5, :gauge}]
"# TYPE simple gauge\nsimple{node=\"10.74.181.35\"} 5\n"
```
```elixir
iex> Wobserver.Util.Metrics.Formatter.format_all [simple: {5, :gauge, "Example desc."}]
"# HELP simple Example desc.\n
# TYPE simple gauge\n
simple{node=\"10.74.181.35\"} 5\n"
```
```elixir
iex> Wobserver.Util.Metrics.Formatter.format_all [simple: %{floor: 5, wall: 8}]
"simple{node=\"10.74.181.35\",type=\"floor\"} 5\n
simple{node=\"10.74.181.35\",type=\"wall\"} 8\n"
```
```elixir
iex> Wobserver.Util.Metrics.Formatter.format_all [simple: [floor: 5, wall: 8]]
"simple{node=\"10.74.181.35\",type=\"floor\"} 5\n
simple{node=\"10.74.181.35\",type=\"wall\"} 8\n"
```
```elixir
iex> Wobserver.Util.Metrics.Formatter.format_all [simple: [{5, [location: :floor]}, {8, [location: :wall]}]]
"simple{node=\"10.74.181.35\",location=\"floor\"} 5\n
simple{node=\"10.74.181.35\",location=\"wall\"} 8\n"
```
"""
@spec format_all(data :: list, formatter :: atom) :: String.t | :error
def format_all(data, formatter \\ nil)
def format_all(data, nil) do
format_all(data, Application.get_env(
:wobserver,
:metric_format,
Wobserver.Util.Metrics.Prometheus
))
end
def format_all(data, formatter) do
formatted =
data
|> Enum.map(&helper(&1, formatter))
case Enum.member?(formatted, :error) do
true -> :error
_ -> formatted |> formatter.combine_metrics
end
end
@doc ~S"""
Merges formatted sets of metrics from different nodes together using a given `formatter`.
The merge should prevent double declarations of help and type.
Arguments:
- `metrics`, a list of formatted sets metrics for multiple node.
"""
@spec merge_metrics(metrics :: list(String.t), formatter :: atom)
:: String.t | :error
def merge_metrics(metrics, formatter \\ nil)
def merge_metrics(metrics, nil) do
merge_metrics(metrics,
Application.get_env(
:wobserver,
:metric_format,
Wobserver.Util.Metrics.Prometheus
)
)
end
def merge_metrics(metrics, formatter) do
# Old way: One node error, means no metrics
#
# case Enum.member?(metrics, :error) do
# true -> :error
# false -> formatter.merge_metrics(metrics)
# end
metrics
|> Enum.filter(fn m -> m != :error end)
|> formatter.merge_metrics
end
# Helpers
defp helper({key, {data, type, help}}, formatter) do
format(data, Atom.to_string(key), type, help, formatter)
end
defp helper({key, {data, type}}, formatter) do
format(data, Atom.to_string(key), type, nil, formatter)
end
defp helper({key, data}, formatter) do
format(data, Atom.to_string(key), nil, nil, formatter)
end
defp map_to_data(map) do
map
|> Map.to_list
|> Enum.filter(fn {a, _} -> a != :__struct__ && a != :total end)
|> list_to_data()
end
defp list_to_data(list) do
list
|> Enum.map(fn {key, value} -> {value, [type: key]} end)
end
end