Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions instrumentation/opentelemetry_tesla/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
"plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
"tesla": {:hex, :tesla, "1.17.0", "fde23e9d1a237ccdafd5b0d5193769104dad2091e400242f1048f72d7a2d0c11", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "98c85e5e614dad082789835d9561e2534c9b5aa980d45e465b898b166c2f3b21"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
"tesla": {:hex, :tesla, "1.18.0", "5f08414822f3ec94fabcb573b054ff9a8f85eb66ba2acaeb39367cb5c9b261cd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "074003577fa1a0fdafdeab889d4acf571dd44c62d40c20c5489b7ea46a8c7946"},
}
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,277 @@ defmodule Tesla.Middleware.OpenTelemetryTest do
end
end

describe "Tesla OpenAPI (1.18) integration" do
test "uses path template as span name and url.template, fully resolves url.full", ctx do
Bypass.expect_once(ctx.bypass, "GET", "/items/42;coords=blue;coords=black", fn conn ->
Plug.Conn.resp(conn, 200, "ok")
end)

client =
MyApi.new(
base_url: ctx.base_url,
opentelemetry: [opt_in_attrs: [URLAttributes.url_template()]]
)

{:ok, _env} =
MyApi.get_item(client,
path_params: %{"id" => 42, "coords" => ["blue", "black"]}
)

assert_receive {:span, span(name: "GET /items/{id}{coords}", attributes: attributes)}

mapped_attributes = :otel_attributes.map(attributes)

assert mapped_attributes[HTTPAttributes.http_request_method()] == :GET
assert mapped_attributes[HTTPAttributes.http_response_status_code()] == 200
assert mapped_attributes[URLAttributes.url_template()] == "/items/{id}{coords}"

assert mapped_attributes[URLAttributes.url_full()] ==
"http://localhost:#{ctx.bypass.port}/items/42;coords=blue;coords=black"
end

test "modern query params serialize into url.full alongside path template", ctx do
Bypass.expect_once(
ctx.bypass,
"GET",
"/items/42;coords=blue;coords=black",
fn conn ->
assert conn.query_string ==
"color=blue%7Cblack&filter%5Brole%5D=admin"

Plug.Conn.resp(conn, 200, "ok")
end
)

client = MyApi.new(base_url: ctx.base_url)

{:ok, _env} =
MyApi.get_item(client,
path_params: %{"id" => 42, "coords" => ["blue", "black"]},
query: %{
"color" => ["blue", "black"],
"filter" => [role: "admin"]
}
)

assert_receive {:span, span(name: "GET /items/{id}{coords}", attributes: attributes)}

mapped_attributes = :otel_attributes.map(attributes)

assert mapped_attributes[URLAttributes.url_full()] ==
"http://localhost:#{ctx.bypass.port}" <>
"/items/42;coords=blue;coords=black" <>
"?color=blue%7Cblack&filter%5Brole%5D=admin"
end

test "OpenAPI header and cookie params reach the server and span is recorded", ctx do
Bypass.expect_once(ctx.bypass, "GET", "/items/42;coords=red", fn conn ->
assert Plug.Conn.get_req_header(conn, "x-request-id") == ["req-123"]
assert Plug.Conn.get_req_header(conn, "cookie") == ["session_id=abc123"]
Plug.Conn.resp(conn, 200, "ok")
end)

client = MyApi.new(base_url: ctx.base_url)

{:ok, _env} =
MyApi.get_item(client,
path_params: %{"id" => 42, "coords" => ["red"]},
headers: %{"X-Request-ID" => "req-123"},
cookies: %{"session_id" => "abc123"}
)

assert_receive {:span, span(name: "GET /items/{id}{coords}", attributes: attributes)}

mapped_attributes = :otel_attributes.map(attributes)
assert mapped_attributes[HTTPAttributes.http_response_status_code()] == 200
end

test "captures request and response headers when configured", ctx do
Bypass.expect_once(ctx.bypass, "GET", "/items/42;coords=red", fn conn ->
Plug.Conn.resp(conn, 200, "ok")
end)

client =
MyApi.new(
base_url: ctx.base_url,
opentelemetry: [request_header_attrs: ["x-request-id"]]
)

{:ok, _env} =
MyApi.get_item(client,
path_params: %{"id" => 42, "coords" => ["red"]},
headers: %{"X-Request-ID" => "req-123"}
)

assert_receive {:span, span(name: "GET /items/{id}{coords}", attributes: attributes)}

mapped_attributes = :otel_attributes.map(attributes)

assert mapped_attributes[
String.to_atom("#{HTTPAttributes.http_request_header()}.x-request-id")
] == ["req-123"]
end

test "records error span when adapter cannot reach the server", ctx do
Bypass.down(ctx.bypass)

client = MyApi.new(base_url: ctx.base_url)

{:error, _reason} =
MyApi.get_item(client, path_params: %{"id" => 42, "coords" => ["blue"]})

assert_receive {:span,
span(
name: "GET /items/{id}{coords}",
status: {:status, :error, _},
attributes: attributes
)}

mapped_attributes = :otel_attributes.map(attributes)
assert mapped_attributes[ErrorAttributes.error_type()] == :econnrefused
end

test "GET routes path, query, header, and cookie params end-to-end with span attrs",
ctx do
Bypass.expect_once(
ctx.bypass,
"GET",
"/items/42;coords=blue;coords=black",
fn conn ->
assert conn.query_string ==
"color=red%7Cblue&filter%5Brole%5D=admin&tags=alpha%20beta&page=3"

assert Plug.Conn.get_req_header(conn, "x-request-id") == ["req-123"]
assert Plug.Conn.get_req_header(conn, "x-trace-tag") == ["trace-abc"]
assert Plug.Conn.get_req_header(conn, "cookie") == ["session_id=abc123; theme=dark"]

Plug.Conn.resp(conn, 200, "ok")
end
)

client =
MyApi.new(
base_url: ctx.base_url,
opentelemetry: [
opt_in_attrs: [URLAttributes.url_template()],
request_header_attrs: ["x-request-id", "x-trace-tag"]
]
)

{:ok, _env} =
MyApi.get_item(client,
path_params: %{"id" => 42, "coords" => ["blue", "black"]},
query: %{
"color" => ["red", "blue"],
"filter" => [role: "admin"],
"tags" => ["alpha", "beta"],
"page" => 3
},
headers: %{"X-Request-ID" => "req-123", "X-Trace-Tag" => "trace-abc"},
cookies: %{"session_id" => "abc123", "theme" => "dark"}
)

assert_receive {:span,
span(
name: "GET /items/{id}{coords}",
kind: :client,
attributes: attributes
)}

mapped_attributes = :otel_attributes.map(attributes)

assert mapped_attributes[HTTPAttributes.http_request_method()] == :GET
assert mapped_attributes[HTTPAttributes.http_response_status_code()] == 200
assert mapped_attributes[URLAttributes.url_template()] == "/items/{id}{coords}"

assert mapped_attributes[URLAttributes.url_full()] ==
"http://localhost:#{ctx.bypass.port}" <>
"/items/42;coords=blue;coords=black" <>
"?color=red%7Cblue&filter%5Brole%5D=admin&tags=alpha%20beta&page=3"

assert mapped_attributes[
String.to_atom("#{HTTPAttributes.http_request_header()}.x-request-id")
] == ["req-123"]

assert mapped_attributes[
String.to_atom("#{HTTPAttributes.http_request_header()}.x-trace-tag")
] == ["trace-abc"]
end

test "POST routes path, query, header, cookie params, body, and span attrs", ctx do
response_body = ~s({"id":1})

Bypass.expect_once(
ctx.bypass,
"POST",
"/tenants/acme/items",
fn conn ->
assert conn.query_string == "dry_run=true&expand=owner,tags"

assert Plug.Conn.get_req_header(conn, "x-request-id") == ["req-456"]
assert Plug.Conn.get_req_header(conn, "idempotency-key") == ["idem-789"]
assert Plug.Conn.get_req_header(conn, "cookie") == ["session_id=cookie-abc"]

{:ok, raw_body, conn} = Plug.Conn.read_body(conn)
assert raw_body == ~s({"name":"widget"})

Plug.Conn.resp(conn, 201, response_body)
end
)

client =
MyApi.new(
base_url: ctx.base_url,
opentelemetry: [
opt_in_attrs: [
URLAttributes.url_template(),
HTTPAttributes.http_response_body_size()
],
request_header_attrs: ["idempotency-key"]
]
)

{:ok, _env} =
MyApi.create_item(client, ~s({"name":"widget"}),
path_params: %{"tenant_id" => "acme"},
query: %{
"dry_run" => true,
"expand" => ["owner", "tags"]
},
headers: %{
"X-Request-ID" => "req-456",
"Idempotency-Key" => "idem-789"
},
cookies: %{"session_id" => "cookie-abc"}
)

assert_receive {:span,
span(
name: "POST /tenants/{tenant_id}/items",
kind: :client,
attributes: attributes
)}

mapped_attributes = :otel_attributes.map(attributes)

assert mapped_attributes[HTTPAttributes.http_request_method()] == :POST
assert mapped_attributes[HTTPAttributes.http_response_status_code()] == 201

assert mapped_attributes[URLAttributes.url_template()] ==
"/tenants/{tenant_id}/items"

assert mapped_attributes[URLAttributes.url_full()] ==
"http://localhost:#{ctx.bypass.port}/tenants/acme/items?dry_run=true&expand=owner,tags"

assert mapped_attributes[HTTPAttributes.http_response_body_size()] ==
byte_size(response_body)

assert mapped_attributes[
String.to_atom("#{HTTPAttributes.http_request_header()}.idempotency-key")
] == ["idem-789"]
end
end

defp client(opts \\ []) do
[{Tesla.Middleware.OpenTelemetry, opts}]
|> Tesla.client(fn env -> {:ok, env} end)
Expand Down
Loading
Loading