Skip to content

Commit

Permalink
Extracted from metric_api's statman_reporter.ex (#1)
Browse files Browse the repository at this point in the history
...and turned into a separate application.
  • Loading branch information
legoscia authored Jan 28, 2021
2 parents ca4f29a + 7cce153 commit 0eb7637
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 2 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
telemetry_metrics_statman-*.tar

22 changes: 22 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
language: elixir
# fossa is Ubuntu 20.04
dist: fossa
elixir: '1.10.3'
otp_release: '22.3.4'
before_install:
- git config --global '[email protected]:GameAnalytics.insteadOf' 'https://github.com/GameAnalytics'
script:
- mix test
- travis_wait mix dialyzer
- mix credo

# We want to cache Dialyzer PLTs, in order not to have to rebuild them
# every time. (Dialyzer will automatically rebuild them when needed.)
# They are in _build/dev, but since we can only cache an entire
# directory at a time, we need to remove _build/dev/{lib,rel} before
# caching.
before_cache:
- rm -rf _build/dev/lib _build/dev/rel
cache:
directories:
- _build/dev
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# telemetry_metrics_statman
Telemetry.Metrics reporter for Statman
# TelemetryMetricsStatman

`Telemetry.Metrics` reporter for Statman.

30 changes: 30 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure your application as:
#
# config :telemetry_metrics_statman, key: :value
#
# and access this configuration in your application as:
#
# Application.get_env(:telemetry_metrics_statman, :key)
#
# You can also configure a 3rd-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env()}.exs"
160 changes: 160 additions & 0 deletions lib/telemetry_metrics_statman.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
defmodule TelemetryMetricsStatman do
@moduledoc """
`Telemetry.Metrics` reporter that uses Statman as a middleman for metrics aggregation.
To start using statman reporter, start the reporter under a
supervision tree with a provided list of metrics, that have to be
reported:
import Telemetry.Metrics
TelemetryMetricsStatman.start_link(
metrics: [
counter("phoenix.endpoint.count"),
summary("phoenix.endpoint.duration"),
sum("customers.provisioned.count")
]
)
Telemetry metrics are mapped to the internal statman metric types as follows:
- `counter` -> `counter`
- `sum` -> `gauge` (metric is incremented every when it's reported)
- `last_value` -> `gauge`
- `summary` -> `histogram`
- `distribution` -> `histogram`
"""

use GenServer

alias Telemetry.Metrics


def start_link(options) do
options[:metrics] ||
raise ArgumentError, "the :metrics option is required by #{inspect(__MODULE__)}"

GenServer.start_link(__MODULE__, options)
end


@impl true
def init(options) do
Process.flag(:trap_exit, true)
handler_ids = attach(options[:metrics])

{:ok, handler_ids}
end


@impl true
def terminate(_, handler_ids),
do: detach_handlers(handler_ids)

def handle_event(_event, measurements, metadata, %{metrics: metrics}) do
for metric <- metrics do
if value = keep?(metric, metadata) && find_measurement(metric, measurements) do
key = metric_key(metric, metric.tags, metadata)
report(metric, key, value)
end
end
end


defp report(metric, key, value) when is_float(value) do
report(metric, key, round(value))
end

defp report(%Metrics.Counter{}, key, value),
do: :statman.incr(key, value)

defp report(%Metrics.Sum{}, key, value),
do: :statman_gauge.incr(key, value)

defp report(%Metrics.LastValue{}, key, value),
do: :statman.set_gauge(key, value)

defp report(%Metrics.Summary{}, key, value),
do: :statman_histogram.record_value(key, :statman_histogram.bin(value))

defp report(%Metrics.Distribution{}, key, value),
do: :statman_histogram.record_value(key, :statman_histogram.bin(value))


defp metric_key(metric, [] = _tags, _metadata),
do: metric_name(metric)

defp metric_key(metric, tags, metadata) do
tag_values = metric.tag_values.(metadata)

categories =
tags
|> Enum.map(&Map.fetch!(tag_values, &1))
|> List.to_tuple

{metric_name(metric), categories}
end


defp metric_name(metric) do
case metric.reporter_options[:report_as] do
nil ->
List.to_tuple(metric.name)

name ->
name
end
end


defp keep?(%{keep: nil}, _metadata), do: true
defp keep?(%{keep: keep}, metadata), do: keep.(metadata)


defp find_measurement(%Metrics.Counter{} = metric, measurements) do
case extract_measurement(metric, measurements) do
nil ->
1

value ->
value
end
end


defp find_measurement(metric, measurements),
do: extract_measurement(metric, measurements)


defp extract_measurement(metric, measurements) do
case metric.measurement do
fun when is_function(fun, 1) ->
fun.(measurements)

key ->
measurements[key]
end
end


defp attach(metrics) do
metrics_by_event = Enum.group_by(metrics, & &1.event_name)
event_handler = &__MODULE__.handle_event/4

for {event_name, event_metrics} <- metrics_by_event do
id = handler_id(event_name)
:telemetry.attach(id, event_name, event_handler, %{metrics: event_metrics})

id
end
end


defp detach_handlers(handler_ids) do
for handler_id <- handler_ids,
do: :telemetry.detach(handler_id)
end


defp handler_id(event_name), do: {__MODULE__, event_name, self()}
end
31 changes: 31 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule TelemetryMetricsStatman.MixProject do
use Mix.Project

def project do
[
app: :telemetry_metrics_statman,
version: "0.1.0",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:telemetry_metrics, "~> 0.4"},
{:statman, github: "GameAnalytics/statman", tag: "v0.13"},
{:dialyxir, "~> 1.0", only: [:dev], runtime: false},
{:credo, "~> 1.4", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3", only: :test},
]
end
end
15 changes: 15 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"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"},
"decorators": {:hex, :decorators, "0.1.0", "1f4fd3682de23c6bce769201613812f5eaf809310a27e35ddbfd91e53b126267", [:rebar3], [{:parse_trans, "2.9.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "6a8f521232611f44ae09d0c7b40495018e2ea16b9b19a15a499f50ba347794f4"},
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"},
"mock": {:hex, :mock, "0.3.6", "e810a91fabc7adf63ab5fdbec5d9d3b492413b8cda5131a2a8aa34b4185eb9b4", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "bcf1d0a6826fb5aee01bae3d74474669a3fa8b2df274d094af54a25266a1ebd2"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"statman": {:git, "https://github.com/GameAnalytics/statman.git", "0efecca9adf74fb62f04e3e671bc1f2aa38118e2", [tag: "v0.13"]},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
}
19 changes: 19 additions & 0 deletions test/telemetry_metrics_statman_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule TelemetryMetricsStatmanTest do
use ExUnit.Case
doctest TelemetryMetricsStatman
import Mock

test "test counter" do
with_mock :statman, [:passthrough], [] do
# Map Telemetry event [:bar, :baz] to Statman counter "foo.bar"
{:ok, pid} = TelemetryMetricsStatman.start_link(metrics: [
Telemetry.Metrics.counter("foo.bar", event_name: [:bar, :baz])])
:telemetry.execute([:bar, :baz], %{})
# Assert that the Statman counter was actually incremented
assert_called(:statman.incr({:foo, :bar}, 1))

Process.unlink(pid)
Process.exit(pid, :kill)
end
end
end
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()

0 comments on commit 0eb7637

Please sign in to comment.