diff --git a/instrumentation/opentelemetry_tesla/lib/middleware/opentelemetry_tesla_middleware.ex b/instrumentation/opentelemetry_tesla/lib/middleware/opentelemetry_tesla_middleware.ex index b7b33b5..473e8cf 100644 --- a/instrumentation/opentelemetry_tesla/lib/middleware/opentelemetry_tesla_middleware.ex +++ b/instrumentation/opentelemetry_tesla/lib/middleware/opentelemetry_tesla_middleware.ex @@ -1,9 +1,24 @@ 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 @behaviour Tesla.Middleware - def call(env, next, _options) do - span_name = get_span_name(env) + def call(env, next, opts) do + span_name = get_span_name(env, Keyword.get(opts, :span_name)) OpenTelemetry.Tracer.with_span span_name, %{kind: :client} do env @@ -14,7 +29,15 @@ defmodule Tesla.Middleware.OpenTelemetry do 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 nil -> "HTTP #{http_method(env.method)}" _ -> URI.parse(env.url).path diff --git a/instrumentation/opentelemetry_tesla/test/middleware/opentelemetry_tesla_middleware_test.exs b/instrumentation/opentelemetry_tesla/test/middleware/opentelemetry_tesla_middleware_test.exs index 0f7cfcc..c613ebb 100644 --- a/instrumentation/opentelemetry_tesla/test/middleware/opentelemetry_tesla_middleware_test.exs +++ b/instrumentation/opentelemetry_tesla/test/middleware/opentelemetry_tesla_middleware_test.exs @@ -25,38 +25,141 @@ defmodule Tesla.Middleware.OpenTelemetryTest do {:ok, bypass: bypass} end - test "it records a generic span name if opentelemetry middleware is configured before path params middleware", - %{ - bypass: bypass - } do - defmodule TestClient do - def get(client) do - params = [id: '3'] + describe "span name" do + test "uses generic route name when opentelemetry middleware is configured before path params middleware", + %{ + bypass: bypass + } do + defmodule TestClient do + def get(client) do + params = [id: '3'] - Tesla.get(client, "/users/:id", opts: [path_params: params]) + Tesla.get(client, "/users/:id", opts: [path_params: params]) + end + + def client(url) do + middleware = [ + {Tesla.Middleware.BaseUrl, url}, + Tesla.Middleware.OpenTelemetry, + Tesla.Middleware.PathParams + ] + + Tesla.client(middleware) + end end - def client(url) do - middleware = [ - {Tesla.Middleware.BaseUrl, url}, - Tesla.Middleware.OpenTelemetry, - Tesla.Middleware.PathParams - ] + Bypass.expect_once(bypass, "GET", "/users/3", fn conn -> + Plug.Conn.resp(conn, 204, "") + end) - Tesla.client(middleware) - end + bypass.port + |> endpoint_url() + |> TestClient.client() + |> TestClient.get() + + assert_receive {:span, span(name: "/users/:id", attributes: _attributes)} end - Bypass.expect_once(bypass, "GET", "/users/3", fn conn -> - Plug.Conn.resp(conn, 204, "") - 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 - bypass.port - |> endpoint_url() - |> TestClient.client() - |> TestClient.get() + def client(url) do + middleware = [ + {Tesla.Middleware.BaseUrl, url}, + Tesla.Middleware.OpenTelemetry + ] - assert_receive {:span, span(name: "/users/:id", attributes: _attributes)} + 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 @@ -311,9 +414,9 @@ defmodule Tesla.Middleware.OpenTelemetryTest do ] }} = Tesla.Middleware.OpenTelemetry.call( - %Tesla.Env{url: ""}, - [], - "http://example.com" + _env = %Tesla.Env{url: ""}, + _next = [], + _opts = [] ) assert is_binary(traceparent) diff --git a/instrumentation/opentelemetry_tesla/test/test_helper.exs b/instrumentation/opentelemetry_tesla/test/test_helper.exs index 869559e..6a0af57 100644 --- a/instrumentation/opentelemetry_tesla/test/test_helper.exs +++ b/instrumentation/opentelemetry_tesla/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start() +ExUnit.start(capture_log: true)