Add telepoison (#148)

* add telepoison

* rename

* rename file too, duh

* rename test file

* port updates

* add standard workflow
This commit is contained in:
Cristiano Piemontese 2023-06-04 16:23:03 +02:00 committed by GitHub
parent 901b571b07
commit 0cc8c760d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1362 additions and 0 deletions

View File

@ -463,3 +463,40 @@ jobs:
run: mix format --check-formatted
- name: Test
run: mix test
opentelemetry-httpoison:
needs: [test-matrix]
if: (contains(github.event.pull_request.labels.*.name, 'elixir') && contains(github.event.pull_request.labels.*.name, 'opentelemetry_httpoison'))
env:
app: 'opentelemetry_httpoison'
defaults:
run:
working-directory: instrumentation/${{ env.app }}
runs-on: ubuntu-20.04
name: Opentelemetry HTTPoison test on Elixir ${{ matrix.elixir_version }} (OTP ${{ matrix.otp_version }})
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.test-matrix.outputs.matrix) }}
steps:
- uses: actions/checkout@v3
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ matrix.otp_version }}
elixir-version: ${{ matrix.elixir_version }}
rebar3-version: ${{ matrix.rebar3_version }}
- name: Cache
uses: actions/cache@v3
with:
path: |
~/deps
~/_build
key: ${{ runner.os }}-build-${{ matrix.otp_version }}-${{ matrix.elixir_version }}-v3-${{ hashFiles('**/mix.lock') }}
- name: Fetch deps
if: steps.deps-cache.outputs.cache-hit != 'true'
run: mix deps.get
- name: Compile project
run: mix compile --warnings-as-errors
- name: Check formatting
run: mix format --check-formatted
- name: Test
run: mix test

View File

@ -0,0 +1,136 @@
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any config using `mix credo -C <name>`. If no config name is given
# "default" is used.
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
included: ["lib/", "test/"],
excluded: [~r"/_build/", ~r"/deps/"]
},
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
requires: [],
#
# Credo automatically checks for updates, like e.g. Hex does.
# You can disable this behaviour below:
check_for_updates: true,
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
strict: true,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: [
{Credo.Check.Consistency.ExceptionNames},
{Credo.Check.Consistency.LineEndings},
{Credo.Check.Consistency.MultiAliasImportRequireUse, false},
{Credo.Check.Consistency.ParameterPatternMatching},
{Credo.Check.Consistency.SpaceAroundOperators},
{Credo.Check.Consistency.SpaceInParentheses},
{Credo.Check.Consistency.TabsOrSpaces},
{Credo.Check.Refactor.MapInto, false},
# For some checks, like AliasUsage, you can only customize the priority
# Priority values are: `low, normal, high, higher`
{Credo.Check.Design.AliasUsage, priority: :low, if_nested_deeper_than: 10},
# For others you can set parameters
# If you don't want the `setup` and `test` macro calls in ExUnit tests
# or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just
# set the `excluded_macros` parameter to `[:schema, :setup, :test]`.
{Credo.Check.Design.DuplicatedCode, mass_threshold: 60},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
{Credo.Check.Design.TagTODO, exit_status: 0},
{Credo.Check.Design.TagFIXME},
{Credo.Check.Readability.AliasOrder, false},
{Credo.Check.Readability.FunctionNames},
{Credo.Check.Readability.LargeNumbers},
{Credo.Check.Readability.MaxLineLength, false},
{Credo.Check.Readability.ModuleAttributeNames},
{Credo.Check.Readability.ModuleDoc},
{Credo.Check.Readability.ModuleNames},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs},
{Credo.Check.Readability.ParenthesesInCondition},
{Credo.Check.Readability.PredicateFunctionNames},
{Credo.Check.Readability.PreferImplicitTry, false},
{Credo.Check.Readability.RedundantBlankLines},
{Credo.Check.Readability.SinglePipe},
{Credo.Check.Readability.StringSigils},
{Credo.Check.Readability.TrailingBlankLine},
{Credo.Check.Readability.TrailingWhiteSpace},
{Credo.Check.Readability.VariableNames},
{Credo.Check.Readability.Semicolons},
{Credo.Check.Readability.SpaceAfterCommas},
{Credo.Check.Refactor.DoubleBooleanNegation},
{Credo.Check.Refactor.CondStatements},
{Credo.Check.Refactor.MatchInCondition},
{Credo.Check.Refactor.PipeChainStart},
{Credo.Check.Refactor.CyclomaticComplexity},
{Credo.Check.Refactor.FunctionArity, max_arity: 7, ignore_defp: true},
{Credo.Check.Refactor.LongQuoteBlocks},
{Credo.Check.Refactor.MatchInCondition},
{Credo.Check.Refactor.NegatedConditionsInUnless},
{Credo.Check.Refactor.NegatedConditionsWithElse},
{Credo.Check.Refactor.Nesting},
{Credo.Check.Refactor.UnlessWithElse},
{Credo.Check.Warning.BoolOperationOnSameValues},
{Credo.Check.Warning.IExPry},
{Credo.Check.Warning.IoInspect},
{Credo.Check.Warning.OperationOnSameValues},
{Credo.Check.Warning.OperationWithConstantResult},
{Credo.Check.Warning.UnusedEnumOperation},
{Credo.Check.Warning.UnusedFileOperation},
{Credo.Check.Warning.UnusedKeywordOperation},
{Credo.Check.Warning.UnusedListOperation},
{Credo.Check.Warning.UnusedPathOperation},
{Credo.Check.Warning.UnusedRegexOperation},
{Credo.Check.Warning.UnusedStringOperation},
{Credo.Check.Warning.UnusedTupleOperation},
{Credo.Check.Warning.RaiseInsideRescue},
# Controversial and experimental checks (opt-in, just remove `, false`)
{Credo.Check.Warning.LazyLogging, false},
{Credo.Check.Refactor.ABCSize, false},
{Credo.Check.Refactor.AppendSingleItem, false},
{Credo.Check.Refactor.VariableRebinding, false},
{Credo.Check.Warning.MapGetUnsafePass, false},
# Deprecated checks (these will be deleted after a grace period)
{Credo.Check.Readability.Specs, false}
# Custom checks can be created using `mix credo.gen.check`.
#
]
}
]
}

View File

@ -0,0 +1,21 @@
[
locals_without_parens: [
field: :*,
resolve: :*,
plug: :*,
arg: :*,
add: :*,
parse: :*,
serialize: :*,
value: :*,
has_one: :*,
has_many: :*,
from: :*,
get: :*,
post: :*,
put: :*,
belongs_to: :*
],
inputs: ["lib/**/*.{ex,exs}", "test/**/*.{ex,exs}", "config/**/*.{ex,exs}"],
line_length: 120
]

View File

@ -0,0 +1,189 @@
# OpentelemetryHTTPoison
[![Module Version](https://img.shields.io/hexpm/v/opentelemetry_httpoison.svg)](https://hex.pm/packages/opentelemetry_httpoison)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/opentelemetry_httpoison/)
[![Total Downloads](https://img.shields.io/hexpm/dt/opentelemetry_httpoison.svg)](https://hex.pm/packages/opentelemetry_httpoison)
OpentelemetryHTTPoison is a [opentelemetry-instrumented](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/glossary.md#instrumented-library) wrapper around HTTPoison.
## Usage
Replace usages of the `HTTPoison` module with `OpentelemetryHTTPoison` when calling one of the *derived* request functions provided by `HTTPoison` (`HTTPoison.get/3`, `HTTPoison.get!/3` etc.)
```elixir
# Before
HTTPoison.get!(url, headers, opts)
# After
OpentelemetryHTTPoison.get!(url, headers, opts)
```
## Configuration
OpentelemetryHTTPoison can be configured through `config :opentelemetry_httpoison`. The configurable options are:
* `:ot_attributes`: what default Open Telemetry metadata attributes will be sent per request
If no value is provided, then no default Open Telemetry metadata attributes will sent per request by default
If a `list` of two element `tuple`s (both elements of `String.t()`) is provided, then these will form the default Open Telemetry metadata attributes sent per request
The first element of a provided `tuple` is the attribute name, e.g. `service.name`, whilst the second element is the attribute value, e.g. "shoppingcart"
* `:infer_route`: how the `http.route` Open Telemetry metadata will be set per request
If no value is provided then the out of the box, conservative inference provided by `OpentelemetryHTTPoison.URI.infer_route_from_request/1` is used to determine the inference
If a function with an arity of 1 (the argument given being the `t:HTTPoison.Request/0` `request`) is provided then that function is used to determine the inference
Both of these can be overridden per each call to OpentelemetryHTTPoison functions that wrap `OpentelemetryHTTPoison.request/1`, such as `OpentelemetryHTTPoison.get/3`, `OpentelemetryHTTPoison.get!/3`, `OpentelemetryHTTPoison.post/3` etc.
See here for [examples](#examples)
## Open Telemetry integration
Additionally, `OpentelemetryHTTPoison` provides some options that can be added to each derived function via
the `Keyword list` `opts` parameter (or the `t:HTTPoison.Request/0` `Keyword list` `options` parameter if calling `OpentelemetryHTTPoison.Request/1` directly). These are prefixed with `:ot_`.
* `:ot_span_name` - sets the span name.
* `:ot_attributes` - a list of `{name, value}` `tuple` attributes that will be added to the span.
* `:ot_resource_route` - sets the `http.route` attribute, depending on the value provided.
If the value is a string or an function with an arity of 1 (the `t:HTTPoison.Request/0` `request`) that is used to set the attribute
If `:infer` is provided, then the function discussed within the [Configuration](#configuration) section is used to set the attribute
If the atom `:ignore` is provided then the `http.route` attribute is ignored entirely
**It is highly recommended** to supply the `:ot_resource_route` explicitly as either a string or a function with an arity of 1 (the `t:HTTPoison.Request/0` `request`)
## Examples
In the below examples, `OpentelemetryHTTPoison.get!/3` is used for the sake of simplicity but other functions derived from `OpentelemetryHTTPoison.request/1` can be used
```elixir
config :opentelemetry_httpoison,
ot_attributes: [{"service.name", "users"}]
OpentelemetryHTTPoison.get!(
"https://www.example.com/user/list",
[],
ot_span_name: "list example users",
ot_attributes: [{"example.language", "en"}],
ot_resource_route: :infer
)
```
In the example above:
* OpentelemetryHTTPoison is configured with `{"service.name", "users"}` as the value for the `:ot_attributes` option
* `:infer` is passed as the value for the `:ot_resource_route` `Keyword list` option
Given the above, the `service.name` attribute will be set to "users" and the `http.route` attribute will be inferred as */user/:subpath*
```elixir
OpentelemetryHTTPoison.get!(
"https://www.example.com/user/list",
[],
ot_span_name: "list example users",
ot_attributes: [{"example.language", "en"}],
ot_resource_route: :infer
)
```
In the example above:
* `:infer` is passed as the value for `:ot_resource_route` `Keyword list` option
Given the above, the `http.route` attribute will be inferred as */user/:subpath*
```elixir
config :opentelemetry_httpoison,
infer_route: fn
%HTTPoison.Request{} = request -> URI.parse(request.url).path
end
OpentelemetryHTTPoison.get!(
"https://www.example.com/user/list",
[],
ot_resource_route: :infer
)
```
In the example above:
* OpentelemetryHTTPoison is configured with the `:infer_route` option set to a function which takes a `%HTTPoison.Request/0` argument, returning the path of the request URL
* `:infer` is passed as the value for `:ot_resource_route` `Keyword list` option
Given the above, the `http.route` attribute will be inferred as */user/list*
```elixir
OpentelemetryHTTPoison.get!(
"https://www.example.com/user/list",
[],
ot_resource_route: "my secret path"
)
```
In the example above:
* `"my secret path"` is passed as the value for `:ot_resource_route` `Keyword list` option
Given the above, the `http.route` attribute will be set as *my secret path*
```elixir
OpentelemetryHTTPoison.get!(
"https://www.example.com/user/list",
[],
ot_resource_route: :ignore
)
```
In the example above:
* `:ignore` is passed as the value for `:ot_resource_route` `Keyword list` option
Given the above, the `http.route` attribute will not be set to any value
## How it works
OpentelemetryHTTPoison, when executing an HTTP request to an external service, creates an OpenTelemetry span, injects
the [trace context propagation headers](https://www.w3.org/TR/trace-context/) in the request headers, and
ends the span once the response is received.
It automatically sets some of the [HTTP span attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md) like `http.status` etc,
based on the request and response data.
OpentelemetryHTTPoison by itself is not particularly useful: it becomes useful when used in conjunction with a "server-side"
opentelemetry-instrumented library, e.g. [opentelemetry_plug](https://github.com/opentelemetry-beam/opentelemetry_plug).
These do the opposite work: they take the trace context information from the request headers,
and they create a [SERVER](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/api.md#spankind) span which becomes the currently active span.
Using the two libraries together, it's possible to propagate trace information across several microservices and
through HTTP "jumps".
## Keep in mind
* The [Erlang opentelemetry SDK](https://github.com/open-telemetry/opentelemetry-erlang) stores
the currently active span in a `pdict`, a per-process dict.
If OpentelemetryHTTPoison is called from a different process than the one that initially handled the request and created
the "server-side" span, OpentelemetryHTTPoison won't find a parent span and will create a new root client span,
losing the trace context.
In this case, your only option to correctly propagate the trace context is to manually pass around the parent
span, and pass it to OpentelemetryHTTPoison when doing the HTTP client request.
* If the request fails due to nxdomain, the `process_response_status_code` hook is not called and therefore
the span is not ended.
## What's missing
* Set [SpanKind](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/api.md#spankind) to client
* Support for explicit parent span
* Support for fixed span attributes
* A lot of other stuff..
## Copyright and License
Copyright (c) 2020 Prima.it
This work is free. You can redistribute it and/or modify it under the
terms of the MIT License. See the [LICENSE.md](./LICENSE.md) file for more details.

View File

@ -0,0 +1,7 @@
import Config
try do
import_config "#{config_env()}.exs"
rescue
_ -> :ok
end

View File

@ -0,0 +1,5 @@
import Config
config :opentelemetry,
traces_exporter: :none,
processors: [{:otel_simple_processor, %{}}]

View File

@ -0,0 +1,69 @@
defmodule OpentelemetryHTTPoison.Configuration do
@moduledoc false
require Logger
@spec setup(any) :: nil
def setup(opts \\ []) do
errors = []
opts_infer_route = Keyword.get(opts, :infer_route)
if opts_infer_route != nil do
set(:infer_route, opts_infer_route)
end
route_inference_fn = get(:infer_route)
errors =
add_if(
errors,
not is_function(route_inference_fn, 1),
[
"\nThe configured :infer_route keyword option value must be a function with an arity of 1"
]
)
opts_ot_attributes = Keyword.get(opts, :ot_attributes)
if opts_ot_attributes != nil do
set(:ot_attributes, opts_ot_attributes)
end
ot_attributes = get(:ot_attributes)
errors =
add_if(
errors,
not is_list(ot_attributes),
["\nThe configured :ot_attributes option must be a [{key, value}] list"]
)
case errors do
[] -> nil
_ -> raise ArgumentError, Enum.join(errors)
end
end
@doc """
Get a configured option
"""
@spec get(:infer_route | :ot_attributes) :: any()
def get(key)
def get(:infer_route),
do:
Application.get_env(
:opentelemetry_httpoison,
:infer_route,
&OpentelemetryHTTPoison.URI.infer_route_from_request/1
)
def get(:ot_attributes), do: Application.get_env(:opentelemetry_httpoison, :ot_attributes, [])
defp set(key, value), do: Application.put_env(:opentelemetry_httpoison, key, value)
defp add_if(list, condition, value)
defp add_if(list, true, value), do: [value | list]
defp add_if(list, false, _), do: list
end

View File

@ -0,0 +1,267 @@
defmodule OpentelemetryHTTPoison do
@moduledoc """
OpenTelemetry-instrumented wrapper around HTTPoison.Base
A client request span is created on request creation, and ended once we get the response.
http.status and other standard http span attributes are set automatically.
"""
use HTTPoison.Base
require OpenTelemetry
require OpenTelemetry.SemanticConventions.Trace, as: Conventions
require OpenTelemetry.Span
require OpenTelemetry.Tracer
require Record
require Logger
alias HTTPoison.Request
alias OpenTelemetry.Tracer
alias OpentelemetryHTTPoison.Configuration
@http_url Atom.to_string(Conventions.http_url())
@http_method Atom.to_string(Conventions.http_method())
@http_route Atom.to_string(Conventions.http_route())
@http_status_code Atom.to_string(Conventions.http_status_code())
@net_peer_name Atom.to_string(Conventions.net_peer_name())
@doc ~S"""
Configures OpentelemetryHTTPoison using the provided `opts` `Keyword list`.
You should call this function within your application startup, before OpentelemetryHTTPoison is used.
Using the `:ot_attributes` option, you can set default Open Telemetry metadata attributes
to be added to each OpentelemetryHTTPoison request in the format of a list of two element tuples, with both elements
being strings.
Attributes can be overridden per each call to `OpentelemetryHTTPoison.request/1`.
Using the `:infer_route` option, you can customise the URL resource route inference procedure
that is used to set the `http.route` Open Telemetry metadata attribute.
If a function with an arity of 1 (the `t:HTTPoison.Request/0` `request`) is provided
then that function is used to determine the inference.
If no value is provided then the out of the box, conservative inference provided by
`OpentelemetryHTTPoison.URI.infer_route_from_request/1` is used to determine the inference.
This can be overridden per each call to `OpentelemetryHTTPoison.request/1`.
## Examples
iex> OpentelemetryHTTPoison.setup()
:ok
iex> infer_fn = fn
...> %HTTPoison.Request{} = request -> URI.parse(request.url).path
...> end
iex> OpentelemetryHTTPoison.setup(infer_route: infer_fn)
:ok
iex> OpentelemetryHTTPoison.setup(ot_attributes: [{"service.name", "..."}, {"service.namespace", "..."}])
:ok
iex> infer_fn = fn
...> %HTTPoison.Request{} = request -> URI.parse(request.url).path
...> end
iex> ot_attributes = [{"service.name", "..."}, {"service.namespace", "..."}]
iex> OpentelemetryHTTPoison.setup(infer_route: infer_fn, ot_attributes: ot_attributes)
:ok
"""
def setup(opts \\ []) do
Logger.warning("setup/1 is deprecated, use `config :opentelemetry_httpoison, ...` instead")
Configuration.setup(opts)
:ok
end
def process_request_headers(headers) when is_map(headers) do
headers
|> Enum.into([])
|> process_request_headers()
end
def process_request_headers(headers) when is_list(headers) do
headers
# Convert atom header keys.
# otel_propagator_text_map only accepts string keys, while Request.headers() keys can be atoms or strings.
# The value in Request.headers() has to be a binary() so we don't need to convert it
#
# Note that this causes the header keys from HTTPoison.Response{request: %{headers: headers}} to also become strings
# while with plain HTTPoison they would remain atoms.
|> Enum.map(fn {k, v} -> {to_string(k), v} end)
|> :otel_propagator_text_map.inject()
end
@doc ~S"""
Performs a request using OpentelemetryHTTPoison with the provided `t:HTTPoison.Request/0` `request`.
Depending how `OpentelemetryHTTPoison` is configured and whether or not the `:ot_resource_route`
option is set to `:infer` (provided as a part of the `t:HTTPoison.Request/0` `options` `Keyword list`)
this may attempt to automatically set the `http.route` Open Telemetry metadata attribute by obtaining
the first segment of the `t:HTTPoison.Request/0` `url` (since this part typically does not contain dynamic data)
If this behavior is not desirable, it can be set directly as a string or a function
with an arity of 1 (the `t:HTTPoison.Request/0` `request`) by using the aforementioned `:ot_resource_route` option.
It can also be circumvented entirely by suppling `:ignore` instead.
## Examples
iex> request = %HTTPoison.Request{
...> method: :post,
...> url: "https://www.example.com/users/edit/2",
...> body: ~s({"foo": 3}),
...> headers: [{"Accept", "application/json"}]}
iex> OpentelemetryHTTPoison.request(request)
iex> request = %HTTPoison.Request{
...> method: :post,
...> url: "https://www.example.com/users/edit/2",
...> body: ~s({"foo": 3}),
...> headers: [{"Accept", "application/json"}],
...> options: [ot_resource_route: :infer]}
iex> OpentelemetryHTTPoison.request(request)
iex> resource_route = "/users/edit/"
iex> request = %HTTPoison.Request{
...> method: :post,
...> url: "https://www.example.com/users/edit/2",
...> body: ~s({"foo": 3}),
...> headers: [{"Accept", "application/json"}],
...> options: [ot_resource_route: resource_route]}
iex> OpentelemetryHTTPoison.request(request)
iex> infer_fn = fn
...> %HTTPoison.Request{} = request -> URI.parse(request.url).path
...> end
iex> request = %HTTPoison.Request{
...> method: :post,
...> url: "https://www.example.com/users/edit/2",
...> body: ~s({"foo": 3}),
...> headers: [{"Accept", "application/json"}],
...> options: [ot_resource_route: infer_fn]}
iex> OpentelemetryHTTPoison.request(request)
iex> request = %HTTPoison.Request{
...> method: :post,
...> url: "https://www.example.com/users/edit/2",
...> body: ~s({"foo": 3}),
...> headers: [{"Accept", "application/json"}],
...> options: [ot_resource_route: :ignore]}
iex> OpentelemetryHTTPoison.request(request)
"""
def request(%Request{options: opts} = request) do
save_parent_ctx()
span_name = Keyword.get_lazy(opts, :ot_span_name, fn -> default_span_name(request) end)
%URI{host: host} = request.url |> process_request_url() |> URI.parse()
resource_route_attribute =
opts
|> Keyword.get(:ot_resource_route, :unset)
|> get_resource_route(request)
|> case do
resource_route when is_binary(resource_route) ->
[{@http_route, resource_route}]
nil ->
[]
end
ot_attributes =
get_standard_ot_attributes(request, host) ++
get_ot_attributes(opts) ++
resource_route_attribute
request_ctx = Tracer.start_span(span_name, %{kind: :client, attributes: ot_attributes})
Tracer.set_current_span(request_ctx)
result = super(request)
if Tracer.current_span_ctx() == request_ctx do
case result do
{:error, %{reason: reason}} ->
Tracer.set_status(:error, inspect(reason))
end_span()
_ ->
:ok
end
end
result
end
def process_response_status_code(status_code) do
# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status
if status_code >= 400 do
Tracer.set_status(:error, "")
end
Tracer.set_attribute(@http_status_code, status_code)
end_span()
status_code
end
defp end_span do
Tracer.end_span()
restore_parent_ctx()
end
# see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#name
defp default_span_name(request), do: request.method |> Atom.to_string() |> String.upcase()
@ctx_key {__MODULE__, :parent_ctx}
defp save_parent_ctx do
ctx = Tracer.current_span_ctx()
Process.put(@ctx_key, ctx)
end
defp restore_parent_ctx do
ctx = Process.get(@ctx_key, :undefined)
Process.delete(@ctx_key)
Tracer.set_current_span(ctx)
end
defp get_standard_ot_attributes(request, host) do
[
{@http_method,
request.method
|> Atom.to_string()
|> String.upcase()},
{@http_url, strip_uri_credentials(request.url)},
{@net_peer_name, host}
]
end
defp get_ot_attributes(opts) do
default_ot_attributes = Configuration.get(:ot_attributes)
default_ot_attributes
|> Enum.concat(Keyword.get(opts, :ot_attributes, []))
|> Enum.reduce(%{}, fn {key, value}, acc -> Map.put(acc, key, value) end)
|> Enum.into([], fn {key, value} -> {key, value} end)
end
defp get_resource_route(option, request)
defp get_resource_route(route, _) when is_binary(route), do: route
defp get_resource_route(infer_fn, request) when is_function(infer_fn, 1), do: infer_fn.(request)
defp get_resource_route(:infer, request), do: Configuration.get(:infer_route).(request)
defp get_resource_route(:ignore, _), do: nil
defp get_resource_route(:unset, _), do: nil
defp get_resource_route(_unknown_option, _),
do:
raise(
ArgumentError,
"The :ot_resource_route keyword option value must either be a binary, a function with an arity of 1 or the :infer or :ignore atom"
)
defp strip_uri_credentials(uri) do
uri |> URI.parse() |> Map.put(:userinfo, nil) |> Map.put(:authority, nil) |> URI.to_string()
end
end

View File

@ -0,0 +1,41 @@
defmodule OpentelemetryHTTPoison.URI do
@moduledoc """
Exposes a function to normalise URIs in a format suitable for usage as Open Telemetry metadata.
"""
alias HTTPoison.Request
@default_route "/"
@spec infer_route_from_request(Request.t()) :: binary()
@doc """
Infers the route of the provided `HTTPoison.Request`, returned in a format suitable for usage as Open Telemetry metadata.
"""
def infer_route_from_request(%Request{url: url}) when is_binary(url),
do: infer_route_from_url(url)
def infer_route_from_request(%Request{}), do: @default_route
@spec infer_route_from_url(binary()) :: binary()
defp infer_route_from_url(url) when is_binary(url) do
url
|> URI.parse()
|> Map.get(:path, "/")
|> case do
nil ->
@default_route
value ->
case String.split(value, "/", parts: 2, trim: true) do
[] ->
@default_route
[path] ->
"/#{path}"
[path, _] ->
"/#{path}/:subpath"
end
end
end
end

View File

@ -0,0 +1,88 @@
defmodule OpentelemetryHTTPoison.MixProject do
use Mix.Project
@source_url "https://github.com/open-telemetry/opentelemetry-erlang-contrib/tree/main/instrumentation/opentelemetry_httpoison"
@version "1.3.0"
def project do
[
app: :opentelemetry_httpoison,
version: @version,
elixir: "~> 1.11",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
deps: deps(),
docs: docs(),
package: package(),
aliases: aliases()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:httpoison, "~> 1.6 or ~> 2.0"},
{:opentelemetry_api, "~> 1.0"},
{:opentelemetry_semantic_conventions, "~> 0.2"}
] ++ dev_deps()
end
def dev_deps,
do: [
{:opentelemetry, "~> 1.0", only: :test},
{:opentelemetry_exporter, "~> 1.0", only: :test},
{:plug, "~> 1.12", only: :test},
{:plug_cowboy, "~> 2.2", only: :test},
{:credo, "~> 1.6", only: [:dev, :test]},
{:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false},
{:ex_doc, ">= 0.25.3", only: :dev, runtime: false}
]
defp package do
[
name: "opentelemetry_httpoison",
description:
"OpentelemetryHTTPoison is a opentelemetry-instrumented wrapper around HTTPoison",
licenses: ["Apache-2.0"],
links: %{
"GitHub" => @source_url,
"OpenTelemetry Erlang" => "https://github.com/open-telemetry/opentelemetry-erlang",
"OpenTelemetry Erlang Contrib" =>
"https://github.com/open-telemetry/opentelemetry-erlang-contrib",
"OpenTelemetry.io" => "https://opentelemetry.io"
}
]
end
defp aliases do
[
"format.all": [
"format mix.exs \"lib/**/*.{ex,exs}\" \"test/**/*.{ex,exs}\" \"priv/**/*.{ex,exs}\" \"config/**/*.{ex,exs}\""
]
]
end
defp elixirc_paths(:test), do: ["lib", "test"]
defp elixirc_paths(:dev), do: ["lib", "test"]
defp elixirc_paths(_), do: ["lib"]
defp docs do
[
extras: [
"LICENSE.md": [title: "License"],
"README.md": [title: "Overview"]
],
main: "readme",
source_url: @source_url,
source_ref: "v#{@version}",
formatters: ["html"]
]
end
end

View File

@ -0,0 +1,43 @@
%{
"acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"chatterbox": {:hex, :ts_chatterbox, "0.13.0", "6f059d97bcaa758b8ea6fffe2b3b81362bd06b639d3ea2bb088335511d691ebf", [:rebar3], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "b93d19104d86af0b3f2566c4cba2a57d2e06d103728246ba1ac6c3c0ff010aa7"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"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.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
"ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"},
"dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"},
"earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"gproc": {:hex, :gproc, "0.8.0", "cea02c578589c61e5341fce149ea36ccef236cc2ecac8691fba408e7ea77ec2f", [:rebar3], [], "hexpm", "580adafa56463b75263ef5a5df4c86af321f68694e7786cb057fd805d1e2a7de"},
"grpcbox": {:hex, :grpcbox, "0.16.0", "b83f37c62d6eeca347b77f9b1ec7e9f62231690cdfeb3a31be07cd4002ba9c82", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.13.0", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.8.0", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "294df743ae20a7e030889f00644001370a4f7ce0121f3bbdaf13cf3169c62913"},
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm", "06f580167c4b8b8a6429040df36cc93bba6d571faeaec1b28816523379cbb23a"},
"httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.0", "9e18a119d9efc3370a3ef2a937bf0b24c088d9c4bf0ba9d7c3751d49d347d035", [:mix], [], "hexpm", "7977f183127a7cbe9346981e2f480dc04c55ffddaef746bd58debd566070eef8"},
"opentelemetry": {:hex, :opentelemetry, "1.3.0", "988ac3c26acac9720a1d4fb8d9dc52e95b45ecfec2d5b5583276a09e8936bc5e", [:rebar3], [{:opentelemetry_api, "~> 1.2.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "8e09edc26aad11161509d7ecad854a3285d88580f93b63b0b1cf0bac332bfcc0"},
"opentelemetry_api": {:hex, :opentelemetry_api, "1.2.1", "7b69ed4f40025c005de0b74fce8c0549625d59cb4df12d15c32fe6dc5076ff42", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "6d7a27b7cad2ad69a09cabf6670514cafcec717c8441beb5c96322bac3d05350"},
"opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.4.1", "5c80c3a22ec084b4e0a9ac7d39a435b332949b2dceec9fb19f5c5d2ca8ae1d56", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.3", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "5a0ff6618b0f7370bd10b50e64099a4c2aa52145ae6567cccf7d76ba2d32e079"},
"opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [: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", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"tls_certificate_check": {:hex, :tls_certificate_check, "1.18.0", "75699bc855ea18de358e3024abd73384691320bb7a0c98ac90a74475311c1ae3", [:rebar3], [{:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "8de62ff34e59317211567e3b1ae70866df195895b051193c21abc381276d395b"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}

View File

@ -0,0 +1,32 @@
defmodule ConfigurationTest do
alias OpentelemetryHTTPoison.Configuration
use OpentelemetryHTTPoison.Case
test "it does not crash on `get` if it has not been setup" do
assert Configuration.get(:infer_route)
assert Configuration.get(:ot_attributes)
end
test "if :ot_attributes are not a list it raises an error" do
assert_raise ArgumentError, fn -> Configuration.setup(ot_attributes: {:key, :value}) end
end
test "if :ot_attributes are a list it sets up successfully" do
assert Configuration.setup(ot_attributes: [{:key, :value}]) == nil
end
test "if :infer_route is not a function it raises an error" do
assert_raise ArgumentError, fn -> Configuration.setup(infer_route: :not_a_function) end
end
test "if :infer_route is a function of arity other than 1 it raises an error" do
assert_raise ArgumentError, fn ->
Configuration.setup(infer_route: fn x, y, z -> {x, y, z} end)
end
end
test "if :infer_route is a function of arity 1 it sets up successfully" do
assert Configuration.setup(infer_route: fn x -> x end) == nil
end
end

View File

@ -0,0 +1,337 @@
defmodule OpentelemetryHTTPoisonTest do
alias OpentelemetryHTTPoison
alias OpenTelemetry.Tracer
use OpentelemetryHTTPoison.Case, async: false
doctest OpentelemetryHTTPoison
require OpenTelemetry.Tracer
require Record
for {name, spec} <- Record.extract_all(from_lib: "opentelemetry/include/otel_span.hrl") do
Record.defrecord(name, spec)
end
setup do
flush_mailbox()
:otel_simple_processor.set_exporter(:otel_exporter_pid, self())
:ok
end
describe "OpentelemetryHTTPoison default attributes and headers" do
test "standard http client span attribute are set in span" do
OpentelemetryHTTPoison.get!("http://localhost:8000")
assert_receive {:span, span(attributes: attributes_record, name: "GET")}
attributes = elem(attributes_record, 4)
assert ["http.method", "http.status_code", "http.url", "net.peer.name"] ==
attributes |> Map.keys() |> Enum.sort()
assert {"http.method", "GET"} in attributes
assert {"net.peer.name", "localhost"} in attributes
end
test "traceparent header is injected when no headers" do
%HTTPoison.Response{request: %{headers: headers}} =
OpentelemetryHTTPoison.get!("http://localhost:8000")
assert "traceparent" in Enum.map(headers, &elem(&1, 0))
end
test "traceparent header is injected when list headers" do
%HTTPoison.Response{request: %{headers: headers}} =
OpentelemetryHTTPoison.get!("http://localhost:8000", [{"Accept", "application/json"}])
assert "traceparent" in Enum.map(headers, &elem(&1, 0))
end
test "traceparent header is injected to user-supplied map headers" do
%HTTPoison.Response{request: %{headers: headers}} =
OpentelemetryHTTPoison.get!("http://localhost:8000", %{"Accept" => "application/json"})
assert "traceparent" in Enum.map(headers, &elem(&1, 0))
end
test "traceparent header is injected to atom user-supplied map headers" do
%HTTPoison.Response{request: %{headers: headers}} =
OpentelemetryHTTPoison.get!("http://localhost:8000", %{atom: "value"})
assert "atom" in Enum.map(headers, &elem(&1, 0))
end
test "http.url doesn't contain credentials" do
OpentelemetryHTTPoison.get!("http://user:pass@localhost:8000/user/edit/24")
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"http.url", "http://localhost:8000/user/edit/24"})
end
end
describe "OpentelemetryHTTPoison calls with additional options" do
test "additional span attributes can be passed to OpentelemetryHTTPoison invocation" do
OpentelemetryHTTPoison.get!("http://localhost:8000", [],
ot_attributes: [{"app.callname", "mariorossi"}]
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"app.callname", "mariorossi"})
end
test "resource route can be explicitly passed to OpentelemetryHTTPoison invocation as a string" do
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_resource_route: "/user/edit"
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"http.route", "/user/edit"})
end
test "resource route can be explicitly passed to OpentelemetryHTTPoison invocation as a function" do
infer_fn = fn request -> URI.parse(request.url).path end
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_resource_route: infer_fn
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"http.route", "/user/edit/24"})
end
test "resource route inference can be explicitly ignored" do
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_resource_route: :ignore
)
assert_receive {:span, span(attributes: attributes)}, 1000
refute confirm_http_route_attribute(attributes)
end
test "resource route inference can be implicitly ignored" do
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24")
assert_receive {:span, span(attributes: attributes)}, 1000
refute confirm_http_route_attribute(attributes)
end
test "resource route inference fails if an incorrect value is passed to the OpentelemetryHTTPoison invocation" do
assert_raise(ArgumentError, fn ->
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_resource_route: nil
)
end)
assert_raise(ArgumentError, fn ->
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [], ot_resource_route: 1)
end)
end
test "resource route and attributes can be passed to OpentelemetryHTTPoison as options together" do
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_resource_route: "/user/edit",
ot_attributes: [{"app.callname", "mariorossi"}]
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"http.route", "/user/edit"})
assert confirm_attributes(attributes, {"app.callname", "mariorossi"})
end
end
describe "parent span is not affected" do
test "with a successful request" do
Tracer.with_span "parent" do
pre_request_ctx = Tracer.current_span_ctx()
OpentelemetryHTTPoison.get("http://localhost:8000")
post_request_ctx = Tracer.current_span_ctx()
assert post_request_ctx == pre_request_ctx
end
end
test "with an nxdomain request" do
Tracer.with_span "parent" do
pre_request_ctx = Tracer.current_span_ctx()
OpentelemetryHTTPoison.get("http://domain.invalid:8000")
post_request_ctx = Tracer.current_span_ctx()
assert post_request_ctx == pre_request_ctx
end
end
end
describe "span_status is set to error for" do
test "status codes >= 400" do
OpentelemetryHTTPoison.get!("http://localhost:8000/status/400")
assert_receive {:span, span(status: {:status, :error, ""})}
end
test "HTTP econnrefused errors" do
{:error, %HTTPoison.Error{reason: expected_reason}} =
OpentelemetryHTTPoison.get("http://localhost:8001")
assert_receive {:span, span(status: {:status, :error, recorded_reason})}
assert inspect(expected_reason) == recorded_reason
end
test "HTTP nxdomain errors" do
{:error, %HTTPoison.Error{reason: expected_reason}} =
OpentelemetryHTTPoison.get("http://domain.invalid:8001")
assert_receive {:span, span(status: {:status, :error, recorded_reason})}
assert inspect(expected_reason) == recorded_reason
end
test "HTTP tls errors" do
{:error, %HTTPoison.Error{reason: expected_reason}} =
OpentelemetryHTTPoison.get("https://localhost:8000")
assert_receive {:span, span(status: {:status, :error, recorded_reason})}
assert inspect(expected_reason) == recorded_reason
end
end
describe "OpentelemetryHTTPoison with additional configuration" do
test "default attributes can be set via a two element tuple list" do
set_env(:ot_attributes, [{"test_attribute", "test"}])
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24")
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"test_attribute", "test"})
end
test "default attributes that are not binary will be ignored" do
set_env(:ot_attributes, [
{"test_attribute", "test"},
{1, "ignored"},
{:ignored, "ignored_too"}
])
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24")
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"test_attribute", "test"})
end
test "default attributes can be overridden via a two element tuple list passed to the OpentelemetryHTTPoison invocation" do
set_env(:ot_attributes, [{"test_attribute", "test"}])
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_attributes: [{"test_attribute", "overridden"}]
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"test_attribute", "overridden"})
end
test "default attributes can be combined with attributes passed to the OpentelemetryHTTPoison invocation" do
set_env(:ot_attributes, [{"test_attribute", "test"}])
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_attributes: [
{"another_test_attribute", "another test"},
{"test_attribute_overridden", "overridden"}
]
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"test_attribute", "test"})
assert confirm_attributes(attributes, {"another_test_attribute", "another test"})
assert confirm_attributes(attributes, {"test_attribute_overridden", "overridden"})
end
test "resource route can be implicitly inferred by OpentelemetryHTTPoison invocation using a default function" do
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_resource_route: :infer
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_http_route_attribute(attributes, "/user/:subpath")
end
test "resource route can be inferred by OpentelemetryHTTPoison invocation via a configured function" do
infer_fn = fn
%HTTPoison.Request{} = request -> URI.parse(request.url).path
end
set_env(:infer_route, infer_fn)
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_resource_route: :infer
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_http_route_attribute(attributes, "/user/edit/24")
end
test "implicit resource route inference can be overridden with a function passed to the OpentelemetryHTTPoison invocation" do
infer_fn = fn
%HTTPoison.Request{} = request -> URI.parse(request.url).path
end
invocation_infer_fn = fn _ -> "test" end
set_env(:infer_route, infer_fn)
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_resource_route: invocation_infer_fn
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_http_route_attribute(attributes, "test")
end
test "implicit resource route inference can be overridden with a string passed to the OpentelemetryHTTPoison invocation" do
infer_fn = fn
%HTTPoison.Request{} = request -> URI.parse(request.url).path
end
set_env(:infer_route, infer_fn)
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_resource_route: "test"
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_http_route_attribute(attributes, "test")
end
end
test "OpentelemetryHTTPoison works if setup is not called" do
OpentelemetryHTTPoison.get!("http://localhost:8000/user/edit/24", [],
ot_attributes: [{"some_attribute", "some value"}]
)
assert_receive {:span, span(attributes: attributes)}, 1000
assert confirm_attributes(attributes, {"some_attribute", "some value"})
end
def flush_mailbox do
receive do
_ -> flush_mailbox()
after
10 -> :ok
end
end
defp confirm_attributes(attributes, attributes_to_confirm) do
attributes
|> Tuple.to_list()
|> Enum.filter(&is_map/1)
|> Enum.any?(fn map ->
attributes_to_confirm in map
end)
end
defp confirm_http_route_attribute(attributes, value) do
confirm_attributes(attributes, {"http.route", value})
end
defp confirm_http_route_attribute(attributes) do
confirm_http_route_attribute(attributes, "")
end
end

View File

@ -0,0 +1,18 @@
defmodule OpentelemetryHTTPoison.Case do
@moduledoc false
use ExUnit.CaseTemplate
setup do
on_exit(fn ->
Application.delete_env(:opentelemetry_httpoison, :infer_route)
Application.delete_env(:opentelemetry_httpoison, :ot_attributes)
end)
end
using do
quote do
defp set_env(key, value), do: Application.put_env(:opentelemetry_httpoison, key, value)
end
end
end

View File

@ -0,0 +1,22 @@
Application.ensure_all_started(:opentelemetry)
Application.ensure_all_started(:opentelemetry_api)
defmodule TestServer do
use Plug.Router
plug(:match)
plug(:dispatch)
match "/status/:status_code" do
send_resp(conn, String.to_integer(status_code), "Here's that status code you ordered!")
end
match _ do
send_resp(conn, 200, "It's polite to reply!")
end
end
child_spec = [{Plug.Cowboy, scheme: :http, plug: TestServer, options: [port: 8000]}]
{:ok, _pid} = Supervisor.start_link(child_spec, strategy: :one_for_one)
ExUnit.start()

View File

@ -0,0 +1,50 @@
defmodule OpentelemetryHTTPoison.URITest do
@moduledoc """
Tests for `OpentelemetryHTTPoison.URI`
"""
alias HTTPoison.Request
use ExUnit.Case
alias OpentelemetryHTTPoison.URI, as: UtilsURI
@base_uri "https://www.test.com"
describe "infer_route_from_request/1" do
test "Request URL consisiting of whitespace is inferred as a route of '/'" do
request = %Request{url: ""}
result = UtilsURI.infer_route_from_request(request)
assert result == "/"
end
test "Request URL '#{@base_uri}/user/edit/24' is inferred as a route of '/user/:subpath'" do
url = "#{@base_uri}/user/edit/24"
request = %Request{url: url}
result = UtilsURI.infer_route_from_request(request)
assert result == "/user/:subpath"
end
test "Request URL '#{@base_uri}/user/24' is inferred as a route of '/user/:subpath'" do
url = "#{@base_uri}/user/24"
request = %Request{url: url}
result = UtilsURI.infer_route_from_request(request)
assert result == "/user/:subpath"
end
test "Request URL #{@base_uri}/'user' is inferred as route of '/user'" do
url = "#{@base_uri}/user"
request = %Request{url: url}
result = UtilsURI.infer_route_from_request(request)
assert result == "/user"
end
end
end