Packages
prom_ex
0.1.11-alpha
1.12.0
1.11.0
1.10.0
1.9.0
1.8.0
1.7.1
1.7.0
retired
1.6.0
1.5.0
1.4.1
1.4.0
1.3.0
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
0.1.15-beta
0.1.14-beta
0.1.13-beta
0.1.12-beta
0.1.11-alpha
0.1.10-alpha
0.1.9-alpha
0.1.8-alpha
0.1.7-alpha
0.1.6-alpha
0.1.5-alpha
0.1.4-alpha
0.1.3-alpha
0.1.2-alpha
0.1.1-alpha
0.1.0-alpha
Prometheus metrics and Grafana dashboards for all of your favorite Elixir libraries
Current section
Files
Jump to
Current section
Files
lib/core/aggregator.ex
defmodule PromEx.TelemetryMetricsPrometheus.Core.Aggregator do
@moduledoc false
require Logger
alias Telemetry.Metrics
alias PromEx.TelemetryMetricsPrometheus.Core
@typep bucket :: {upper_bound :: String.t(), count :: non_neg_integer()}
@typep sample :: {name :: :telemetry.event_name(), {labels :: map(), measurement :: number()}}
@typep key :: {name :: :telemetry.event_name(), map()}
@typep aggregation :: {[bucket()], non_neg_integer(), number()}
@spec aggregate(Core.metrics(), atom(), atom()) :: :ok
def aggregate(definitions, table_id, dist_table_id) do
for %Metrics.Distribution{} = metric <- definitions do
:ets.take(dist_table_id, metric.name)
|> aggregate_and_store(metric, table_id)
end
:ok
end
@spec aggregate_and_store(samples :: [sample()], Metrics.Distribution.t(), atom()) :: :ok
defp aggregate_and_store(samples, metric, tid) do
samples
|> group_samples()
|> Enum.map(fn {name, measurements} ->
Enum.map(measurements, fn {labels, samples} ->
key = {name, labels}
prev_agg = get_aggregation(key, tid)
Enum.sort(samples)
|> bucket_measurements(metric.reporter_options[:buckets])
|> merge(prev_agg)
|> put_aggregation(key, tid)
end)
end)
:ok
end
@spec get_time_series(atom()) :: %{:telemetry.event_name() => [sample()]}
def get_time_series(table_id) do
:ets.tab2list(table_id)
|> Stream.filter(&filter_and_drop_time_series_with_bad_tag_values(&1, table_id))
|> Enum.group_by(fn row -> row |> elem(0) |> elem(0) end)
end
defp filter_and_drop_time_series_with_bad_tag_values({[_, %{}], _}, _), do: true
defp filter_and_drop_time_series_with_bad_tag_values({key, _}, table_id) do
key
|> elem(1)
|> Enum.map(fn {label_key, value} ->
case String.Chars.impl_for(value) do
nil ->
Logger.warn(
"Dropping aggregation for bad tag value. metric:=#{inspect(elem(key, 0))} tag: #{inspect(label_key)}"
)
delete_aggregation(table_id, key)
false
_ ->
true
end
end)
|> Enum.all?()
end
defp delete_aggregation(table_id, key) do
:ets.delete(table_id, key)
end
defp merge(new, {}), do: new
defp merge({l_b, l_c, l_s}, {r_b, r_c, r_s}) do
buckets =
Enum.zip(l_b, r_b)
|> Enum.map(fn {{bucket, a}, {bucket, b}} -> {bucket, a + b} end)
{buckets, l_c + r_c, l_s + r_s}
end
@spec get_aggregation(key :: key(), table :: atom()) :: {} | aggregation()
defp get_aggregation(key, table) do
case :ets.lookup(table, key) do
[] -> {}
[agg] -> agg |> elem(1)
end
end
@spec put_aggregation(aggregation :: nil | aggregation(), key :: key(), table :: atom()) :: true
def put_aggregation(nil, _, _), do: true
def put_aggregation(aggregation, key, tid) do
:ets.insert(tid, {key, aggregation})
end
@spec group_samples(samples :: [sample()]) :: map()
def group_samples(samples) do
Enum.reduce(samples, %{}, fn {name, {labels, measurement}}, acc ->
metric = Map.get(acc, name, %{})
values = Map.get(metric, labels, [])
new_values = [measurement | values]
new_metric = Map.put(metric, labels, new_values)
Map.put(acc, name, new_metric)
end)
end
@spec bucket_measurements(measurements :: [number()], buckets :: Core.Distribution.buckets()) ::
{[bucket()], non_neg_integer(), number()}
def bucket_measurements(measurements, [b | buckets]),
do: bucket(measurements, buckets, b, 0, 0, [])
defp bucket([], [], _, count, sum, result), do: {Enum.reverse(result), count, sum}
defp bucket(measurements, [], "+Inf", count, sum, result) do
{new_count, new_sum} =
Enum.reduce(measurements, {count, sum}, fn m, {c, s} ->
{c + 1, s + m}
end)
bucket([], [], "+Inf", new_count, new_sum, [{"+Inf", new_count} | result])
end
defp bucket([], buckets, cur_bucket, count, sum, result) do
rest = Enum.reverse([cur_bucket | buckets]) |> Enum.map(&{"#{&1}", count})
bucket([], [], nil, count, sum, rest ++ result)
end
defp bucket([m | r_m] = measurements, [b | r_b] = buckets, cur_bucket, count, sum, result) do
if m <= cur_bucket do
bucket(r_m, buckets, cur_bucket, count + 1, sum + m, result)
else
bucket(measurements, r_b, b, count, sum, [{"#{cur_bucket}", count} | result])
end
end
end