Current section
Files
Jump to
Current section
Files
lib/prometheus_exometer.ex
defmodule PrometheusExometer do
@moduledoc """
Interface functions which publish Exometer metrics in Prometheus format.
"""
alias PrometheusExometer.FormatText
# require Lager
@doc """
Format data from Exometer metrics in Prometheus text format.
It takes a map with configuration options.
config = %{namespace: list(atom), converters: list(module)}
Keys:
- `namespace`: a prefix which will be added to each metric, normally the app name
- `converters`: a list of modules which have callback functions to convert
internal Exometer names into external format
"""
@spec scrape(map) :: iolist
def scrape(config) do
start_time = :os.timestamp()
entries = :exometer.find_entries([:_])
entries = Enum.sort(entries)
# Lager.debug("entries: #{inspect entries, pretty: true, limit: 30_000}")
entry_info = List.foldl(entries, %{}, &collect_children/2)
entry_keys =
entry_info
|> Map.keys()
|> Enum.sort()
results =
Enum.flat_map(entry_keys, fn name ->
value = entry_info[name]
# Lager.debug("name: #{inspect name} #{inspect value.info}")
# Enum.each(value.children, &(Lager.debug("children: #{inspect &1, pretty: true}")))
FormatText.format_entry(value, config)
end)
# {results, _names_seen} = List.foldl(entries, {[], %{}}, &format_entry/2)
[
results,
FormatText.format_scrape_duration(config, start_time),
FormatText.format_namespace_up(config)
]
end
@doc "Scrape with default config"
@spec scrape() :: iolist
def scrape, do: scrape(%{})
defp collect_children({exometer_name, exometer_type, :enabled}, parents) do
info = :exometer.info(exometer_name)
options = info[:options]
prometheus_options = options[:prometheus] || %{}
case Map.fetch(prometheus_options, :parent) do
:error ->
# Parent or simple metric
# Lager.debug("parent #{inspect exometer_name} info #{inspect info}")
Map.put(parents, exometer_name, %{info: info, children: []})
{:ok, parent_name} ->
# Child, may also be a parent if it's a composite type
# Lager.debug("child #{inspect exometer_name} info #{inspect info}")
if exometer_type in [:prometheus_counter, :prometheus_gauge, :prometheus_histogram] do
# parents = Map.put(parents, exometer_name, %{info: info, children: []})
update_in(parents[parent_name][:children], &(&1 ++ [info]))
else
if Map.has_key?(prometheus_options, :internal) do
parents
else
update_in(parents[parent_name][:children], &(&1 ++ [info]))
end
end
end
end
defp collect_children({_, _, _}, acc) do
# Skip disabled entries
acc
end
@doc "Filter list of modules to ones with a `prometheus_convert_name` callback"
@spec get_converters(list(module)) :: list(module)
def get_converters([]), do: []
def get_converters(modules) do
Enum.filter(modules, &exports_prometheus_convert_name?/1)
end
@doc "Return true if module exports an `prometheus_convert_name` callback function"
@spec exports_prometheus_convert_name?(module) :: boolean
def exports_prometheus_convert_name?(module) do
exports = module.module_info(:exports)
Keyword.has_key?(exports, :prometheus_convert_name)
end
end