|
| 1 | +defmodule OpentelemetryTesla do |
| 2 | + require OpenTelemetry.SemanticConventions.Trace, as: Trace |
| 3 | + |
| 4 | + alias OpenTelemetry.Span |
| 5 | + alias OpenTelemetry.Tracer |
| 6 | + |
| 7 | + @tracer_id __MODULE__ |
| 8 | + |
| 9 | + def setup(_opts \\ []) do |
| 10 | + :telemetry.attach( |
| 11 | + {__MODULE__, :request_start}, |
| 12 | + [:tesla, :request, :start], |
| 13 | + &__MODULE__.handle_request_start/4, |
| 14 | + %{} |
| 15 | + ) |
| 16 | + |
| 17 | + :telemetry.attach( |
| 18 | + {__MODULE__, :request_stop}, |
| 19 | + [:tesla, :request, :stop], |
| 20 | + &__MODULE__.handle_request_stop/4, |
| 21 | + %{} |
| 22 | + ) |
| 23 | + |
| 24 | + :telemetry.attach( |
| 25 | + {__MODULE__, :request_exception}, |
| 26 | + [:tesla, :request, :exception], |
| 27 | + &__MODULE__.handle_request_exception/4, |
| 28 | + %{} |
| 29 | + ) |
| 30 | + end |
| 31 | + |
| 32 | + @doc false |
| 33 | + def handle_request_start(_event, measurements, metadata, config) do |
| 34 | + url = Tesla.build_url(metadata.env.url, metadata.env.query) |
| 35 | + uri = URI.parse(url) |
| 36 | + method = String.upcase(Atom.to_string(metadata.env.method)) |
| 37 | + |
| 38 | + OpentelemetryTelemetry.start_telemetry_span(@tracer_id, "HTTP #{method}", metadata, %{ |
| 39 | + start_time: measurements.system_time, |
| 40 | + kind: :client, |
| 41 | + attributes: %{ |
| 42 | + Trace.http_method() => method, |
| 43 | + Trace.http_scheme() => uri.scheme, |
| 44 | + Trace.net_host_name() => uri.host, |
| 45 | + Trace.net_peer_name() => uri.host, |
| 46 | + Trace.net_peer_port() => uri.port, |
| 47 | + Trace.http_target() => uri.path, |
| 48 | + Trace.http_url() => sanitize_url(uri), |
| 49 | + Trace.http_request_content_length() => get_content_length(metadata.env) |
| 50 | + # TODO: Add retry count in Tesla.MIddleware.Telemetry |
| 51 | + # reading from Tesla.Middleware.Retry |
| 52 | + # Trace.http_retry_count() => nil |
| 53 | + } |
| 54 | + }) |
| 55 | + end |
| 56 | + |
| 57 | + @doc false |
| 58 | + def handle_request_stop(_event, measurements, metadata, _config) do |
| 59 | + OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, metadata) |
| 60 | + |
| 61 | + status = if metadata.env.status >= 400, do: :error, else: :ok |
| 62 | + |
| 63 | + Tracer.set_status(status) |
| 64 | + |
| 65 | + Tracer.set_attributes(%{ |
| 66 | + :duration => measurements.duration, |
| 67 | + Trace.http_status_code() => metadata.env.status, |
| 68 | + Trace.http_response_content_length() => get_content_length(metadata.env) |
| 69 | + }) |
| 70 | + |
| 71 | + OpentelemetryTelemetry.end_telemetry_span(@tracer_id, metadata) |
| 72 | + end |
| 73 | + |
| 74 | + @doc false |
| 75 | + def handle_request_exception(_event, measurements, metadata, _config) do |
| 76 | + ctx = OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, metadata) |
| 77 | + status = OpenTelemetry.status(:error, inspect(metadata.reason)) |
| 78 | + |
| 79 | + Tracer.set_status(status) |
| 80 | + |
| 81 | + Span.record_exception( |
| 82 | + ctx, |
| 83 | + metadata.kind, |
| 84 | + metadata.stacktrace, |
| 85 | + %{duration: measurements.duration} |
| 86 | + ) |
| 87 | + |
| 88 | + OpentelemetryTelemetry.end_telemetry_span(@tracer_id, metadata) |
| 89 | + end |
| 90 | + |
| 91 | + defp sanitize_url(uri) do |
| 92 | + %{uri | userinfo: nil} |
| 93 | + |> URI.to_string() |
| 94 | + end |
| 95 | + |
| 96 | + defp get_content_length(env) do |
| 97 | + case Enum.find(env.headers, fn {k, _v} -> k == "content-length" end) do |
| 98 | + nil -> |
| 99 | + body_byte_size(env.body) |
| 100 | + |
| 101 | + {_key, value} -> |
| 102 | + value |
| 103 | + end |
| 104 | + end |
| 105 | + |
| 106 | + defp body_byte_size(nil), do: 0 |
| 107 | + defp body_byte_size(body) when is_binary(body), do: byte_size(body) |
| 108 | + defp body_byte_size(_body), do: nil |
| 109 | +end |
0 commit comments