[opentelemetry-tesla] add custom span name override as middleware opt (#105)

* fix remove extra bracket in mix.exs

* use capture log for less verbose test output

* add span_name opt for overriding span name

* add moduledoc

* allow function for span_name opt
This commit is contained in:
Guilherme de Maio 2022-10-04 19:32:36 -03:00 committed by GitHub
parent 7a4c33ef7c
commit c69a3c7b49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 157 additions and 31 deletions

View File

@ -1,9 +1,24 @@
defmodule Tesla.Middleware.OpenTelemetry do defmodule Tesla.Middleware.OpenTelemetry do
@moduledoc """
Creates OpenTelemetry spans and injects tracing headers into HTTP requests
When used with `Tesla.Middleware.PathParams`, the span name will be created
based on the provided path. Without it, the span name follow OpenTelemetry
standards and use just the method name, if not being overriden by opts.
NOTE: This middleware needs to come before `Tesla.Middleware.PathParams`
## Options
- `:span_name` - override span name. Can be a `String` for a static span name,
or a function that takes the `Tesla.Env` and returns a `String`
"""
require OpenTelemetry.Tracer require OpenTelemetry.Tracer
@behaviour Tesla.Middleware @behaviour Tesla.Middleware
def call(env, next, _options) do def call(env, next, opts) do
span_name = get_span_name(env) span_name = get_span_name(env, Keyword.get(opts, :span_name))
OpenTelemetry.Tracer.with_span span_name, %{kind: :client} do OpenTelemetry.Tracer.with_span span_name, %{kind: :client} do
env env
@ -14,7 +29,15 @@ defmodule Tesla.Middleware.OpenTelemetry do
end end
end end
defp get_span_name(env) do defp get_span_name(_env, span_name) when is_binary(span_name) do
span_name
end
defp get_span_name(env, span_name_fun) when is_function(span_name_fun, 1) do
span_name_fun.(env)
end
defp get_span_name(env, _) do
case env.opts[:path_params] do case env.opts[:path_params] do
nil -> "HTTP #{http_method(env.method)}" nil -> "HTTP #{http_method(env.method)}"
_ -> URI.parse(env.url).path _ -> URI.parse(env.url).path

View File

@ -25,7 +25,8 @@ defmodule Tesla.Middleware.OpenTelemetryTest do
{:ok, bypass: bypass} {:ok, bypass: bypass}
end end
test "it records a generic span name if opentelemetry middleware is configured before path params middleware", describe "span name" do
test "uses generic route name when opentelemetry middleware is configured before path params middleware",
%{ %{
bypass: bypass bypass: bypass
} do } do
@ -59,6 +60,108 @@ defmodule Tesla.Middleware.OpenTelemetryTest do
assert_receive {:span, span(name: "/users/:id", attributes: _attributes)} assert_receive {:span, span(name: "/users/:id", attributes: _attributes)}
end end
test "uses low-cardinality method name when path params middleware is not used",
%{
bypass: bypass
} do
defmodule TestClient do
def get(client) do
Tesla.get(client, "/users/")
end
def client(url) do
middleware = [
{Tesla.Middleware.BaseUrl, url},
Tesla.Middleware.OpenTelemetry
]
Tesla.client(middleware)
end
end
Bypass.expect_once(bypass, "GET", "/users/", fn conn ->
Plug.Conn.resp(conn, 204, "")
end)
bypass.port
|> endpoint_url()
|> TestClient.client()
|> TestClient.get()
assert_receive {:span, span(name: "HTTP GET", attributes: _attributes)}
end
test "uses custom span name when passed in middleware opts",
%{
bypass: bypass
} do
defmodule TestClient do
def get(client) do
params = [id: '3']
Tesla.get(client, "/users/:id", opts: [path_params: params])
end
def client(url) do
middleware = [
{Tesla.Middleware.BaseUrl, url},
{Tesla.Middleware.OpenTelemetry, span_name: "POST :my-high-cardinality-url"},
Tesla.Middleware.PathParams
]
Tesla.client(middleware)
end
end
Bypass.expect_once(bypass, "GET", "/users/3", fn conn ->
Plug.Conn.resp(conn, 204, "")
end)
bypass.port
|> endpoint_url()
|> TestClient.client()
|> TestClient.get()
assert_receive {:span, span(name: "POST :my-high-cardinality-url", attributes: _attributes)}
end
test "uses custom span name function when passed in middleware opts",
%{
bypass: bypass
} do
defmodule TestClient do
def get(client) do
params = [id: '3']
Tesla.get(client, "/users/:id", opts: [path_params: params])
end
def client(url) do
middleware = [
{Tesla.Middleware.BaseUrl, url},
{Tesla.Middleware.OpenTelemetry, span_name: fn env ->
"#{String.upcase(to_string(env.method))} potato"
end},
Tesla.Middleware.PathParams
]
Tesla.client(middleware)
end
end
Bypass.expect_once(bypass, "GET", "/users/3", fn conn ->
Plug.Conn.resp(conn, 204, "")
end)
bypass.port
|> endpoint_url()
|> TestClient.client()
|> TestClient.get()
assert_receive {:span, span(name: "GET potato", attributes: _attributes)}
end
end
test "Records spans for Tesla HTTP client", %{bypass: bypass} do test "Records spans for Tesla HTTP client", %{bypass: bypass} do
defmodule TestClient do defmodule TestClient do
def get(client) do def get(client) do
@ -311,9 +414,9 @@ defmodule Tesla.Middleware.OpenTelemetryTest do
] ]
}} = }} =
Tesla.Middleware.OpenTelemetry.call( Tesla.Middleware.OpenTelemetry.call(
%Tesla.Env{url: ""}, _env = %Tesla.Env{url: ""},
[], _next = [],
"http://example.com" _opts = []
) )
assert is_binary(traceparent) assert is_binary(traceparent)

View File

@ -1 +1 @@
ExUnit.start() ExUnit.start(capture_log: true)