From 0db4db5af54c56e6492c6267fc48b11993cacc7a Mon Sep 17 00:00:00 2001 From: Bryan Naegele Date: Tue, 13 Aug 2024 10:21:36 -0600 Subject: [PATCH] HTTP instrumentation header extraction functions (#356) * HTTP instrumentation header extraction functions * allow setting beam opts to publishing * export types * Add test for x-forwarded-for w/multi entries --- .github/workflows/publish-mix-hex-release.yml | 18 +- .../CHANGELOG.md | 14 + .../README.md | 4 +- .../docs.config | 4 - .../docs.sh | 15 - .../rebar.config | 22 +- .../opentelemetry_instrumentation_http.erl | 461 +++++++++++++++--- ...entelemetry_instrumentation_http_SUITE.erl | 255 ++++++++++ 8 files changed, 702 insertions(+), 91 deletions(-) delete mode 100644 utilities/opentelemetry_instrumentation_http/docs.config delete mode 100755 utilities/opentelemetry_instrumentation_http/docs.sh create mode 100644 utilities/opentelemetry_instrumentation_http/test/opentelemetry_instrumentation_http_SUITE.erl diff --git a/.github/workflows/publish-mix-hex-release.yml b/.github/workflows/publish-mix-hex-release.yml index f161fe25..36b1728c 100644 --- a/.github/workflows/publish-mix-hex-release.yml +++ b/.github/workflows/publish-mix-hex-release.yml @@ -27,6 +27,18 @@ on: - "tesla" - "xandra" required: true + otp-version: + description: "OTP version" + type: string + default: "25.3.2.5" + elixir-version: + description: "Elixir version" + type: string + default: "1.14.5" + rebar3-version: + description: "Rebar3 version" + type: string + default: "3.22.1" action: description: "Publish release" required: true @@ -195,9 +207,9 @@ jobs: - uses: erlef/setup-beam@b9c58b0450cd832ccdb3c17cc156a47065d2114f # v1.18.1 with: version-type: strict - otp-version: "25.3.2.5" - elixir-version: "1.14.5" - rebar3-version: "3.22.1" + otp-version: ${{ inputs.otp-version }} + elixir-version: ${{ inputs.elixir-version }} + rebar3-version: ${{ inputs.rebar3-version }} - name: "Mix Hex Publish Dry-run" if: ${{ needs.config.outputs.build_tool == 'mix' }} diff --git a/utilities/opentelemetry_instrumentation_http/CHANGELOG.md b/utilities/opentelemetry_instrumentation_http/CHANGELOG.md index 6f2fd935..84f8ff07 100644 --- a/utilities/opentelemetry_instrumentation_http/CHANGELOG.md +++ b/utilities/opentelemetry_instrumentation_http/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## v0.2.0 + +## Features + +* Adds several header extraction and manipulation functions for common tasks + in instrumentation libraries: + * `extract_client_info/1` + * `extract_client_info/2` + * `extract_scheme/1` + * `extract_scheme/2` + * `extract_server_info/1` + * `extract_server_info/2` +* Generate OTP27 docs + ## v0.1.0 ### Changed diff --git a/utilities/opentelemetry_instrumentation_http/README.md b/utilities/opentelemetry_instrumentation_http/README.md index a88f8202..3bd52eaf 100644 --- a/utilities/opentelemetry_instrumentation_http/README.md +++ b/utilities/opentelemetry_instrumentation_http/README.md @@ -9,13 +9,13 @@ ```erlang {deps, [ - {opentelemetry_instrumentation_http, "~> 0.1"} + {opentelemetry_instrumentation_http, "~> 0.2"} ]} ``` ```elixir def deps do [ - {:opentelemetry_instrumentation_http, "~> 0.1"} + {:opentelemetry_instrumentation_http, "~> 0.2"} ] end ``` diff --git a/utilities/opentelemetry_instrumentation_http/docs.config b/utilities/opentelemetry_instrumentation_http/docs.config deleted file mode 100644 index e09daa28..00000000 --- a/utilities/opentelemetry_instrumentation_http/docs.config +++ /dev/null @@ -1,4 +0,0 @@ -{source_url, <<"https://github.com/open-telemetry/opentelemetry-erlang-contrib/tree/main/utilities/opentelemetry_instrumentation_http">>}. -{extras, [<<"LICENSE">>]}. -{main, <<"opentelemetry_instrumentation_http">>}. -{proglang, erlang}. \ No newline at end of file diff --git a/utilities/opentelemetry_instrumentation_http/docs.sh b/utilities/opentelemetry_instrumentation_http/docs.sh deleted file mode 100755 index eb2b2e05..00000000 --- a/utilities/opentelemetry_instrumentation_http/docs.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -e - -# Setup: -# -# mix escript.install github elixir-lang/ex_doc -# asdf install erlang 24.0.2 -# asdf local erlang 24.0.2 - -rebar3 compile -rebar3 as docs edoc -version=0.1.0 -ex_doc "opentelemetry_instrumentation_http" $version "_build/default/lib/opentelemetry_instrumentation_http/ebin" \ - --source-ref v${version} \ - --config docs.config $@ diff --git a/utilities/opentelemetry_instrumentation_http/rebar.config b/utilities/opentelemetry_instrumentation_http/rebar.config index 6e60337b..acfb7bb1 100644 --- a/utilities/opentelemetry_instrumentation_http/rebar.config +++ b/utilities/opentelemetry_instrumentation_http/rebar.config @@ -7,16 +7,6 @@ rebar3_hex ]}. {profiles, [ - {docs, [ - {deps, [edown]}, - {edoc_opts, [ - {preprocess, true}, - {doclet, edoc_doclet_chunks}, - {layout, edoc_layout_chunks}, - {dir, "_build/default/lib/opentelemetry_instrumentation_http/doc"}, - {subpackages, true} - ]} - ]}, {test, [ {erl_opts, [nowarn_export_all]}, {deps, []}, @@ -35,3 +25,15 @@ {cover_enabled, true}. {cover_export_enabled, true}. {covertool, [{coverdata_files, ["ct.coverdata"]}]}. + +{plugins, [rebar3_ex_doc]}. + +{hex, [ + {doc, #{provider => ex_doc}} +]}. + +{ex_doc, [ + {source_url, <<"https://github.com/opentelemetry-erlang-contrib/utilities/opentelemetry_instrumentation_http">>}, + {extras, [<<"README.md">>, <<"CHANGELOG.md">>, <<"LICENSE">>]}, + {main, <<"readme">>} +]}. \ No newline at end of file diff --git a/utilities/opentelemetry_instrumentation_http/src/opentelemetry_instrumentation_http.erl b/utilities/opentelemetry_instrumentation_http/src/opentelemetry_instrumentation_http.erl index 3cb59460..751c48b7 100644 --- a/utilities/opentelemetry_instrumentation_http/src/opentelemetry_instrumentation_http.erl +++ b/utilities/opentelemetry_instrumentation_http/src/opentelemetry_instrumentation_http.erl @@ -1,29 +1,423 @@ -module(opentelemetry_instrumentation_http). --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). -endif. -export([ extract_headers_attributes/3, - normalize_header_name/1 + extract_client_info/1, + extract_client_info/2, + extract_ip_port/1, + extract_scheme/1, + extract_scheme/2, + extract_server_info/1, + extract_server_info/2, + normalize_header_name/1, + parse_forwarded_header/1 ]). + +-type client_info() :: #{ip => binary() | undefined, port => integer()}. +-type header_name() :: binary() | string(). +-type header_value() :: binary(). +-type header() :: {header_name(), header_value()}. +-type headers_map() :: #{header_name() => header_value()}. +-type header_sort_fun() :: fun((Header1 :: header_name(), Header2 :: header_name()) -> boolean()). +-type server_info() :: #{address => binary() | undefined, port => integer() | undefined}. + +-export_type([ + client_info/0, + header_name/0, + header_value/0, + headers_map/0, + header_sort_fun/0, + server_info/0]). + +?MODULEDOC(""" +`opentelemetry_instrumentation_http` provides utility functions for +common otel http-related instrumentation operations such as extraction +of schemes, client and server info, and header operations. +"""). + +?DOC(""" +Extract the original client request protocol scheme from request headers. + +For lists of headers, the scheme is extracted from the first header +which can contain the scheme. For maps of headers, the map is converted +to a list and a sort function applied according to the semantic convention's +priority order. See the documentation for `extract_scheme/2` for more +information. +"""). + +-spec extract_scheme([header()] | headers_map()) -> http | https | undefined. +extract_scheme([]) -> + undefined; +extract_scheme(Map) when is_map(Map) and map_size(Map) == 0 -> + undefined; +extract_scheme(Headers) when is_list(Headers) -> + extract_scheme(Headers, fun list_scheme_headers_sort/2); +extract_scheme(Headers) when is_map(Headers) -> + extract_scheme(maps:to_list(Headers), fun map_scheme_headers_sort/2). + +?DOC(""" +Extract the original client request protocol scheme from request headers. +Users may supply their own sort function for prioritizing a particular header +over others. + +The default sort order per SemConv gives equal priority to `forwarded` +and `x-forwarded-proto` headers, followed by the `:scheme` HTTP2 header. +"""). +-spec extract_scheme(Headers, Fun) -> http | https | undefined + when Fun :: header_sort_fun(), Headers :: [header()]. +extract_scheme(Headers, SortFun) when is_list(Headers) -> + SortedHeaders = lists:sort(SortFun, Headers), + reduce_while( + SortedHeaders, undefined, fun(Header, Acc) -> + case extract_scheme_from_header(Header) of + undefined -> + {cont, Acc}; + <<"http">> -> + {halt, http}; + <<"https">> -> + {halt, https}; + _ -> + {halt, undefined} + end + end + ). + +list_scheme_headers_sort(_, _) -> true. + +map_scheme_headers_sort(HeaderName1, HeaderName2) -> + HN1Priority = scheme_header_priority(HeaderName1), + HN2Priority = scheme_header_priority(HeaderName2), + case {HN1Priority, HN2Priority} of + {H1, H2} when H1 =< H2 -> + true; + {H1, H2} when H1 > H2 -> + false + end. + +% Define header priority +scheme_header_priority({HeaderName, _Value}) -> + Priority = + case HeaderName of + <<"forwarded">> -> 1; + <<"x-forwarded-proto">> -> 1; + <<":scheme">> -> 2; + % Default priority for other headers + _ -> 4 + end, + Priority. + +extract_scheme_from_header({<<"forwarded">>, Value}) -> + DirectiveMap = parse_forwarded_header(Value), + SchemeValue = maps:get(<<"proto">>, DirectiveMap, []), + case SchemeValue of + [] -> + undefined; + [Scheme | _Rest] -> + Scheme; + _ -> + undefined + end; +extract_scheme_from_header({_Other, Value}) -> + Value. + +?DOC(""" +Extract the original client request protocol scheme from request headers. + +For lists of headers, the address and port are extracted from the first header +which can contain that vbalue. For maps of headers, the map is converted +to a list and a sort function applied according to the semantic convention's +priority order. See the documentation for `extract_server_info/2` for more +information. +"""). + +-spec extract_server_info([header()] | headers_map()) -> server_info(). +extract_server_info([]) -> + #{address => undefined, port => undefined}; +extract_server_info(Map) when is_map(Map) and map_size(Map) == 0 -> + #{address => undefined, port => undefined}; +extract_server_info(Headers) when is_list(Headers) -> + extract_server_info(Headers, fun list_server_headers_sort/2); +extract_server_info(Headers) when is_map(Headers) -> + extract_server_info(maps:to_list(Headers), fun map_server_headers_sort/2). + +?DOC(""" +Extract the original server address and port from request headers. +Users may supply their own sort function for prioritizing a particular header +over others. + +The default sort order per SemConv gives equal priority to `forwarded` +and `x-forwarded-host` headers, followed by the `:authority` HTTP2 header +and then `host`. +"""). +-spec extract_server_info(Headers, Fun) -> server_info() + when Fun :: header_sort_fun(), Headers :: [header()]. +extract_server_info(Headers, SortFun) -> + SortedHeaders = lists:sort(SortFun, Headers), + reduce_while( + SortedHeaders, #{address => undefined, port => undefined}, fun(Header, Acc) -> + case extract_server_info_from_header(Header) of + {undefined, undefined} -> + {cont, Acc}; + {Address, Port} -> + {halt, #{address => Address, port => Port}} + end + end + ). + +list_server_headers_sort(_, _) -> true. + +map_server_headers_sort(HeaderName1, HeaderName2) -> + HN1Priority = server_header_priority(HeaderName1), + HN2Priority = server_header_priority(HeaderName2), + case {HN1Priority, HN2Priority} of + {H1, H2} when H1 =< H2 -> + true; + {H1, H2} when H1 > H2 -> + false + end. + +server_header_priority({HeaderName, _Value}) -> + Priority = + case HeaderName of + <<"forwarded">> -> 1; + <<"x-forwarded-host">> -> 1; + <<":authority">> -> 2; + <<"host">> -> 3; + % Default priority for other headers + _ -> 4 + end, + Priority. + +extract_server_info_from_header({<<"forwarded">>, Value}) -> + DirectiveMap = parse_forwarded_header(Value), + HostValue = maps:get(<<"host">>, DirectiveMap, []), + case HostValue of + [] -> + {undefined, undefined}; + [LeftMostHost | _Rest] -> + case string:split(LeftMostHost, ":") of + [Host] -> + {Host, undefined}; + [Host, Port] -> + {Host, extract_port(Port)}; + _ -> + {undefined, undefined} + end + end; +extract_server_info_from_header({_Other, Value}) -> + case string:split(Value, ":") of + [Host] -> + {Host, undefined}; + [Host, Port] -> + {Host, extract_port(Port)}; + _ -> + {undefined, undefined} + end. + +?DOC(""" +Extract the original client request information from request headers. + +For lists of headers, the ip and port are extracted from the first header +which can contain the scheme. For maps of headers, the map is converted +to a list and a sort function applied according to the semantic convention's +priority order. See the documentation for `extract_client_info/2` for more +information. +"""). +-spec extract_client_info([header()] | headers_map()) -> client_info(). +extract_client_info([]) -> + #{ip => undefined, port => undefined}; +extract_client_info(Map) when is_map(Map) and map_size(Map) == 0 -> + #{ip => undefined, port => undefined}; +extract_client_info(Headers) when is_list(Headers) -> + extract_client_info(Headers, fun list_client_headers_sort/2); +extract_client_info(Headers) when is_map(Headers) -> + extract_client_info(maps:to_list(Headers), fun map_client_headers_sort/2). + +?DOC(""" +Extract the original server address and port from request headers. +Users may supply their own sort function for prioritizing a particular header +over others. + +The default sort order per SemConv gives equal priority to `forwarded` +and `x-forwarded-for` headers. +"""). +-spec extract_client_info(Headers, Fun) -> server_info() + when Fun :: header_sort_fun(), Headers :: [header()]. +extract_client_info(Headers, SortFun) -> + SortedHeaders = lists:sort(SortFun, Headers), + reduce_while( + SortedHeaders, #{ip => undefined, port => undefined}, fun(Header, Acc) -> + case extract_client_info_from_header(Header) of + {undefined, undefined} -> + {cont, Acc}; + {Ip, Port} -> + {halt, #{ip => Ip, port => Port}} + end + end + ). + +extract_client_info_from_header({<<"forwarded">>, Value}) -> + DirectiveMap = parse_forwarded_header(Value), + ForValue = maps:get(<<"for">>, DirectiveMap, []), + case ForValue of + [] -> + {undefined, undefined}; + [LeftMostFor | _Rest] -> + extract_ip_port(LeftMostFor) + end; +extract_client_info_from_header({_Other, Value}) -> + extract_ip_port(Value). + +map_client_headers_sort(HeaderName1, HeaderName2) -> + HN1Priority = client_header_priority(HeaderName1), + HN2Priority = client_header_priority(HeaderName2), + case {HN1Priority, HN2Priority} of + {H1, H2} when H1 =< H2 -> + true; + {H1, H2} when H1 > H2 -> + false + end. + +list_client_headers_sort(_, _) -> true. + +% Define header priority +client_header_priority({HeaderName, _Value}) -> + Priority = + case HeaderName of + <<"forwarded">> -> 1; + <<"x-forwarded-for">> -> 1; + % Default priority for other headers + _ -> 4 + end, + Priority. + +?DOC(""" +Parse a `forwarded` header to a map of directives. +"""). +-spec parse_forwarded_header(header()) -> + #{binary() => [header_value()]}. +parse_forwarded_header(Header) -> + KvpList = string:split(Header, <<";">>, all), + Grouped = lists:foldl(fun group_by/2, #{}, KvpList), + Grouped. + +group_by(Kvp, Acc) -> + SplitDirectives = string:split(Kvp, <<",">>, all), + lists:foldr( + fun(ProcessedKvp, A) -> + case string:split(string:trim(ProcessedKvp), <<"=">>, all) of + [Directive, Value] -> + TrimmedValue = string:trim(Value), + update_group(string:trim(Directive), TrimmedValue, A); + _Malformed -> + A + end + end, + Acc, + SplitDirectives + ). + +update_group(Key, Value, Acc) -> + case maps:get(Key, Acc, []) of + List -> Acc#{Key => [Value | List]} + end. + +extract_ip_port(IpStr) when is_binary(IpStr) -> + extract_ip_port(binary_to_list(IpStr)); +extract_ip_port(IpStr) when is_list(IpStr) -> + case re:split(IpStr, "[\]\[/\/\"]", [{return, list}, trim]) of + [[], [], Ip] -> + case inet:parse_ipv6strict_address(Ip) of + {ok, IpV6} -> + {inet:ntoa(IpV6), undefined}; + _ -> + {undefined, undefined} + end; + [[], [], Ip, Port] -> + case inet:parse_ipv6strict_address(Ip) of + {ok, IpV6} -> + {inet:ntoa(IpV6), extract_port(string:trim(Port, leading, ":"))}; + _ -> + {undefined, undefined} + end; + [IpV4Str] -> + case string:split(IpV4Str, ":") of + [Ip, Port] -> + case inet:parse_ipv4strict_address(Ip) of + {ok, IpV4} -> + {inet:ntoa(IpV4), extract_port(Port)}; + _ -> + {undefined, undefined} + end; + [Ip] -> + case inet:parse_ipv4strict_address(Ip) of + {ok, IpV4} -> + {inet:ntoa(IpV4), undefined}; + _ -> + {undefined, undefined} + end; + _ -> + {undefined, undefined} + end; + _Other -> + {undefined, undefined} + end; +extract_ip_port(_) -> + {undefined, undefined}. + +extract_port(PortStr) -> + case string:to_integer(PortStr) of + {error, _} -> + undefined; + {Port, _} when Port =< 65535 -> + Port; + _ -> + undefined + end. + +reduce_while([], Acc, _Fun) -> + Acc; +reduce_while([H | T], Acc, Fun) -> + case Fun(H, Acc) of + {halt, NewAcc} -> NewAcc; + {cont, NewAcc} -> reduce_while(T, NewAcc, Fun) + end. + +?DOC(""" +Normalizes a header name to a lowercase binary. +"""). -spec normalize_header_name(string() | binary()) -> Result when - Result :: binary() | {error, binary(), RestData} | {incomplete, binary(), binary()}, - RestData :: unicode:latin1_chardata() | unicode:chardata() | unicode:external_chardata(). + Result :: binary() | {error, binary(), RestData} | {incomplete, binary(), binary()}, + RestData :: unicode:latin1_chardata() | unicode:chardata() | unicode:external_chardata(). normalize_header_name(Header) when is_binary(Header) -> normalize_header_name(binary_to_list(Header)); normalize_header_name(Header) when is_list(Header) -> - unicode:characters_to_binary(string:replace(string:to_lower(Header), "-", "_", all)). + unicode:characters_to_binary(string:to_lower(Header)). + +?DOC(""" +Extracts a map of request or response header attributes for a given +set of attributes and list of headers to extract. +NOTE: keys are generated as atoms for efficiency purposes so take care +that header names have a defined cardinality set. These values should never +be dynamic. +"""). -spec extract_headers_attributes( request | response, Headers :: - #{binary() | string() => binary() | [binary()]} - | [{binary() | string(), binary() | [binary()]}], - HeadersToExtract :: [binary()] -) -> #{atom() => [binary()]}. + #{header_name() => header_value() | [header_value()]} + | [{header_name(), header_value() | [header_value()]}], + HeadersToExtract :: [header_name()] +) -> #{atom() => [header_value()]}. extract_headers_attributes(_Context, _Headers, []) -> #{}; extract_headers_attributes(Context, Headers, HeadersToExtract) when is_list(Headers) -> @@ -61,50 +455,3 @@ attribute_name(request, HeaderName) -> binary_to_atom(<<"http.request.header.", HeaderName/binary>>); attribute_name(response, HeaderName) -> binary_to_atom(<<"http.response.header.", HeaderName/binary>>). - --ifdef(TEST). - -normalize_header_name_test_() -> - [ - ?_assertEqual(normalize_header_name(<<"Content-Type">>), <<"content_type">>), - ?_assertEqual(normalize_header_name("Some-Header-NAME"), <<"some_header_name">>) - ]. - -extract_headers_attributes_test_() -> - [ - ?_assertEqual(extract_headers_attributes(request, [], []), #{}), - ?_assertEqual(extract_headers_attributes(response, #{}, []), #{}), - ?_assertEqual( - extract_headers_attributes( - request, - #{ - <<"Foo">> => <<"1">>, - "Bar-Baz" => [<<"2">>, <<"3">>], - "To-Not-Extract" => <<"4">> - }, - [<<"foo">>, <<"bar_baz">>] - ), - #{ - 'http.request.header.foo' => [<<"1">>], - 'http.request.header.bar_baz' => [<<"2">>, <<"3">>] - } - ), - ?_assertEqual( - extract_headers_attributes( - response, - [ - {<<"Foo">>, <<"1">>}, - {"Bar-Baz", <<"2">>}, - {"To-Not-Extract", <<"3">>}, - {<<"foo">>, <<"4">>} - ], - [<<"foo">>, <<"bar_baz">>] - ), - #{ - 'http.response.header.foo' => [<<"1">>, <<"4">>], - 'http.response.header.bar_baz' => [<<"2">>] - } - ) - ]. - --endif. diff --git a/utilities/opentelemetry_instrumentation_http/test/opentelemetry_instrumentation_http_SUITE.erl b/utilities/opentelemetry_instrumentation_http/test/opentelemetry_instrumentation_http_SUITE.erl new file mode 100644 index 00000000..c6158555 --- /dev/null +++ b/utilities/opentelemetry_instrumentation_http/test/opentelemetry_instrumentation_http_SUITE.erl @@ -0,0 +1,255 @@ +-module(opentelemetry_instrumentation_http_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +all() -> + [ + header_name_normalization, + extract_headers_attributes, + extracts_client_info_from_headers, + extracts_scheme_from_headers, + extracts_server_info_from_headers, + extracting_ip_and_port_addresses, + parse_forwarded_headers + ]. + +header_name_normalization(_Config) -> + ?assertEqual( + opentelemetry_instrumentation_http:normalize_header_name(<<"Content-Type">>), + <<"content-type">> + ), + ?assertEqual( + opentelemetry_instrumentation_http:normalize_header_name("Some-Header-NAME"), + <<"some-header-name">> + ), + ok. + +extract_headers_attributes(_Config) -> + ?assertEqual(opentelemetry_instrumentation_http:extract_headers_attributes(request, [], []), #{}), + ?assertEqual(opentelemetry_instrumentation_http:extract_headers_attributes(response, #{}, []), #{}), + ?assertEqual( + opentelemetry_instrumentation_http:extract_headers_attributes( + request, + #{ + <<"Foo">> => <<"1">>, + <<"Bar-Baz">> => [<<"2">>, <<"3">>], + <<"To-Not-Extract">> => <<"4">> + }, + [<<"foo">>, <<"bar-baz">>] + ), + #{ + 'http.request.header.foo' => [<<"1">>], + 'http.request.header.bar-baz' => [<<"2">>, <<"3">>] + } + ), + ?assertEqual( + opentelemetry_instrumentation_http:extract_headers_attributes( + response, + [ + {<<"Foo">>, <<"1">>}, + {"Bar-Baz", <<"2">>}, + {"To-Not-Extract", <<"3">>}, + {<<"foo">>, <<"4">>} + ], + [<<"foo">>, <<"bar-baz">>] + ), + #{ + 'http.response.header.foo' => [<<"1">>, <<"4">>], + 'http.response.header.bar-baz' => [<<"2">>] + } + ), + ok. + +parse_forwarded_headers(_Config) -> + ?assertEqual( + #{ + <<"host">> => [<<"developer.mozilla.org:4321">>], + <<"for">> => [<<"192.0.2.60">>, <<"\"[2001:db8:cafe::17]\"">>], + <<"proto">> => [<<"http">>], + <<"by">> => [<<"203.0.113.43">>] + }, + opentelemetry_instrumentation_http:parse_forwarded_header( + <<"host=developer.mozilla.org:4321; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">> + ) + ). + +extracting_ip_and_port_addresses(_Config) -> + ?assertEqual( + {"192.0.2.60", undefined}, + opentelemetry_instrumentation_http:extract_ip_port(<<"192.0.2.60">>) + ), + ?assertEqual( + {"192.0.2.60", 443}, + opentelemetry_instrumentation_http:extract_ip_port(<<"192.0.2.60:443">>) + ), + ?assertEqual( + {"192.0.2.60", undefined}, + opentelemetry_instrumentation_http:extract_ip_port(<<"192.0.2.60:junk">>) + ), + ?assertEqual( + {"2001:db8:cafe::17", undefined}, + opentelemetry_instrumentation_http:extract_ip_port(<<"\"[2001:db8:cafe::17]\"">>) + ), + ?assertEqual( + {"2001:db8:cafe::17", 8000}, + opentelemetry_instrumentation_http:extract_ip_port(<<"\"[2001:db8:cafe::17]:8000\"">>) + ), + ?assertEqual( + {"::", undefined}, + opentelemetry_instrumentation_http:extract_ip_port(<<"\"[::]:99999\"">>) + ), + ?assertEqual( + {"2001:db8:cafe::17", undefined}, + opentelemetry_instrumentation_http:extract_ip_port(<<"\"[2001:db8:cafe::17]:junk\"">>) + ). + +extracts_client_info_from_headers(_Config) -> + ?assertEqual( + #{ip => "192.0.2.60", port => undefined}, + opentelemetry_instrumentation_http:extract_client_info(#{ + <<"forwarded">> => + <<"host=developer.mozilla.org:4321; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">> + }) + ), + ?assertEqual( + #{ip => "2001:db8:cafe::17", port => undefined}, + opentelemetry_instrumentation_http:extract_client_info([ + {<<"forwarded">>, + <<"host=developer.mozilla.org:4321; for=\"[2001:db8:cafe::17]\", for=192.0.2.60; proto=http;by=203.0.113.43">>} + ]) + ), + ?assertEqual( + #{ip => "2001:db8:cafe::17", port => 9678}, + opentelemetry_instrumentation_http:extract_client_info([ + {<<"forwarded">>, + <<"host=developer.mozilla.org:4321;for=\"[2001:db8:cafe::17]:9678\",for=192.0.2.60;proto=http;by=203.0.113.43">>} + ]) + ), + ?assertEqual( + #{ip => "23.0.2.1", port => 2121}, + opentelemetry_instrumentation_http:extract_client_info([ + {<<"x-forwarded-for">>, <<"23.0.2.1:2121,25.2.2.2">>} + ]) + ), + ?assertEqual( + #{ip => "192.0.2.60", port => undefined}, + opentelemetry_instrumentation_http:extract_client_info(#{ + <<"x-forwarded-for">> => <<"23.0.2.1:2121">>, + <<"forwarded">> => + <<"host=developer.mozilla.org:4321; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">> + }) + ), + ?assertEqual( + #{ip => "192.0.2.60", port => undefined}, + opentelemetry_instrumentation_http:extract_client_info(#{ + <<"forwarded">> => + <<"host=developer.mozilla.org:4321; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">>, + <<"x-forwarded-for">> => <<"23.0.2.1:2121">> + }) + ), + ?assertEqual( + #{ip => "23.0.2.1", port => 2121}, + opentelemetry_instrumentation_http:extract_client_info([ + {<<"x-forwarded-for">>, <<"23.0.2.1:2121,10.100.10.10">>}, + {<<"forwarded">>, + <<"host=developer.mozilla.org:4321; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">>} + ]) + ), + ?assertEqual( + #{ip => "192.0.2.60", port => undefined}, + opentelemetry_instrumentation_http:extract_client_info([ + {<<"forwarded">>, + <<"host=developer.mozilla.org:4321; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">>}, + {<<"x-forwarded-for">>, <<"23.0.2.1:2121,10.100.10.10">>} + ]) + ), + ?assertEqual( + #{ip => "27.27.27.27", port => 2222}, + opentelemetry_instrumentation_http:extract_client_info([ + {<<"forwarded">>, + <<"host=developer.mozilla.org:4321; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">>}, + {<<"x-forwarded-for">>, <<"23.0.2.1:2121">>}, + {<<"x-real-client-ip">>, <<"27.27.27.27:2222">>} + ], + fun(Header1, _Header2) -> + Header1 == <<"x-real-client-ip">> + end) + ). + + +extracts_server_info_from_headers(_Config) -> + ?assertEqual( + #{address => <<"developer.mozilla.org">>, port => 4321}, + opentelemetry_instrumentation_http:extract_server_info(#{ + <<"forwarded">> => + <<"host=developer.mozilla.org:4321; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">> + }) + ), + ?assertEqual( + #{address => <<"developer.mozilla.org">>, port => undefined}, + opentelemetry_instrumentation_http:extract_server_info([ + {<<"forwarded">>, + <<"host=developer.mozilla.org; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">> + }]) + ), + ?assertEqual( + #{address => <<"d1.mozilla.org">>, port => undefined}, + opentelemetry_instrumentation_http:extract_server_info([ + {<<"host">>, <<"d1.mozilla.org">>}, + {<<"forwarded">>, + <<"host=developer.mozilla.org; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">>} + ]) + ), + ?assertEqual( + #{address => <<"developer.mozilla.org">>, port => undefined}, + opentelemetry_instrumentation_http:extract_server_info([ + {<<"x-forwarded-host">>, <<"developer.mozilla.org">>}, + {<<"forwarded">>, + <<"host=d1.mozilla.org; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">>} + ]) + ), + ?assertEqual( + #{address => <<"developer.mozilla.org">>, port => undefined}, + opentelemetry_instrumentation_http:extract_server_info([ + {<<"forwarded">>, + <<"host=developer.mozilla.org; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">>}, + {<<"host">>, <<"d1.mozilla.org">>} + ]) + ). + + extracts_scheme_from_headers(_Config) -> + ?assertEqual( + http, + opentelemetry_instrumentation_http:extract_scheme(#{ + <<"forwarded">> => + <<"host=developer.mozilla.org:4321; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">> + }) + ), + ?assertEqual( + http, + opentelemetry_instrumentation_http:extract_scheme([ + {<<"forwarded">>, + <<"host=developer.mozilla.org; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">> + }]) + ), + ?assertEqual( + https, + opentelemetry_instrumentation_http:extract_scheme([ + {<<"x-forwarded-proto">>, <<"https">>}, + {<<"forwarded">>, + <<"host=developer.mozilla.org; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">>} + ]) + ), + ?assertEqual( + https, + opentelemetry_instrumentation_http:extract_scheme([ + {<<":scheme">>, <<"https">>}, + {<<"forwarded">>, + <<"host=d1.mozilla.org; for=192.0.2.60, for=\"[2001:db8:cafe::17]\";proto=http;by=203.0.113.43">>} + ]) + ). + \ No newline at end of file