opentelemetry-erlang-contrib/instrumentation/opentelemetry_tesla/test/middleware/opentelemetry_tesla_middlew...

524 lines
13 KiB
Elixir
Raw Permalink Normal View History

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
Improve test matrix and add support for Elixir 1.15 and OTP 26 (#188) * Use test matrix from file * Only check formatting on specific Elixir version * Use latest patch version of each Elixir/OTP release in test matrix * Test on Elixir 1.15 and OTP 26 * Run formatter on opentelemetry_httpoison * Run formatter on opentelemetry_phoenix * Run formatter on opentelemetry_tesla * Fix building opentelemetry_ecto on Elixir 1.15 Upgraded deps to fix ssl_verify_fun not compiling * Fix building opentelemetry_dataloader on Elixir 1.15 Upgraded deps to fix ssl_verify_fun and ecto_sql not compiling * Upgrade opentelemetry_finch to build on Elixir 1.15 * Upgrade opentelemetry_httpoison deps to build on 1.15 * Upgrade opentelemetry_nebulex to build on Elixir 1.15 * Upgrade opentelemetry_oban to build on Elixir 1.15 * Upgrade opentelemetry_phoenix deps to build on 1.15 * Upgrade opentelemetry_redix deps to build on 1.15 * Fix warning about <> being ambiguous * Fix assertion on attributes keys These are always atoms, not strings. * Upgrade ssl_verify_fun in opentelemetry_telemetry * Deterministically sort keys before asserting in tests * Upgrade opentelemetry_process_propogator to build on Elixir 1.15 * Run mix format on opentelemetry_process_propogator * Assert keys are atoms, not strings * Use matrix.os to define runs-on parameter * Pin test matrix to specific OTP + Elixir versions * Run formatter on telemetry and process_propagator * Run formatter over opentelemetry_phoenix --------- Co-authored-by: Tristan Sloughter <t@crashfast.com>
2023-08-25 20:11:23 +00:00
params = [id: ~c"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
Improve test matrix and add support for Elixir 1.15 and OTP 26 (#188) * Use test matrix from file * Only check formatting on specific Elixir version * Use latest patch version of each Elixir/OTP release in test matrix * Test on Elixir 1.15 and OTP 26 * Run formatter on opentelemetry_httpoison * Run formatter on opentelemetry_phoenix * Run formatter on opentelemetry_tesla * Fix building opentelemetry_ecto on Elixir 1.15 Upgraded deps to fix ssl_verify_fun not compiling * Fix building opentelemetry_dataloader on Elixir 1.15 Upgraded deps to fix ssl_verify_fun and ecto_sql not compiling * Upgrade opentelemetry_finch to build on Elixir 1.15 * Upgrade opentelemetry_httpoison deps to build on 1.15 * Upgrade opentelemetry_nebulex to build on Elixir 1.15 * Upgrade opentelemetry_oban to build on Elixir 1.15 * Upgrade opentelemetry_phoenix deps to build on 1.15 * Upgrade opentelemetry_redix deps to build on 1.15 * Fix warning about <> being ambiguous * Fix assertion on attributes keys These are always atoms, not strings. * Upgrade ssl_verify_fun in opentelemetry_telemetry * Deterministically sort keys before asserting in tests * Upgrade opentelemetry_process_propogator to build on Elixir 1.15 * Run mix format on opentelemetry_process_propogator * Assert keys are atoms, not strings * Use matrix.os to define runs-on parameter * Pin test matrix to specific OTP + Elixir versions * Run formatter on telemetry and process_propagator * Run formatter over opentelemetry_phoenix --------- Co-authored-by: Tristan Sloughter <t@crashfast.com>
2023-08-25 20:11:23 +00:00
params = [id: ~c"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
Improve test matrix and add support for Elixir 1.15 and OTP 26 (#188) * Use test matrix from file * Only check formatting on specific Elixir version * Use latest patch version of each Elixir/OTP release in test matrix * Test on Elixir 1.15 and OTP 26 * Run formatter on opentelemetry_httpoison * Run formatter on opentelemetry_phoenix * Run formatter on opentelemetry_tesla * Fix building opentelemetry_ecto on Elixir 1.15 Upgraded deps to fix ssl_verify_fun not compiling * Fix building opentelemetry_dataloader on Elixir 1.15 Upgraded deps to fix ssl_verify_fun and ecto_sql not compiling * Upgrade opentelemetry_finch to build on Elixir 1.15 * Upgrade opentelemetry_httpoison deps to build on 1.15 * Upgrade opentelemetry_nebulex to build on Elixir 1.15 * Upgrade opentelemetry_oban to build on Elixir 1.15 * Upgrade opentelemetry_phoenix deps to build on 1.15 * Upgrade opentelemetry_redix deps to build on 1.15 * Fix warning about <> being ambiguous * Fix assertion on attributes keys These are always atoms, not strings. * Upgrade ssl_verify_fun in opentelemetry_telemetry * Deterministically sort keys before asserting in tests * Upgrade opentelemetry_process_propogator to build on Elixir 1.15 * Run mix format on opentelemetry_process_propogator * Assert keys are atoms, not strings * Use matrix.os to define runs-on parameter * Pin test matrix to specific OTP + Elixir versions * Run formatter on telemetry and process_propagator * Run formatter over opentelemetry_phoenix --------- Co-authored-by: Tristan Sloughter <t@crashfast.com>
2023-08-25 20:11:23 +00:00
params = [id: ~c"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