Skip to content

Commit 0eb7637

Browse files
authored
Extracted from metric_api's statman_reporter.ex (#1)
...and turned into a separate application.
2 parents ca4f29a + 7cce153 commit 0eb7637

File tree

10 files changed

+310
-2
lines changed

10 files changed

+310
-2
lines changed

.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where 3rd-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
telemetry_metrics_statman-*.tar
24+

.travis.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
language: elixir
2+
# fossa is Ubuntu 20.04
3+
dist: fossa
4+
elixir: '1.10.3'
5+
otp_release: '22.3.4'
6+
before_install:
7+
- git config --global '[email protected]:GameAnalytics.insteadOf' 'https://github.com/GameAnalytics'
8+
script:
9+
- mix test
10+
- travis_wait mix dialyzer
11+
- mix credo
12+
13+
# We want to cache Dialyzer PLTs, in order not to have to rebuild them
14+
# every time. (Dialyzer will automatically rebuild them when needed.)
15+
# They are in _build/dev, but since we can only cache an entire
16+
# directory at a time, we need to remove _build/dev/{lib,rel} before
17+
# caching.
18+
before_cache:
19+
- rm -rf _build/dev/lib _build/dev/rel
20+
cache:
21+
directories:
22+
- _build/dev

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
# telemetry_metrics_statman
2-
Telemetry.Metrics reporter for Statman
1+
# TelemetryMetricsStatman
2+
3+
`Telemetry.Metrics` reporter for Statman.
4+

config/config.exs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
use Mix.Config
4+
5+
# This configuration is loaded before any dependency and is restricted
6+
# to this project. If another project depends on this project, this
7+
# file won't be loaded nor affect the parent project. For this reason,
8+
# if you want to provide default values for your application for
9+
# 3rd-party users, it should be done in your "mix.exs" file.
10+
11+
# You can configure your application as:
12+
#
13+
# config :telemetry_metrics_statman, key: :value
14+
#
15+
# and access this configuration in your application as:
16+
#
17+
# Application.get_env(:telemetry_metrics_statman, :key)
18+
#
19+
# You can also configure a 3rd-party app:
20+
#
21+
# config :logger, level: :info
22+
#
23+
24+
# It is also possible to import configuration files, relative to this
25+
# directory. For example, you can emulate configuration per environment
26+
# by uncommenting the line below and defining dev.exs, test.exs and such.
27+
# Configuration from the imported file will override the ones defined
28+
# here (which is why it is important to import them last).
29+
#
30+
# import_config "#{Mix.env()}.exs"

lib/telemetry_metrics_statman.ex

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
defmodule TelemetryMetricsStatman do
2+
@moduledoc """
3+
`Telemetry.Metrics` reporter that uses Statman as a middleman for metrics aggregation.
4+
5+
To start using statman reporter, start the reporter under a
6+
supervision tree with a provided list of metrics, that have to be
7+
reported:
8+
9+
import Telemetry.Metrics
10+
11+
TelemetryMetricsStatman.start_link(
12+
metrics: [
13+
counter("phoenix.endpoint.count"),
14+
summary("phoenix.endpoint.duration"),
15+
sum("customers.provisioned.count")
16+
]
17+
)
18+
19+
Telemetry metrics are mapped to the internal statman metric types as follows:
20+
21+
- `counter` -> `counter`
22+
- `sum` -> `gauge` (metric is incremented every when it's reported)
23+
- `last_value` -> `gauge`
24+
- `summary` -> `histogram`
25+
- `distribution` -> `histogram`
26+
"""
27+
28+
use GenServer
29+
30+
alias Telemetry.Metrics
31+
32+
33+
def start_link(options) do
34+
options[:metrics] ||
35+
raise ArgumentError, "the :metrics option is required by #{inspect(__MODULE__)}"
36+
37+
GenServer.start_link(__MODULE__, options)
38+
end
39+
40+
41+
@impl true
42+
def init(options) do
43+
Process.flag(:trap_exit, true)
44+
handler_ids = attach(options[:metrics])
45+
46+
{:ok, handler_ids}
47+
end
48+
49+
50+
@impl true
51+
def terminate(_, handler_ids),
52+
do: detach_handlers(handler_ids)
53+
54+
def handle_event(_event, measurements, metadata, %{metrics: metrics}) do
55+
for metric <- metrics do
56+
if value = keep?(metric, metadata) && find_measurement(metric, measurements) do
57+
key = metric_key(metric, metric.tags, metadata)
58+
report(metric, key, value)
59+
end
60+
end
61+
end
62+
63+
64+
defp report(metric, key, value) when is_float(value) do
65+
report(metric, key, round(value))
66+
end
67+
68+
defp report(%Metrics.Counter{}, key, value),
69+
do: :statman.incr(key, value)
70+
71+
defp report(%Metrics.Sum{}, key, value),
72+
do: :statman_gauge.incr(key, value)
73+
74+
defp report(%Metrics.LastValue{}, key, value),
75+
do: :statman.set_gauge(key, value)
76+
77+
defp report(%Metrics.Summary{}, key, value),
78+
do: :statman_histogram.record_value(key, :statman_histogram.bin(value))
79+
80+
defp report(%Metrics.Distribution{}, key, value),
81+
do: :statman_histogram.record_value(key, :statman_histogram.bin(value))
82+
83+
84+
defp metric_key(metric, [] = _tags, _metadata),
85+
do: metric_name(metric)
86+
87+
defp metric_key(metric, tags, metadata) do
88+
tag_values = metric.tag_values.(metadata)
89+
90+
categories =
91+
tags
92+
|> Enum.map(&Map.fetch!(tag_values, &1))
93+
|> List.to_tuple
94+
95+
{metric_name(metric), categories}
96+
end
97+
98+
99+
defp metric_name(metric) do
100+
case metric.reporter_options[:report_as] do
101+
nil ->
102+
List.to_tuple(metric.name)
103+
104+
name ->
105+
name
106+
end
107+
end
108+
109+
110+
defp keep?(%{keep: nil}, _metadata), do: true
111+
defp keep?(%{keep: keep}, metadata), do: keep.(metadata)
112+
113+
114+
defp find_measurement(%Metrics.Counter{} = metric, measurements) do
115+
case extract_measurement(metric, measurements) do
116+
nil ->
117+
1
118+
119+
value ->
120+
value
121+
end
122+
end
123+
124+
125+
defp find_measurement(metric, measurements),
126+
do: extract_measurement(metric, measurements)
127+
128+
129+
defp extract_measurement(metric, measurements) do
130+
case metric.measurement do
131+
fun when is_function(fun, 1) ->
132+
fun.(measurements)
133+
134+
key ->
135+
measurements[key]
136+
end
137+
end
138+
139+
140+
defp attach(metrics) do
141+
metrics_by_event = Enum.group_by(metrics, & &1.event_name)
142+
event_handler = &__MODULE__.handle_event/4
143+
144+
for {event_name, event_metrics} <- metrics_by_event do
145+
id = handler_id(event_name)
146+
:telemetry.attach(id, event_name, event_handler, %{metrics: event_metrics})
147+
148+
id
149+
end
150+
end
151+
152+
153+
defp detach_handlers(handler_ids) do
154+
for handler_id <- handler_ids,
155+
do: :telemetry.detach(handler_id)
156+
end
157+
158+
159+
defp handler_id(event_name), do: {__MODULE__, event_name, self()}
160+
end

mix.exs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
defmodule TelemetryMetricsStatman.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :telemetry_metrics_statman,
7+
version: "0.1.0",
8+
elixir: "~> 1.10",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger]
18+
]
19+
end
20+
21+
# Run "mix help deps" to learn about dependencies.
22+
defp deps do
23+
[
24+
{:telemetry_metrics, "~> 0.4"},
25+
{:statman, github: "GameAnalytics/statman", tag: "v0.13"},
26+
{:dialyxir, "~> 1.0", only: [:dev], runtime: false},
27+
{:credo, "~> 1.4", only: [:dev, :test], runtime: false},
28+
{:mock, "~> 0.3", only: :test},
29+
]
30+
end
31+
end

mix.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
%{
2+
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
3+
"credo": {:hex, :credo, "1.5.4", "9914180105b438e378e94a844ec3a5088ae5875626fc945b7c1462b41afc3198", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cf51af45eadc0a3f39ba13b56fdac415c91b34f7b7533a13dc13550277141bc4"},
4+
"decorators": {:hex, :decorators, "0.1.0", "1f4fd3682de23c6bce769201613812f5eaf809310a27e35ddbfd91e53b126267", [:rebar3], [{:parse_trans, "2.9.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "6a8f521232611f44ae09d0c7b40495018e2ea16b9b19a15a499f50ba347794f4"},
5+
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
6+
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
7+
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
8+
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
9+
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"},
10+
"mock": {:hex, :mock, "0.3.6", "e810a91fabc7adf63ab5fdbec5d9d3b492413b8cda5131a2a8aa34b4185eb9b4", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "bcf1d0a6826fb5aee01bae3d74474669a3fa8b2df274d094af54a25266a1ebd2"},
11+
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
12+
"statman": {:git, "https://github.com/GameAnalytics/statman.git", "0efecca9adf74fb62f04e3e671bc1f2aa38118e2", [tag: "v0.13"]},
13+
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
14+
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule TelemetryMetricsStatmanTest do
2+
use ExUnit.Case
3+
doctest TelemetryMetricsStatman
4+
import Mock
5+
6+
test "test counter" do
7+
with_mock :statman, [:passthrough], [] do
8+
# Map Telemetry event [:bar, :baz] to Statman counter "foo.bar"
9+
{:ok, pid} = TelemetryMetricsStatman.start_link(metrics: [
10+
Telemetry.Metrics.counter("foo.bar", event_name: [:bar, :baz])])
11+
:telemetry.execute([:bar, :baz], %{})
12+
# Assert that the Statman counter was actually incremented
13+
assert_called(:statman.incr({:foo, :bar}, 1))
14+
15+
Process.unlink(pid)
16+
Process.exit(pid, :kill)
17+
end
18+
end
19+
end

test/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)