2022-08-25 18:47:59 +00:00
|
|
|
defmodule Tesla.Middleware.OpenTelemetry do
|
2022-10-04 22:32:36 +00:00
|
|
|
@moduledoc """
|
|
|
|
Creates OpenTelemetry spans and injects tracing headers into HTTP requests
|
|
|
|
|
|
|
|
When used with `Tesla.Middleware.PathParams`, the span name will be created
|
|
|
|
based on the provided path. Without it, the span name follow OpenTelemetry
|
|
|
|
standards and use just the method name, if not being overriden by opts.
|
|
|
|
|
|
|
|
NOTE: This middleware needs to come before `Tesla.Middleware.PathParams`
|
|
|
|
|
|
|
|
## Options
|
|
|
|
|
|
|
|
- `:span_name` - override span name. Can be a `String` for a static span name,
|
|
|
|
or a function that takes the `Tesla.Env` and returns a `String`
|
|
|
|
|
|
|
|
"""
|
2022-11-15 23:39:22 +00:00
|
|
|
|
|
|
|
alias OpenTelemetry.SemanticConventions.Trace
|
|
|
|
|
2022-08-25 18:47:59 +00:00
|
|
|
require OpenTelemetry.Tracer
|
2022-11-15 23:39:22 +00:00
|
|
|
require Trace
|
|
|
|
|
2022-08-25 18:47:59 +00:00
|
|
|
@behaviour Tesla.Middleware
|
|
|
|
|
2022-10-04 22:32:36 +00:00
|
|
|
def call(env, next, opts) do
|
|
|
|
span_name = get_span_name(env, Keyword.get(opts, :span_name))
|
2022-08-25 18:47:59 +00:00
|
|
|
|
|
|
|
OpenTelemetry.Tracer.with_span span_name, %{kind: :client} do
|
|
|
|
env
|
|
|
|
|> Tesla.put_headers(:otel_propagator_text_map.inject([]))
|
|
|
|
|> Tesla.run(next)
|
|
|
|
|> set_span_attributes()
|
|
|
|
|> handle_result()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-04 22:32:36 +00:00
|
|
|
defp get_span_name(_env, span_name) when is_binary(span_name) do
|
|
|
|
span_name
|
|
|
|
end
|
|
|
|
|
|
|
|
defp get_span_name(env, span_name_fun) when is_function(span_name_fun, 1) do
|
|
|
|
span_name_fun.(env)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp get_span_name(env, _) do
|
2022-08-25 18:47:59 +00:00
|
|
|
case env.opts[:path_params] do
|
|
|
|
nil -> "HTTP #{http_method(env.method)}"
|
|
|
|
_ -> URI.parse(env.url).path
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp set_span_attributes({_, %Tesla.Env{} = env} = result) do
|
|
|
|
OpenTelemetry.Tracer.set_attributes(build_attrs(env))
|
|
|
|
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
defp set_span_attributes(result) do
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
defp handle_result({:ok, %Tesla.Env{status: status} = env}) when status > 400 do
|
|
|
|
OpenTelemetry.Tracer.set_status(OpenTelemetry.status(:error, ""))
|
|
|
|
|
|
|
|
{:ok, env}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp handle_result({:error, {Tesla.Middleware.FollowRedirects, :too_many_redirects}} = result) do
|
|
|
|
OpenTelemetry.Tracer.set_status(OpenTelemetry.status(:error, ""))
|
|
|
|
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
defp handle_result({:ok, env}) do
|
|
|
|
{:ok, env}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp handle_result(result) do
|
|
|
|
OpenTelemetry.Tracer.set_status(OpenTelemetry.status(:error, ""))
|
|
|
|
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
defp build_attrs(%Tesla.Env{
|
|
|
|
method: method,
|
|
|
|
url: url,
|
|
|
|
status: status_code,
|
|
|
|
headers: headers,
|
|
|
|
query: query
|
|
|
|
}) do
|
|
|
|
url = Tesla.build_url(url, query)
|
|
|
|
uri = URI.parse(url)
|
|
|
|
|
|
|
|
attrs = %{
|
2022-11-15 23:39:22 +00:00
|
|
|
Trace.http_method() => http_method(method),
|
|
|
|
Trace.http_url() => url,
|
|
|
|
Trace.http_target() => uri.path,
|
|
|
|
Trace.net_host_name() => uri.host,
|
|
|
|
Trace.http_scheme() => uri.scheme,
|
|
|
|
Trace.http_status_code() => status_code
|
2022-08-25 18:47:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
maybe_append_content_length(attrs, headers)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp maybe_append_content_length(attrs, headers) do
|
|
|
|
case Enum.find(headers, fn {k, _v} -> k == "content-length" end) do
|
|
|
|
nil ->
|
|
|
|
attrs
|
|
|
|
|
|
|
|
{_key, content_length} ->
|
2022-11-15 23:39:22 +00:00
|
|
|
Map.put(attrs, Trace.http_response_content_length(), content_length)
|
2022-08-25 18:47:59 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp http_method(method) do
|
|
|
|
method
|
|
|
|
|> Atom.to_string()
|
|
|
|
|> String.upcase()
|
|
|
|
end
|
|
|
|
end
|