From 3f43ca222bd321a0ba106a43110dc26f28ddd18e Mon Sep 17 00:00:00 2001 From: Bryan Naegele Date: Wed, 16 Oct 2024 17:31:56 -0600 Subject: [PATCH] Req 1.27 SemConv (#375) * Req 1.26 SemConv * bump semconv * Format * otel http * sem conv in main * update deps * Update deps * Update instrumentation/opentelemetry_req/lib/opentelemetry_req.ex Co-authored-by: Greg Mefford * Update instrumentation/opentelemetry_req/lib/opentelemetry_req.ex Co-authored-by: Greg Mefford * Guard against multiple headers * Bump otel * moar pipes * Update docs around path params * Remove unused opt * Align opt-in attrs with other HTTP libs * Make RC and update changelog * Dialyzer and fixes --------- Co-authored-by: Greg Mefford --- .../opentelemetry_req/CHANGELOG.md | 27 +- instrumentation/opentelemetry_req/README.md | 2 +- .../lib/opentelemetry_req.ex | 401 +++++++++++++----- instrumentation/opentelemetry_req/mix.exs | 16 +- instrumentation/opentelemetry_req/mix.lock | 51 ++- .../test/opentelemetry_req_test.exs | 383 +++++++++++++++-- 6 files changed, 724 insertions(+), 156 deletions(-) diff --git a/instrumentation/opentelemetry_req/CHANGELOG.md b/instrumentation/opentelemetry_req/CHANGELOG.md index 4c91e678..adced421 100644 --- a/instrumentation/opentelemetry_req/CHANGELOG.md +++ b/instrumentation/opentelemetry_req/CHANGELOG.md @@ -1,32 +1,43 @@ # Changelog +### 1.0.0-rc.1 + +### Features + +- OpenTelemetry v1.27 support + +### Breaking Changes + +- Various HTTP Semantic Convention changes are included. One major change + regards span naming. This may affect your observability tools when keying + on span names. The key change there is the HTTP method is now a prefix, e.g. "GET /users/:user_id" + ## 0.2.0 ### Fixes -* Add support for Req v0.4 +- Add support for Req v0.4 -* Change http.url to follow [OpenTelemetry http spec](https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#http-client). +- Change http.url to follow [OpenTelemetry http spec](https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#http-client). -* Full HTTP request URL in the form `scheme://host[:port]/path?query[#fragment]` +- Full HTTP request URL in the form `scheme://host[:port]/path?query[#fragment]` -* Strip user credentials passed via URL +- Strip user credentials passed via URL ## 0.1.2 ### Fixes -* Fix ctx not being set back to parent upon completion +- Fix ctx not being set back to parent upon completion ## 0.1.1 ### Fixes -* Fix client span to be the ctx injected to headers +- Fix client span to be the ctx injected to headers ## 0.1.0 ### Features -* Initial release - +- Initial release diff --git a/instrumentation/opentelemetry_req/README.md b/instrumentation/opentelemetry_req/README.md index e9912bec..f5956203 100644 --- a/instrumentation/opentelemetry_req/README.md +++ b/instrumentation/opentelemetry_req/README.md @@ -12,7 +12,7 @@ See [Docs](https://hex.pm/packages/opentelemetry_req) for usage instructions. ```elixir def deps do [ - {:opentelemetry_req, "~> 0.2.0"} + {:opentelemetry_req, "~> 1.0.0-beta.1"} ] end ``` diff --git a/instrumentation/opentelemetry_req/lib/opentelemetry_req.ex b/instrumentation/opentelemetry_req/lib/opentelemetry_req.ex index f7328520..6b179c45 100644 --- a/instrumentation/opentelemetry_req/lib/opentelemetry_req.ex +++ b/instrumentation/opentelemetry_req/lib/opentelemetry_req.ex @@ -1,26 +1,109 @@ defmodule OpentelemetryReq do + alias OpenTelemetry.Ctx + + alias OpenTelemetry.SemConv.ErrorAttributes + alias OpenTelemetry.SemConv.NetworkAttributes + alias OpenTelemetry.SemConv.ServerAttributes + alias OpenTelemetry.SemConv.UserAgentAttributes + alias OpenTelemetry.SemConv.Incubating.HTTPAttributes + alias OpenTelemetry.SemConv.Incubating.URLAttributes + + alias OpenTelemetry.Tracer + alias OpenTelemetry.SemanticConventions.Trace + require Trace + require Tracer + require Logger + + opt_ins = [ + HTTPAttributes.http_request_body_size(), + HTTPAttributes.http_response_body_size(), + NetworkAttributes.network_transport(), + URLAttributes.url_scheme(), + URLAttributes.url_template(), + UserAgentAttributes.user_agent_original() + ] + + @options_schema NimbleOptions.new!( + opt_in_attrs: [ + type: {:list, {:in, opt_ins}}, + default: [], + type_spec: quote(do: opt_in_attrs()), + doc: """ + Opt-in and experimental attributes. Use semantic conventions library to ensure compatability, e.g. `[{HTTPAttributes.http_request_body_size(), true}]` + + #{Enum.map_join(opt_ins, "\n\n", &" * `#{inspect(&1)}`")} + """ + ], + propagate_trace_headers: [ + type: :boolean, + default: false, + doc: "Trace headers will be propagated" + ], + request_header_attrs: [ + type: {:list, :string}, + default: [], + doc: "List of request headers to add as attributes. (lowercase)" + ], + response_header_attrs: [ + type: {:list, :string}, + default: [], + doc: "List of response headers to add as attributes. (lowercase)" + ], + span_name: [ + type: {:or, [:atom, nil, :string]}, + default: nil, + doc: "User defined span name override" + ] + ) + + @typedoc "Use semantic conventions library to ensure compatability, e.g. `HTTPAttributes.http_request_body_size()`" + @type opt_in_attr() :: + unquote(HTTPAttributes.http_request_body_size()) + | unquote(HTTPAttributes.http_response_body_size()) + | unquote(NetworkAttributes.network_transport()) + | unquote(URLAttributes.url_scheme()) + | unquote(URLAttributes.url_template()) + | unquote(UserAgentAttributes.user_agent_original()) + + @type opt_in_attrs() :: [opt_in_attr()] + + @type options() :: [unquote(NimbleOptions.option_typespec(@options_schema))] + @moduledoc """ - Wraps the request in an opentelemetry span. Span names must be parameterized, so the - `req_path_params` module and step should be registered before this step. This step is - expected by default and an error will be raised if the path params option is - not set for the request. + Wraps a Req request in an opentelemetry span. Spans are not created until the request is completed or errored. - ## Request Options + ## Req Path Params + + It is strongly encouraged to use the [`put_path_params` step](https://hexdocs.pm/req/Req.Steps.html#put_path_params/1) option. + This allows the span name to include the `{target}` portion of the span name described in the + [HTTP Span Name guidelines](https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name). - * `:span_name` - `String.t()` if provided, overrides the span name. Defaults to `nil`. - * `:no_path_params` - `boolean()` when set to `true` no path params are expected for the request. Defaults to `false` - * `:propagate_trace_ctx` - `boolean()` when set to `true`, trace headers will be propagated. Defaults to `false` + > #### Requirements {: .info} + > + > * `path_params` option should be set along with a templated path. Only `:colon` style is supported + > * `URLAttributes.url_template()` opt-in attribute must be set to `true` - ### Example with path_params + ## Semantic Conventions + All available required and recommended [Client HTTP Span](https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client) semantic conventions are implemented. + Supported opt-in and experimental attributes can be configured using the `opt_in_attrs` option. + + ## Options + + ### Opt-in Semantic Convention Attributes + + Otel SemConv requires users to explicitly opt in for any attribute with a + requirement level of `opt-in` or `experimental`. To ensure compatability, always use the + SemConv attribute. + + Example: ``` client = Req.new() |> OpentelemetryReq.attach( - base_url: "http://localhost:4000", - propagate_trace_ctx: true + opt_in_attrs: [SemConv.URLAttributes.url_template()] ) client @@ -30,39 +113,76 @@ defmodule OpentelemetryReq do ) ``` - ### Example without path_params + Request and response header attributes are opt-in and can be set with the + `request_header_attrs` and `response_header_attrs` options. Values should be lower-case. + + ### Trace Header Propagation + By default, trace propagation headers are not injected to requests. There are + two options available to propagate trace headers: + + * set `propagate_trace_headers` option to `true` when attaching or in the call + * manually use `:otel_propagator_text_map.inject/1` + + Example: ``` client = Req.new() - |> OpentelemetryReq.attach( - base_url: "http://localhost:4000", - propagate_trace_ctx: true, - no_path_params: true - ) + |> OpentelemetryReq.attach(propagate_trace_headers: true) - client - |> Req.get( - url: "/api/users" - ) + # or + + client = + Req.new() + |> OpentelemetryReq.attach() + + Req.get(client, "/", propagate_trace_headers: true) ``` - If you don't set `path_params` the request will raise. - """ - alias OpenTelemetry.Tracer - alias OpenTelemetry.SemanticConventions.Trace - require Trace - require Tracer - require Logger + ### Span Name Override + + The span name can be overridden by setting the `span_name` option in the call. + + Example: + ``` + client = + Req.new() + |> OpentelemetryReq.attach() + + Req.get(client, "/", span_name: "custom") + ``` + > #### Option Precedence {: .info} + > + > Options passed in a request take precedence over those passed in `attach/1`. + > + """ + + @spec attach(Req.Request.t(), options()) :: Req.Request.t() def attach(%Req.Request{} = request, options \\ []) do + config = + options + |> NimbleOptions.validate!(@options_schema) + |> Enum.into(%{}) + |> then(fn config -> + if Enum.member?(config.opt_in_attrs, URLAttributes.url_template()) do + Map.put(config, :url_template_enabled, true) + else + Map.put(config, :url_template_enabled, false) + end + end) + request - |> Req.Request.register_options([:span_name, :no_path_params, :propagate_trace_ctx]) - |> Req.Request.merge_options(options) + |> Req.Request.put_private(:otel, config) + |> Req.Request.register_options([ + :propagate_trace_headers, + :request_header_attrs, + :response_header_attrs, + :span_name + ]) |> Req.Request.append_request_steps( - require_path_params: &require_path_params_option/1, start_span: &start_span/1, - put_trace_headers: &maybe_put_trace_headers/1 + put_trace_headers: &propagate_trace_headers/1 ) |> Req.Request.prepend_response_steps(otel_end_span: &end_span/1) |> Req.Request.prepend_error_steps(otel_end_span: &end_errored_span/1) @@ -86,14 +206,31 @@ defmodule OpentelemetryReq do end defp end_span({request, %Req.Response{} = response}) do - attrs = - Map.put(%{}, Trace.http_status_code(), response.status) - |> maybe_append_resp_content_length(response) + config = Req.Request.get_private(request, :otel) - Tracer.set_attributes(attrs) + opt_in = + %{ + HTTPAttributes.http_response_body_size() => extract_response_body_size(response) + } + |> Map.take(config.opt_in_attrs) if response.status >= 400 do - OpenTelemetry.Tracer.set_status(OpenTelemetry.status(:error, "")) + Tracer.set_status(OpenTelemetry.status(:error, "")) + + %{ + HTTPAttributes.http_response_status_code() => response.status, + ErrorAttributes.error_type() => to_string(response.status) + } + |> set_resp_header_attrs(response, request.options) + |> Map.merge(opt_in) + |> Tracer.set_attributes() + else + %{ + HTTPAttributes.http_response_status_code() => response.status + } + |> set_resp_header_attrs(response, request.options) + |> Map.merge(opt_in) + |> Tracer.set_attributes() end OpenTelemetry.Tracer.end_span() @@ -105,12 +242,16 @@ defmodule OpentelemetryReq do end defp end_errored_span({request, exception}) do - OpenTelemetry.Tracer.set_status(OpenTelemetry.status(:error, format_exception(exception))) + Tracer.set_status(OpenTelemetry.status(:error, format_exception(exception))) - OpenTelemetry.Tracer.end_span() + Tracer.set_attributes(%{ + ErrorAttributes.error_type() => exception.__struct__ + }) + + Tracer.end_span() Process.delete(:otel_parent_ctx) - |> OpenTelemetry.Ctx.attach() + |> Ctx.attach() {request, exception} end @@ -124,11 +265,21 @@ defmodule OpentelemetryReq do defp span_name(request) do case request.options[:span_name] do nil -> - method = http_method(request.method) - - case Req.Request.get_private(request, :path_params_template) do - nil -> "HTTP #{method}" - params_template -> "#{params_template}" + config = Req.Request.get_private(request, :otel) + + if config.span_name do + config.span_name + else + method = parse_method(request.method) + + if config.url_template_enabled do + case Req.Request.get_private(request, :path_params_template) do + nil -> method + params_template -> "#{method} #{params_template}" + end + else + method + end end span_name -> @@ -139,16 +290,29 @@ defmodule OpentelemetryReq do defp build_req_attrs(request) do uri = request.url url = sanitize_url(uri) + config = Req.Request.get_private(request, :otel) + + opt_in = %{ + HTTPAttributes.http_request_body_size() => extract_request_body_size(request), + NetworkAttributes.network_transport() => :tcp, + URLAttributes.url_scheme() => extract_scheme(uri), + URLAttributes.url_template() => extract_url_template(request), + UserAgentAttributes.user_agent_original() => extract_user_agent(request) + } %{ - Trace.http_method() => http_method(request.method), - Trace.http_url() => url, - Trace.http_target() => uri.path, - Trace.net_host_name() => uri.host, - Trace.http_scheme() => uri.scheme + HTTPAttributes.http_request_method() => parse_method(request.method), + ServerAttributes.server_address() => uri.host, + ServerAttributes.server_port() => extract_port(uri), + URLAttributes.url_full() => url } - |> maybe_append_req_content_length(request) - |> maybe_append_retry_count(request) + |> set_retry_count(request) + |> set_req_header_attrs(request) + |> then(fn attrs -> + opt_in + |> Map.take(config.opt_in_attrs) + |> Map.merge(attrs) + end) end defp sanitize_url(uri) do @@ -156,81 +320,124 @@ defmodule OpentelemetryReq do |> URI.to_string() end - defp maybe_append_req_content_length(attrs, req) do - case Req.Request.get_header(req, "content-length") do + defp extract_port(%{port: port}) when is_integer(port), do: port + + defp extract_port(%{scheme: scheme}) do + case scheme do + nil -> 80 + "http" -> 80 + "https" -> 443 + _ -> 80 + end + end + + defp extract_scheme(%{scheme: scheme}) do + case scheme do + nil -> :http + "http" -> :http + "https" -> :https + _ -> :http + end + end + + defp extract_url_template(request) do + Req.Request.get_private(request, :path_params_template, "") + end + + defp extract_user_agent(request) do + case Req.Request.get_header(request, "user-agent") do [] -> - attrs + "" - [length] -> - Map.put(attrs, Trace.http_request_content_length(), length) + [user_agent | _] -> + user_agent end end - defp maybe_append_resp_content_length(attrs, req) do + defp extract_response_body_size(req) do case Req.Response.get_header(req, "content-length") do [] -> - attrs + 0 + + [length_str | _] when is_binary(length_str) -> + # Req sets this as a string but should be an integer + # https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length + # https://opentelemetry.io/docs/specs/semconv/attributes-registry/http + String.to_integer(length_str) + end + end + + defp extract_request_body_size(req) do + case Req.Request.get_header(req, "content-length") do + [] -> + 0 - [length] -> - Map.put(attrs, Trace.http_response_content_length(), length) + [length_str | _] when is_binary(length_str) -> + # Req sets this as a string but should be an integer + # https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length + # https://opentelemetry.io/docs/specs/semconv/attributes-registry/http + String.to_integer(length_str) end end - defp maybe_append_retry_count(attrs, req) do + defp set_retry_count(attrs, req) do retry_count = Req.Request.get_private(req, :req_retry_count, 0) if retry_count > 0 do - Map.put(attrs, Trace.http_retry_count(), retry_count) + Map.put(attrs, HTTPAttributes.http_request_resend_count(), retry_count) else attrs end end - defp http_method(method) do + defp parse_method(method) do case method do - :get -> :GET - :head -> :HEAD - :post -> :POST - :patch -> :PATCH - :put -> :PUT - :delete -> :DELETE - :connect -> :CONNECT - :options -> :OPTIONS - :trace -> :TRACE + :connect -> HTTPAttributes.http_request_method_values().connect + :delete -> HTTPAttributes.http_request_method_values().delete + :get -> HTTPAttributes.http_request_method_values().get + :head -> HTTPAttributes.http_request_method_values().head + :options -> HTTPAttributes.http_request_method_values().options + :patch -> HTTPAttributes.http_request_method_values().patch + :post -> HTTPAttributes.http_request_method_values().post + :put -> HTTPAttributes.http_request_method_values().put + :trace -> HTTPAttributes.http_request_method_values().trace end end - defp maybe_put_trace_headers(request) do - if request.options[:propagate_trace_ctx] do - propagator = :opentelemetry.get_text_map_injector() - headers_to_inject = :otel_propagator_text_map.inject(propagator, [], &[{&1, &2} | &3]) + defp propagate_trace_headers(request) do + should_inject = + Req.Request.get_option( + request, + :propagate_trace_headers, + Req.Request.get_private(request, :otel)[:propagate_trace_headers] + ) - Enum.reduce(headers_to_inject, request, fn {name, value}, acc -> - Req.Request.put_header(acc, name, value) - end) + if should_inject do + Req.Request.put_headers(request, :otel_propagator_text_map.inject([])) else request end end - defp require_path_params_option(request) do - if !request.options[:no_path_params] and !request.options[:path_params] do - {Req.Request.halt(request), __MODULE__.PathParamsOptionError.new()} - else - request - end + defp set_req_header_attrs(attrs, req) do + Map.merge( + attrs, + :otel_http.extract_headers_attributes( + :request, + req.headers, + Map.get(req.options, :request_header_attrs, []) + ) + ) end - defmodule PathParamsOptionError do - defexception [:message] - - def new do - %__MODULE__{} - end - - @impl true - def message(_) do - ":path_params option must be set" - end + defp set_resp_header_attrs(attrs, resp, options) do + Map.merge( + attrs, + :otel_http.extract_headers_attributes( + :response, + resp.headers, + Map.get(options, :response_header_attrs, []) + ) + ) end end diff --git a/instrumentation/opentelemetry_req/mix.exs b/instrumentation/opentelemetry_req/mix.exs index 779d70d9..2a1bafa7 100644 --- a/instrumentation/opentelemetry_req/mix.exs +++ b/instrumentation/opentelemetry_req/mix.exs @@ -1,14 +1,14 @@ defmodule OpentelemetryReq.MixProject do use Mix.Project - @version "0.2.0" + @version "1.0.0-rc.1" def project do [ app: :opentelemetry_req, description: description(), version: @version, - elixir: "~> 1.11", + elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps(), name: "Opentelemetry Req", @@ -59,11 +59,17 @@ defmodule OpentelemetryReq.MixProject do defp deps do [ {:jason, "~> 1.3"}, - {:opentelemetry_api, "~> 1.0"}, - {:opentelemetry_semantic_conventions, "~> 0.2"}, + {:nimble_options, "~> 1.1"}, + {:opentelemetry_api, "~> 1.4"}, + {:opentelemetry_semantic_conventions, "~> 1.27"}, + {:otel_http, "~> 0.2"}, {:req, ">= 0.3.5"}, {:ex_doc, "~> 0.34", only: [:dev, :test]}, - {:opentelemetry, "~> 1.0", only: :test} + {:opentelemetry_exporter, "~> 1.8", only: [:test]}, + {:opentelemetry, "~> 1.5", only: :test}, + {:bypass, "~> 2.1", only: :test}, + {:plug, ">= 1.15.0", only: [:test]}, + {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false} ] end end diff --git a/instrumentation/opentelemetry_req/mix.lock b/instrumentation/opentelemetry_req/mix.lock index d1c220c2..975f9411 100644 --- a/instrumentation/opentelemetry_req/mix.lock +++ b/instrumentation/opentelemetry_req/mix.lock @@ -1,21 +1,40 @@ %{ - "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, - "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, - "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, - "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"}, + "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, + "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, + "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, + "gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"}, + "grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"}, + "hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"}, + "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, - "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, - "opentelemetry": {:hex, :opentelemetry, "1.3.1", "f0a342a74379e3540a634e7047967733da4bc8b873ec9026e224b2bd7369b1fc", [:rebar3], [{:opentelemetry_api, "~> 1.2.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "de476b2ac4faad3e3fe3d6e18b35dec9cb338c3b9910c2ce9317836dacad3483"}, - "opentelemetry_api": {:hex, :opentelemetry_api, "1.2.2", "693f47b0d8c76da2095fe858204cfd6350c27fe85d00e4b763deecc9588cf27a", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "dc77b9a00f137a858e60a852f14007bb66eda1ffbeb6c05d5fe6c9e678b05e9d"}, - "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, - "req": {:hex, :req, "0.4.8", "2b754a3925ddbf4ad78c56f30208ced6aefe111a7ea07fb56c23dccc13eb87ae", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7146e51d52593bb7f20d00b5308a5d7d17d663d6e85cd071452b613a8277100c"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "opentelemetry": {:hex, :opentelemetry, "1.5.0", "7dda6551edfc3050ea4b0b40c0d2570423d6372b97e9c60793263ef62c53c3c2", [:rebar3], [{:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "cdf4f51d17b592fc592b9a75f86a6f808c23044ba7cf7b9534debbcc5c23b0ee"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.4.0", "63ca1742f92f00059298f478048dfb826f4b20d49534493d6919a0db39b6db04", [:mix, :rebar3], [], "hexpm", "3dfbbfaa2c2ed3121c5c483162836c4f9027def469c41578af5ef32589fcfc58"}, + "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.8.0", "5d546123230771ef4174e37bedfd77e3374913304cd6ea3ca82a2add49cd5d56", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.5.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.4.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "a1f9f271f8d3b02b81462a6bfef7075fd8457fdb06adff5d2537df5e2264d9af"}, + "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "1.27.0", "acd0194a94a1e57d63da982ee9f4a9f88834ae0b31b0bd850815fe9be4bbb45f", [:mix, :rebar3], [], "hexpm", "9681ccaa24fd3d810b4461581717661fd85ff7019b082c2dff89c7d5b1fc2864"}, + "otel_http": {:hex, :otel_http, "0.2.0", "b17385986c7f1b862f5d577f72614ecaa29de40392b7618869999326b9a61d8a", [:rebar3], [], "hexpm", "f2beadf922c8cfeb0965488dd736c95cc6ea8b9efce89466b3904d317d7cc717"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [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", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "tls_certificate_check": {:hex, :tls_certificate_check, "1.24.0", "d00e2887551ff8cdae4d0340d90d9fcbc4943c7b5f49d32ed4bc23aff4db9a44", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "90b25a58ee433d91c17f036d4d354bf8859a089bfda60e68a86f8eecae45ef1b"}, } diff --git a/instrumentation/opentelemetry_req/test/opentelemetry_req_test.exs b/instrumentation/opentelemetry_req/test/opentelemetry_req_test.exs index 0ba8b0e6..6ce7c8f3 100644 --- a/instrumentation/opentelemetry_req/test/opentelemetry_req_test.exs +++ b/instrumentation/opentelemetry_req/test/opentelemetry_req_test.exs @@ -1,6 +1,14 @@ defmodule OpentelemetryReqTest do use ExUnit.Case, async: true doctest OpentelemetryReq + + alias OpenTelemetry.SemConv.ErrorAttributes + alias OpenTelemetry.SemConv.NetworkAttributes + alias OpenTelemetry.SemConv.ServerAttributes + alias OpenTelemetry.SemConv.UserAgentAttributes + alias OpenTelemetry.SemConv.Incubating.HTTPAttributes + alias OpenTelemetry.SemConv.Incubating.URLAttributes + require Record for {name, spec} <- Record.extract_all(from_lib: "opentelemetry/include/otel_span.hrl") do @@ -17,48 +25,365 @@ defmodule OpentelemetryReqTest do :application.start(:opentelemetry) - req = - Req.new() - |> OpentelemetryReq.attach() + bypass = Bypass.open() + {:ok, bypass: bypass} + end + + defp client(opts \\ []) do + Req.new() + |> OpentelemetryReq.attach(opts) + end + + test "basic request" do + plug = fn conn -> + conn + |> Plug.Conn.put_status(200) + |> Req.Test.json(%{id: 3}) + end + + Req.get!(client(), + plug: plug, + url: "http://localtest:8080/users/:id", + path_params: [id: 3], + params: [a: "b"] + ) + + assert_receive {:span, + span( + name: :GET, + kind: :client, + attributes: span_attrs + )} + + attrs = :otel_attributes.map(span_attrs) + + expected_attrs = [ + {HTTPAttributes.http_request_method(), :GET}, + {HTTPAttributes.http_response_status_code(), 200}, + {ServerAttributes.server_address(), "localtest"}, + {ServerAttributes.server_port(), 8080}, + {URLAttributes.url_full(), "http://localtest:8080/users/3?a=b"} + ] + + for {attr, expected} <- expected_attrs do + actual = Map.get(attrs, attr) + assert expected == actual, "#{attr} expected #{expected} got #{actual}" + end + end + + # with all opt-ins + test "with all other opt-ins and header options", %{bypass: bypass} do + Bypass.expect_once(bypass, "POST", "/users/3", fn conn -> + conn + |> Plug.Conn.put_status(200) + |> Req.Test.json(%{user_id: 3}) + end) + + fields = [a: 1, b: {"2", filename: "b.txt"}] + + client = + client(%{ + opt_in_attrs: [ + HTTPAttributes.http_request_body_size(), + HTTPAttributes.http_response_body_size(), + NetworkAttributes.network_transport(), + URLAttributes.url_scheme(), + URLAttributes.url_template(), + UserAgentAttributes.user_agent_original() + ] + }) + + Req.post!(client, + url: "http://localhost:#{bypass.port}/users/:user_id", + path_params: [user_id: 3], + params: [a: "b"], + headers: %{test_header: "request header"}, + form_multipart: fields, + request_header_attrs: ["test-header", "user-agent"], + response_header_attrs: ["content-type"] + ) + + assert_receive {:span, + span( + name: "POST /users/:user_id", + kind: :client, + attributes: span_attrs + )} + + attrs = :otel_attributes.map(span_attrs) + + expected_attrs = [ + {HTTPAttributes.http_request_method(), :POST}, + {HTTPAttributes.http_response_status_code(), 200}, + {HTTPAttributes.http_request_body_size(), 224}, + {HTTPAttributes.http_response_body_size(), 13}, + {NetworkAttributes.network_transport(), :tcp}, + {String.to_atom("#{HTTPAttributes.http_request_header()}.test-header"), ["request header"]}, + {String.to_atom("#{HTTPAttributes.http_response_header()}.content-type"), + ["application/json; charset=utf-8"]}, + {ServerAttributes.server_address(), "localhost"}, + {ServerAttributes.server_port(), bypass.port}, + {URLAttributes.url_full(), "http://localhost:#{bypass.port}/users/3?a=b"}, + {URLAttributes.url_scheme(), :http}, + {URLAttributes.url_template(), "/users/:user_id"} + ] + + for {attr, expected} <- expected_attrs do + actual = Map.get(attrs, attr) + assert expected == actual, "#{attr} expected #{expected} got #{inspect(actual)}" + end + + user_agent = Map.get(attrs, UserAgentAttributes.user_agent_original()) + assert String.starts_with?(user_agent, "req/") + end + + describe "errors" do + test "timeout exception error" do + Req.get(client(), + plug: fn conn -> + Req.Test.transport_error(conn, :timeout) + end, + retry: false, + url: "/" + ) + + expected_status = OpenTelemetry.status(:error, "timeout") + + assert_receive {:span, span(attributes: span_attrs, status: ^expected_status)} + + attrs = :otel_attributes.map(span_attrs) + + expected_attrs = [ + {ErrorAttributes.error_type(), Req.TransportError} + ] + + for {attr, expected} <- expected_attrs do + actual = Map.get(attrs, attr) + assert expected == actual, "#{attr} expected #{expected} got #{inspect(actual)}" + end + end + + test "4xx level error", %{bypass: bypass} do + Bypass.expect_once(bypass, "GET", "/", fn conn -> + conn + |> Plug.Conn.put_status(404) + |> Req.Test.text("not found") + end) + + Req.get(client(), + retry: false, + url: "http://localhost:#{bypass.port}" + ) + + expected_status = OpenTelemetry.status(:error, "") + + assert_receive {:span, span(attributes: span_attrs, status: ^expected_status)} + + attrs = :otel_attributes.map(span_attrs) + + expected_attrs = [ + {ErrorAttributes.error_type(), "404"} + ] + + for {attr, expected} <- expected_attrs do + actual = Map.get(attrs, attr) + assert expected == actual, "#{attr} expected #{expected} got #{inspect(actual)}" + end + end + + test "5xx level error", %{bypass: bypass} do + Bypass.expect_once(bypass, "GET", "/", fn conn -> + conn + |> Plug.Conn.put_status(500) + |> Req.Test.text("internal server error") + end) + + Req.get(client(), + retry: false, + url: "http://localhost:#{bypass.port}" + ) + + expected_status = OpenTelemetry.status(:error, "") + + assert_receive {:span, span(attributes: span_attrs, status: ^expected_status)} + + attrs = :otel_attributes.map(span_attrs) + + expected_attrs = [ + {ErrorAttributes.error_type(), "500"} + ] + + for {attr, expected} <- expected_attrs do + actual = Map.get(attrs, attr) + assert expected == actual, "#{attr} expected #{expected} got #{inspect(actual)}" + end + end + end + + # exception test + def ok_resp(conn) do + conn |> Req.Test.text("ok") + end + + describe "span name" do + test "with no path params" do + Req.get!(client(), + plug: &ok_resp/1, + url: "/" + ) + + assert_receive {:span, span(name: :GET)} + end + + test "with path params but template attr not set" do + Req.get!(client(), + plug: &ok_resp/1, + url: "http://localtest:8080/users/:id", + path_params: [id: 3], + params: [a: "b"] + ) + + assert_receive {:span, span(name: :GET)} + end + + test "with path params" do + Req.get!(client(opt_in_attrs: [URLAttributes.url_template()]), + plug: &ok_resp/1, + url: "http://localtest:8080/users/:id", + path_params: [id: 3], + params: [a: "b"] + ) + + assert_receive {:span, span(name: "GET /users/:id")} + end + + test "with span name attach option" do + Req.get!(client(span_name: "test"), + plug: &ok_resp/1, + url: "http://localtest:8080/users/:id", + path_params: [id: 3], + params: [a: "b"] + ) + + assert_receive {:span, span(name: "test")} + end + + test "with span name request option" do + Req.get!(client(span_name: "test"), + plug: &ok_resp/1, + url: "http://localtest:8080/users/:id", + path_params: [id: 3], + params: [a: "b"], + span_name: "overridden" + ) - {:ok, req: req} + assert_receive {:span, span(name: "overridden")} + end end - test "span", %{req: req} do - adapter = fn request -> - assert URI.to_string(request.url) == "/users/3" - {request, Req.Response.new(status: 204)} + describe "ports" do + test "when port present" do + Req.get!(client(), + plug: &ok_resp/1, + url: "http://localtest:8080" + ) + + assert_receive {:span, span(attributes: span_attrs)} + + attrs = :otel_attributes.map(span_attrs) + assert 8080 == Map.get(attrs, ServerAttributes.server_port()) + end + + test "when port not set and no scheme" do + Req.get!(client(), + plug: &ok_resp/1, + url: "/ok" + ) + + assert_receive {:span, span(attributes: span_attrs)} + + attrs = :otel_attributes.map(span_attrs) + assert 80 == Map.get(attrs, ServerAttributes.server_port()) end - resp = - Req.get!(req, - adapter: adapter, - url: "/users/:id", - path_params: [id: 3] + test "when port not set and http scheme" do + Req.get!(client(), + plug: &ok_resp/1, + url: "http://localtest" ) - assert resp.status == 204 - assert_receive {:span, span(name: "/users/:id")} - refute_receive _ + assert_receive {:span, span(attributes: span_attrs)} + + attrs = :otel_attributes.map(span_attrs) + assert 80 == Map.get(attrs, ServerAttributes.server_port()) + end + + test "when port not set and https scheme" do + Req.get!(client(), + plug: &ok_resp/1, + url: "https://localtest" + ) + + assert_receive {:span, span(attributes: span_attrs)} + + attrs = :otel_attributes.map(span_attrs) + assert 443 == Map.get(attrs, ServerAttributes.server_port()) + end end - test "propagate traces", %{req: req} do - adapter = fn request -> - assert [value] = Req.Request.get_header(request, "traceparent") - assert byte_size(value) > 10 - {request, Req.Response.new(status: 204)} + describe "propagation" do + test "off by default" do + plug = fn conn -> + assert [] == Plug.Conn.get_req_header(conn, "traceparent") + + ok_resp(conn) + end + + Req.get!(client(), + plug: plug, + url: "/" + ) + end + + test "enabled in attach" do + plug = fn conn -> + assert 1 == length(Plug.Conn.get_req_header(conn, "traceparent")) + + ok_resp(conn) + end + + Req.get!(client(propagate_trace_headers: true), + plug: plug, + url: "/" + ) end - resp = - Req.get!(req, - adapter: adapter, + test "enabled in request" do + plug = fn conn -> + assert 1 == length(Plug.Conn.get_req_header(conn, "traceparent")) + + ok_resp(conn) + end + + Req.get!(client(), + plug: plug, url: "/", - no_path_params: true, - propagate_trace_ctx: true + propagate_trace_headers: true ) + end + + test "disabled in request" do + plug = fn conn -> + assert 0 == length(Plug.Conn.get_req_header(conn, "traceparent")) + + ok_resp(conn) + end - assert resp.status == 204 - assert_receive {:span, span()} - refute_receive _ + Req.get!(client(propagate_trace_headers: true), + plug: plug, + url: "/", + propagate_trace_headers: false + ) + end end end