Add telepoison (#148)
* add telepoison * rename * rename file too, duh * rename test file * port updates * add standard workflow
This commit is contained in:
parent
901b571b07
commit
0cc8c760d0
|
@ -463,3 +463,40 @@ jobs:
|
||||||
run: mix format --check-formatted
|
run: mix format --check-formatted
|
||||||
- name: Test
|
- name: Test
|
||||||
run: mix 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
|
||||||
|
|
|
@ -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`.
|
||||||
|
#
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
]
|
|
@ -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.
|
|
@ -0,0 +1,7 @@
|
||||||
|
import Config
|
||||||
|
|
||||||
|
try do
|
||||||
|
import_config "#{config_env()}.exs"
|
||||||
|
rescue
|
||||||
|
_ -> :ok
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Config
|
||||||
|
|
||||||
|
config :opentelemetry,
|
||||||
|
traces_exporter: :none,
|
||||||
|
processors: [{:otel_simple_processor, %{}}]
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"},
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
Loading…
Reference in New Issue