901b571b07
* Optionally disable trace propagation for Tesla While we always want spans being produced, trace progation is not desirable in all cases - namely, when calling external parties, as that may leak sensitive information, like one present on Baggage. This patch introduces a new option `:propagate`, that defaults to `true`. Some tweaks are made to existing propagation test, fixing how options are used. The approach here is closer to what we see in some middleware tests of Tesla itself. * change to propagator override * change propagator to it uses global default * mix format * improve docs --------- Co-authored-by: Andrew Rosa <dev@andrewhr.io>
524 lines
13 KiB
Elixir
524 lines
13 KiB
Elixir
defmodule Tesla.Middleware.OpenTelemetryTest do
|
|
use ExUnit.Case
|
|
require Record
|
|
|
|
for {name, spec} <- Record.extract_all(from_lib: "opentelemetry/include/otel_span.hrl") do
|
|
Record.defrecord(name, spec)
|
|
end
|
|
|
|
for {name, spec} <- Record.extract_all(from_lib: "opentelemetry_api/include/opentelemetry.hrl") do
|
|
Record.defrecord(name, spec)
|
|
end
|
|
|
|
setup do
|
|
bypass = Bypass.open()
|
|
|
|
:application.stop(:opentelemetry)
|
|
:application.set_env(:opentelemetry, :tracer, :otel_tracer_default)
|
|
|
|
:application.set_env(:opentelemetry, :processors, [
|
|
{:otel_batch_processor, %{scheduled_delay_ms: 1, exporter: {:otel_exporter_pid, self()}}}
|
|
])
|
|
|
|
:application.start(:opentelemetry)
|
|
|
|
{:ok, bypass: bypass}
|
|
end
|
|
|
|
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])
|
|
end
|
|
|
|
def client(url) do
|
|
middleware = [
|
|
{Tesla.Middleware.BaseUrl, url},
|
|
Tesla.Middleware.OpenTelemetry,
|
|
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: "/users/:id", attributes: _attributes)}
|
|
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
|
|
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
|
|
|
|
@error_codes [
|
|
400,
|
|
401,
|
|
402,
|
|
403,
|
|
404,
|
|
405,
|
|
406,
|
|
407,
|
|
408,
|
|
409,
|
|
410,
|
|
411,
|
|
412,
|
|
413,
|
|
414,
|
|
415,
|
|
416,
|
|
417,
|
|
418,
|
|
500,
|
|
501,
|
|
502,
|
|
503,
|
|
504,
|
|
505,
|
|
506,
|
|
507,
|
|
508
|
|
]
|
|
|
|
for code <- @error_codes do
|
|
test "Marks Span status as :error when HTTP request fails with #{code}", %{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, unquote(code), "")
|
|
end)
|
|
|
|
bypass.port
|
|
|> endpoint_url()
|
|
|> TestClient.client()
|
|
|> TestClient.get()
|
|
|
|
assert_receive {:span, span(status: {:status, :error, ""})}
|
|
end
|
|
end
|
|
|
|
test "Marks Span status as :errors when max redirects are exceeded", %{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.Middleware.FollowRedirects, max_redirects: 1}
|
|
]
|
|
|
|
Tesla.client(middleware)
|
|
end
|
|
end
|
|
|
|
Bypass.expect(bypass, "GET", "/users", fn conn ->
|
|
conn
|
|
|> Plug.Conn.put_resp_header("Location", "/users/1")
|
|
|> Plug.Conn.resp(301, "")
|
|
end)
|
|
|
|
Bypass.expect(bypass, "GET", "/users/1", fn conn ->
|
|
conn
|
|
|> Plug.Conn.put_resp_header("Location", "/users/2")
|
|
|> Plug.Conn.resp(301, "")
|
|
end)
|
|
|
|
bypass.port
|
|
|> endpoint_url()
|
|
|> TestClient.client()
|
|
|> TestClient.get()
|
|
|
|
assert_receive {:span, span(status: {:status, :error, ""})}
|
|
end
|
|
|
|
test "Marks Span status as :error if error status is within `mark_status_ok` opt list",
|
|
%{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, mark_status_ok: [404]}
|
|
]
|
|
|
|
Tesla.client(middleware)
|
|
end
|
|
end
|
|
|
|
Bypass.expect_once(bypass, "GET", "/users", fn conn ->
|
|
Plug.Conn.resp(conn, 404, "")
|
|
end)
|
|
|
|
bypass.port
|
|
|> endpoint_url()
|
|
|> TestClient.client()
|
|
|> TestClient.get()
|
|
|
|
assert_receive {:span, span(status: {:status, :ok, ""})}
|
|
end
|
|
|
|
test "Marks Span status as :ok unless error status is within `mark_status_ok` opt list",
|
|
%{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, mark_status_ok: []}
|
|
]
|
|
|
|
Tesla.client(middleware)
|
|
end
|
|
end
|
|
|
|
Bypass.expect_once(bypass, "GET", "/users", fn conn ->
|
|
Plug.Conn.resp(conn, 404, "")
|
|
end)
|
|
|
|
bypass.port
|
|
|> endpoint_url()
|
|
|> TestClient.client()
|
|
|> TestClient.get()
|
|
|
|
assert_receive {:span, span(status: {:status, :error, ""})}
|
|
end
|
|
|
|
test "Appends query string parameters to http.url attribute", %{bypass: bypass} do
|
|
defmodule TestClient do
|
|
def get(client, id) do
|
|
params = [id: id]
|
|
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.Middleware.Query, [token: "some-token", array: ["foo", "bar"]]}
|
|
]
|
|
|
|
Tesla.client(middleware)
|
|
end
|
|
end
|
|
|
|
Bypass.expect_once(bypass, "GET", "/users/2", fn conn ->
|
|
Plug.Conn.resp(conn, 204, "")
|
|
end)
|
|
|
|
bypass.port
|
|
|> endpoint_url()
|
|
|> TestClient.client()
|
|
|> TestClient.get("2")
|
|
|
|
assert_receive {:span, span(name: _name, attributes: attributes)}
|
|
|
|
mapped_attributes = :otel_attributes.map(attributes)
|
|
|
|
assert mapped_attributes[:"http.url"] ==
|
|
"http://localhost:#{bypass.port}/users/2?token=some-token&array%5B%5D=foo&array%5B%5D=bar"
|
|
end
|
|
|
|
test "http.url attribute is correct when request doesn't contain query string parameters", %{
|
|
bypass: bypass
|
|
} do
|
|
defmodule TestClient do
|
|
def get(client, id) do
|
|
params = [id: id]
|
|
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.Middleware.Query, []}
|
|
]
|
|
|
|
Tesla.client(middleware)
|
|
end
|
|
end
|
|
|
|
Bypass.expect_once(bypass, "GET", "/users/2", fn conn ->
|
|
Plug.Conn.resp(conn, 204, "")
|
|
end)
|
|
|
|
bypass.port
|
|
|> endpoint_url()
|
|
|> TestClient.client()
|
|
|> TestClient.get("2")
|
|
|
|
assert_receive {:span, span(name: _name, attributes: attributes)}
|
|
|
|
mapped_attributes = :otel_attributes.map(attributes)
|
|
|
|
assert mapped_attributes[:"http.url"] ==
|
|
"http://localhost:#{bypass.port}/users/2"
|
|
end
|
|
|
|
test "Handles url path arguments correctly", %{bypass: bypass} do
|
|
defmodule TestClient do
|
|
def get(client, id) do
|
|
params = [id: id]
|
|
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.Middleware.Query, [token: "some-token"]}
|
|
]
|
|
|
|
Tesla.client(middleware)
|
|
end
|
|
end
|
|
|
|
Bypass.expect_once(bypass, "GET", "/users/2", fn conn ->
|
|
Plug.Conn.resp(conn, 204, "")
|
|
end)
|
|
|
|
bypass.port
|
|
|> endpoint_url()
|
|
|> TestClient.client()
|
|
|> TestClient.get("2")
|
|
|
|
assert_receive {:span, span(name: _name, attributes: attributes)}
|
|
assert %{"http.target": "/users/2"} = :otel_attributes.map(attributes)
|
|
end
|
|
|
|
test "Records http.response_content_length param into the span", %{bypass: bypass} do
|
|
defmodule TestClient do
|
|
def get(client, id) do
|
|
params = [id: id]
|
|
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.Middleware.Query, [token: "some-token"]}
|
|
]
|
|
|
|
Tesla.client(middleware)
|
|
end
|
|
end
|
|
|
|
response = "HELLO 👋"
|
|
|
|
Bypass.expect_once(bypass, "GET", "/users/2", fn conn ->
|
|
Plug.Conn.resp(conn, 200, response)
|
|
end)
|
|
|
|
bypass.port
|
|
|> endpoint_url()
|
|
|> TestClient.client()
|
|
|> TestClient.get("2")
|
|
|
|
assert_receive {:span, span(name: _name, attributes: attributes)}
|
|
|
|
mapped_attributes = :otel_attributes.map(attributes)
|
|
|
|
{response_size, _} = Integer.parse(mapped_attributes[:"http.response_content_length"])
|
|
assert response_size == byte_size(response)
|
|
end
|
|
|
|
describe "trace propagation" do
|
|
test "injects distributed tracing headers by default" do
|
|
{:ok, env} = Tesla.get(client(), "/propagate-traces")
|
|
|
|
assert traceparent = Tesla.get_header(env, "traceparent")
|
|
assert is_binary(traceparent)
|
|
|
|
assert_receive {:span, span(name: _name, attributes: attributes)}
|
|
assert %{"http.target": "/propagate-traces"} = :otel_attributes.map(attributes)
|
|
end
|
|
|
|
test "optionally disable propagation but keep span report" do
|
|
{:ok, env} = Tesla.get(client(propagator: :none), "/propagate-traces")
|
|
|
|
refute Tesla.get_header(env, "traceparent")
|
|
|
|
assert_receive {:span, span(name: _name, attributes: attributes)}
|
|
assert %{"http.target": "/propagate-traces"} = :otel_attributes.map(attributes)
|
|
end
|
|
end
|
|
|
|
defp client(opts \\ []) do
|
|
[
|
|
{Tesla.Middleware.OpenTelemetry, opts}
|
|
]
|
|
|> Tesla.client(fn env -> {:ok, env} end)
|
|
end
|
|
|
|
defp endpoint_url(port), do: "http://localhost:#{port}/"
|
|
end
|