Current section
Files
Jump to
Current section
Files
src/otel_http.erl
-module(otel_http).
-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,
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("""
`otel_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} ->
{list_to_binary(inet:ntoa(IpV6)), undefined};
_ ->
{undefined, undefined}
end;
[[], [], Ip, Port] ->
case inet:parse_ipv6strict_address(Ip) of
{ok, IpV6} ->
{list_to_binary(inet:ntoa(IpV6)), extract_port(string:trim(Port, leading, ":"))};
_ ->
{undefined, undefined}
end;
[IpV4Str] ->
[LeftMostIpV4Str | _] = string:split(IpV4Str, <<",">>),
case string:split(LeftMostIpV4Str, ":") of
[Ip, Port] ->
case inet:parse_ipv4strict_address(Ip) of
{ok, IpV4} ->
{list_to_binary(inet:ntoa(IpV4)), extract_port(Port)};
_ ->
{undefined, undefined}
end;
[Ip] ->
case inet:parse_ipv4strict_address(Ip) of
{ok, IpV4} ->
{list_to_binary(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().
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: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 ::
#{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) ->
lists:foldr(
fun({HeaderName, HeaderValue}, Extracted) ->
extract_if_match(Context, HeaderName, HeaderValue, Extracted, HeadersToExtract)
end,
#{},
Headers
);
extract_headers_attributes(Context, Headers, HeadersToExtract) when is_map(Headers) ->
maps:fold(
fun(HeaderName, HeaderValue, Extracted) ->
extract_if_match(Context, HeaderName, HeaderValue, Extracted, HeadersToExtract)
end,
#{},
Headers
).
extract_if_match(Context, HeaderName, HeaderValue, Extracted, HeadersToExtract) ->
NormalizedHeaderName = normalize_header_name(HeaderName),
case lists:member(NormalizedHeaderName, HeadersToExtract) of
false -> Extracted;
true -> set_header_attribute(Context, NormalizedHeaderName, HeaderValue, Extracted)
end.
set_header_attribute(Context, Header, Value, HeadersAttributes) when is_list(Value) ->
maps:put(attribute_name(Context, Header), Value, HeadersAttributes);
set_header_attribute(Context, Header, Value, HeadersAttributes) ->
maps:update_with(
attribute_name(Context, Header), fun(V) -> [Value | V] end, [Value], HeadersAttributes
).
attribute_name(request, HeaderName) ->
binary_to_atom(<<"http.request.header.", HeaderName/binary>>);
attribute_name(response, HeaderName) ->
binary_to_atom(<<"http.response.header.", HeaderName/binary>>).