New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Library to support Telemetry.Metrics as OpenTelemetry metrics #303
Draft
tsloughter
wants to merge
5
commits into
open-telemetry:main
Choose a base branch
from
tsloughter:telemetry-metrics
base: main
Could not load branches
Branch not found: {{ refName }}
Could not load tags
Nothing to show
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a0cf563
Otel Telemetry.Metrics integration: initial commit
tsloughter a2e40ae
move opentelemetry_telemetry_metrics to utilities and update github a…
tsloughter d16f01f
include unit in opts and add more tests
tsloughter 825d7d1
update readme
tsloughter d8e318f
Update utilities/opentelemetry_telemetry_metrics/lib/otel_telemetry_m…
tsloughter File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# 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 third-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"). | ||
opentelemetry_telemetry_metrics-*.tar | ||
|
||
# Temporary files, for example, from tests. | ||
/tmp/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# OtelTelemetryMetrics | ||
|
||
A utility library for creating OpenTelemetry metrics from metric definitions | ||
done with | ||
[Telemetry.Metrics](https://github.com/beam-telemetry/telemetry_metrics) and | ||
their corresponding telemetry events. | ||
|
||
## Installation | ||
|
||
```elixir | ||
def deps do | ||
[ | ||
{:opentelemetry_telemetry_metrics, "~> 0.1.0"} | ||
] | ||
end | ||
``` | ||
|
||
## Usage | ||
|
||
`OtelTelemetryMetrics.start_link/1` creates OpenTelemetry Instruments for | ||
`Telemetry.Metric` metrics and records to them when their corresponding events | ||
are triggered. | ||
|
||
``` elixir | ||
metrics = [ | ||
Metrics.last_value("vm.memory.binary", unit: :byte), | ||
Metrics.counter("vm.memory.total"), | ||
Metrics.counter("db.query.duration", tags: [:table, :operation]), | ||
Metrics.summary("http.request.response_time", | ||
tag_values: fn | ||
%{foo: :bar} -> %{bar: :baz} | ||
end, | ||
tags: [:bar], | ||
drop: fn metadata -> | ||
metadata[:boom] == :pow | ||
end | ||
), | ||
Metrics.sum("telemetry.event_size.metadata", | ||
unit: {:byte, :megabyte}, | ||
measurement: &__MODULE__.metadata_measurement/2 | ||
), | ||
Metrics.distribution("phoenix.endpoint.stop.duration", | ||
measurement: &__MODULE__.measurement/1 | ||
) | ||
] | ||
|
||
{:ok, _} = OtelTelemetryMetrics.start_link([metrics: metrics]) | ||
``` | ||
|
||
Then either in your Application code or a dependency execute `telemetry` events | ||
containing the measurements. For example, an event that will result in the | ||
metrics `vm.memory.total` and `vm.memory.binary` being recorded to: | ||
|
||
```elixir | ||
:telemetry.execute([:vm, :memory], %{binary: 100, total: 200}, %{}) | ||
``` | ||
|
||
OpenTelemetry does not support a `summary` type metric, the `summary` | ||
`http.request.response_time` is recorded as a single bucket histogram. | ||
|
||
In `Telemetry.Metrics` the `counter` type refers to counting the number of times | ||
an event is triggered, this is represented as a `sum` in OpenTelemetry and when | ||
recording the value is sent as a `1` every time. | ||
|
||
Metrics of type `last_value` are ignored because `last_value` is not yet an | ||
aggregation supported on synchronous instruments in Erlang/Elixir OpenTelemetry. | ||
When it is added to the SDK this library will be updated to no longer ignore | ||
metrics of this type. |
184 changes: 184 additions & 0 deletions
184
utilities/opentelemetry_telemetry_metrics/lib/otel_telemetry_metrics.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
defmodule OtelTelemetryMetrics do | ||
@moduledoc """ | ||
`OtelTelemetryMetrics.start_link/1` creates OpenTelemetry Instruments for | ||
`Telemetry.Metric` metrics and records to them when their corresponding | ||
events are triggered. | ||
|
||
metrics = [ | ||
last_value("vm.memory.binary", unit: :byte), | ||
counter("vm.memory.total"), | ||
counter("db.query.duration", tags: [:table, :operation]), | ||
summary("http.request.response_time", | ||
tag_values: fn | ||
%{foo: :bar} -> %{bar: :baz} | ||
end, | ||
tags: [:bar], | ||
drop: fn metadata -> | ||
metadata[:boom] == :pow | ||
end | ||
), | ||
sum("telemetry.event_size.metadata", | ||
measurement: &__MODULE__.metadata_measurement/2 | ||
), | ||
distribution("phoenix.endpoint.stop.duration", | ||
measurement: &__MODULE__.measurement/1 | ||
) | ||
] | ||
|
||
{:ok, _} = OtelTelemetryMetrics.start_link([metrics: metrics]) | ||
|
||
Then either in your Application code or a dependency execute `telemetry` | ||
events conataining the measurements. For example, an event that will result | ||
in the metrics `vm.memory.total` and `vm.memory.binary` being recorded to: | ||
|
||
:telemetry.execute([:vm, :memory], %{binary: 100, total: 200}, %{}) | ||
|
||
OpenTelemetry does not support a `summary` type metric, the `summary` | ||
`http.request.response_time` is recorded as a single bucket histogram. | ||
|
||
In `Telemetry.Metrics` the `counter` type refers to counting the number of | ||
times an event is triggered, this is represented as a `sum` in OpenTelemetry | ||
and when recording the value is sent as a `1` every time. | ||
|
||
Metrics of type `last_value` are ignored because `last_value` is not yet an | ||
aggregation supported on synchronous instruments in Erlang/Elixir | ||
OpenTelemetry. When it is added to the SDK this library will be updated to | ||
no longer ignore metrics of this type. | ||
""" | ||
|
||
require Logger | ||
use GenServer | ||
|
||
@doc """ | ||
""" | ||
def start_link(options) do | ||
GenServer.start_link(__MODULE__, options) | ||
end | ||
|
||
@impl true | ||
def init(options) do | ||
Process.flag(:trap_exit, true) | ||
|
||
meter = options[:meter] || :opentelemetry_experimental.get_meter() | ||
metrics = options[:metrics] || [] | ||
|
||
handler_ids = create_instruments_and_attach(meter, metrics) | ||
|
||
{:ok, %{handler_ids: handler_ids}} | ||
end | ||
|
||
@impl true | ||
def terminate(_, %{handler_ids: handler_ids}) do | ||
detach(handler_ids) | ||
:ok | ||
end | ||
|
||
defp create_instruments_and_attach(meter, metrics) do | ||
metrics_by_event = Enum.group_by(metrics, & &1.event_name) | ||
|
||
for {event_name, metrics} <- metrics_by_event do | ||
instruments = create_instruments(meter, metrics) | ||
|
||
attach(meter, event_name, instruments) | ||
end | ||
end | ||
|
||
defp create_instruments(meter, metrics) do | ||
for metric <- metrics, | ||
instrument = create_instrument(metric, meter, %{unit: unit(metric.unit)}), | ||
instrument != nil, into: %{} do | ||
{metric, instrument} | ||
end | ||
tsloughter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
|
||
defp create_instrument(%Telemetry.Metrics.Counter{}=metric, meter, opts) do | ||
:otel_counter.create(meter, format_name(metric), opts) | ||
end | ||
|
||
# a summary is represented as an explicit histogram with a single bucket | ||
defp create_instrument(%Telemetry.Metrics.Summary{}=metric, meter, opts) do | ||
:otel_histogram.create(meter, format_name(metric), Map.put(opts, :advisory_params, %{explicit_bucket_boundaries: []})) | ||
end | ||
|
||
defp create_instrument(%Telemetry.Metrics.Distribution{}=metric, meter, opts) do | ||
:otel_histogram.create(meter, format_name(metric), opts) | ||
end | ||
|
||
defp create_instrument(%Telemetry.Metrics.Sum{}=metric, meter, opts) do | ||
:otel_counter.create(meter, format_name(metric), opts) | ||
end | ||
|
||
# waiting on | ||
defp create_instrument(%Telemetry.Metrics.LastValue{}=metric, _meter, _) do | ||
Logger.info("Ignoring metric #{inspect(metric.name)} because LastValue aggregation is not supported in this version of OpenTelemetry Elixir") | ||
nil | ||
end | ||
|
||
defp unit(:unit), do: 1 | ||
defp unit(unit), do: unit | ||
|
||
defp format_name(metric) do | ||
metric.name | ||
|> Enum.join(".") | ||
|> String.to_atom | ||
end | ||
|
||
defp attach(meter, event_name, instruments) do | ||
handler_id = handler_id(event_name) | ||
|
||
:ok = | ||
:telemetry.attach(handler_id, event_name, &__MODULE__.handle_event/4, | ||
%{meter: meter, | ||
instruments: instruments}) | ||
|
||
handler_id | ||
end | ||
|
||
defp detach(handler_ids) do | ||
Enum.each(handler_ids, fn id -> :telemetry.detach(id) end) | ||
end | ||
|
||
defp handler_id(event_name) do | ||
{__MODULE__, event_name, self()} | ||
end | ||
|
||
def handle_event(_event_name, measurements, metadata, %{meter: meter, | ||
instruments: instruments}) do | ||
for {metric, instrument} <- instruments do | ||
if value = keep?(metric, metadata) && extract_measurement(metric, measurements, metadata) do | ||
ctx = OpenTelemetry.Ctx.get_current() | ||
tags = extract_tags(metric, metadata) | ||
:otel_meter.record(ctx, meter, instrument, value, tags) | ||
end | ||
end | ||
end | ||
|
||
|
||
defp keep?(%{keep: nil}, _metadata), do: true | ||
defp keep?(%{keep: keep}, metadata), do: keep.(metadata) | ||
|
||
defp extract_measurement(%Telemetry.Metrics.Counter{}, _measurements, _metadata) do | ||
1 | ||
end | ||
|
||
defp extract_measurement(metric, measurements, metadata) do | ||
case metric.measurement do | ||
nil -> | ||
nil | ||
|
||
fun when is_function(fun, 1) -> | ||
fun.(measurements) | ||
|
||
fun when is_function(fun, 2) -> | ||
fun.(measurements, metadata) | ||
|
||
key -> | ||
measurements[key] || 1 | ||
end | ||
end | ||
|
||
defp extract_tags(metric, metadata) do | ||
tag_values = metric.tag_values.(metadata) | ||
Map.take(tag_values, metric.tags) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
defmodule OtelTelemetryMetrics.MixProject do | ||
use Mix.Project | ||
|
||
def project do | ||
[ | ||
app: :opentelemetry_telemetry_metrics, | ||
version: "0.1.0", | ||
elixir: "~> 1.14", | ||
start_permanent: Mix.env() == :prod, | ||
deps: deps(), | ||
source_url: "https://github.com/open-telemetry/opentelemetry-erlang-contrib", | ||
homepage_url: "http://github.com/open-telemetry/opentelemetry-erlang-contrib", | ||
docs: [main: "OtelTelemetryMetrics", | ||
extras: ["README.md"]] | ||
] | ||
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, "~> 1.0"}, | ||
{:telemetry_metrics, "~> 0.6"}, | ||
{:opentelemetry_api, path: "/home/tristan/Devel/opentelemetry-erlang/apps/opentelemetry_api", override: true}, | ||
{:opentelemetry_api_experimental, path: "/home/tristan/Devel/opentelemetry-erlang/apps/opentelemetry_api_experimental", override: true}, | ||
{:opentelemetry, path: "/home/tristan/Devel/opentelemetry-erlang/apps/opentelemetry", override: true}, | ||
{:opentelemetry_experimental, path: "/home/tristan/Devel/opentelemetry-erlang/apps/opentelemetry_experimental"}, | ||
{:ex_doc, "~> 0.31", only: :dev, runtime: false} | ||
] | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
%{ | ||
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, | ||
"ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, | ||
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, | ||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, | ||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, | ||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, | ||
"opentelemetry_api": {:hex, :opentelemetry_api, "1.2.2", "693f47b0d8c76da2095fe858204cfd6350c27fe85d00e4b763deecc9588cf27a", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "dc77b9a00f137a858e60a852f14007bb66eda1ffbeb6c05d5fe6c9e678b05e9d"}, | ||
"opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, | ||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, | ||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The last line seems wrong? Or at least confusing. That'd make me think the
options
are ones for configuring theGenServer
, stuff likehibernate
, but its the options to be passed toinit
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you are right, sorry.