Guilherme de Maio 901b571b07
[opentelemetry_tesla] Conditionally override propagator (#176)
* 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>
2023-05-29 04:53:21 -06:00

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