opentelemetry-erlang-contrib/instrumentation/opentelemetry_tesla/lib/middleware/opentelemetry_tesla_middlew...

122 lines
3.0 KiB
Elixir

defmodule Tesla.Middleware.OpenTelemetry do
@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`
"""
alias OpenTelemetry.SemanticConventions.Trace
require OpenTelemetry.Tracer
require Trace
@behaviour Tesla.Middleware
def call(env, next, opts) do
span_name = get_span_name(env, Keyword.get(opts, :span_name))
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
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
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 = %{
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
}
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} ->
Map.put(attrs, Trace.http_response_content_length(), content_length)
end
end
defp http_method(method) do
method
|> Atom.to_string()
|> String.upcase()
end
end