2024-02-18 22:36:01 +00:00
|
|
|
defmodule OpentelemetryBandit do
|
|
|
|
@moduledoc """
|
|
|
|
OpentelemetryBandit uses [telemetry](https://hexdocs.pm/telemetry/) handlers to create `OpenTelemetry` spans.
|
|
|
|
|
|
|
|
Supported:
|
2024-04-24 01:14:29 +00:00
|
|
|
1. :bandit, :request, :start
|
|
|
|
2. :bandit, :request, :stop
|
|
|
|
3. :bandit, :request, :exception
|
|
|
|
4. :bandit, :websocket, :start
|
|
|
|
5. :bandit, :websocket, :stop
|
2024-02-18 22:36:01 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
alias OpenTelemetry.SemanticConventions.Trace
|
|
|
|
require Trace
|
|
|
|
require OpenTelemetry.Tracer
|
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
@tracer_id __MODULE__
|
|
|
|
|
2024-02-18 22:36:01 +00:00
|
|
|
@doc """
|
|
|
|
Initializes and configures the telemetry handlers.
|
|
|
|
"""
|
|
|
|
@spec setup(any) :: :ok
|
|
|
|
def setup(_opts \\ []) do
|
2024-04-24 01:14:29 +00:00
|
|
|
:telemetry.attach(
|
|
|
|
{__MODULE__, :request_start},
|
|
|
|
[:bandit, :request, :start],
|
|
|
|
&__MODULE__.handle_request_start/4,
|
|
|
|
%{}
|
|
|
|
)
|
|
|
|
|
2024-02-18 22:36:01 +00:00
|
|
|
:telemetry.attach(
|
|
|
|
{__MODULE__, :request_stop},
|
|
|
|
[:bandit, :request, :stop],
|
|
|
|
&__MODULE__.handle_request_stop/4,
|
|
|
|
%{}
|
|
|
|
)
|
|
|
|
|
|
|
|
:telemetry.attach(
|
|
|
|
{__MODULE__, :request_exception},
|
|
|
|
[:bandit, :request, :exception],
|
|
|
|
&__MODULE__.handle_request_exception/4,
|
|
|
|
%{}
|
|
|
|
)
|
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
:telemetry.attach(
|
|
|
|
{__MODULE__, :websocket_start},
|
|
|
|
[:bandit, :websocket, :start],
|
|
|
|
&__MODULE__.handle_websocket_start/4,
|
|
|
|
%{}
|
|
|
|
)
|
|
|
|
|
2024-02-18 22:36:01 +00:00
|
|
|
:telemetry.attach(
|
|
|
|
{__MODULE__, :websocket_stop},
|
|
|
|
[:bandit, :websocket, :stop],
|
|
|
|
&__MODULE__.handle_websocket_stop/4,
|
|
|
|
%{}
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
def handle_request_start(_event, measurements, meta, _config) do
|
2024-02-18 22:36:01 +00:00
|
|
|
conn = Map.get(meta, :conn)
|
|
|
|
|
|
|
|
url = extract_url(meta, conn)
|
|
|
|
request_path = extract_request_path(meta, conn)
|
|
|
|
|
|
|
|
attributes =
|
|
|
|
if Map.has_key?(meta, :error) do
|
|
|
|
%{
|
|
|
|
Trace.http_url() => url,
|
2024-04-24 01:14:29 +00:00
|
|
|
Trace.http_method() => Map.get(conn, :method),
|
|
|
|
Trace.net_transport() => :"IP.TCP"
|
2024-02-18 22:36:01 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
%{
|
|
|
|
Trace.http_url() => url,
|
|
|
|
Trace.http_client_ip() => client_ip(conn),
|
|
|
|
Trace.http_scheme() => conn.scheme,
|
|
|
|
Trace.net_peer_name() => conn.host,
|
|
|
|
Trace.net_peer_port() => conn.port,
|
|
|
|
Trace.http_target() => conn.request_path,
|
2024-04-23 17:06:22 +00:00
|
|
|
Trace.http_method() => conn.method,
|
2024-02-18 22:36:01 +00:00
|
|
|
Trace.net_transport() => :"IP.TCP",
|
|
|
|
Trace.http_user_agent() => user_agent(conn)
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
span_kind = if Map.has_key?(meta, :error), do: :error, else: :server
|
|
|
|
|
2024-04-23 17:06:22 +00:00
|
|
|
span_id = "HTTP #{conn.method} #{request_path}" |> String.trim()
|
2024-02-18 22:36:01 +00:00
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
OpentelemetryTelemetry.start_telemetry_span(@tracer_id, span_id, meta, %{
|
2024-02-18 22:36:01 +00:00
|
|
|
attributes: attributes,
|
2024-04-24 01:14:29 +00:00
|
|
|
start_time: measurements.monotonic_time,
|
2024-02-18 22:36:01 +00:00
|
|
|
kind: span_kind
|
|
|
|
})
|
2024-04-24 01:14:29 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def handle_request_stop(_event, measurements, meta, _config) do
|
|
|
|
conn = Map.get(meta, :conn)
|
|
|
|
|
|
|
|
OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, meta)
|
|
|
|
|
|
|
|
attributes =
|
|
|
|
if Map.has_key?(meta, :error) do
|
|
|
|
%{}
|
|
|
|
else
|
|
|
|
%{
|
|
|
|
Trace.http_status_code() => Map.get(conn, :status),
|
|
|
|
Trace.http_response_content_length() => Map.get(measurements, :resp_body_bytes)
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
attributes = Map.put(attributes, :duration, measurements.duration)
|
|
|
|
|
|
|
|
OpenTelemetry.Tracer.set_attributes(attributes)
|
|
|
|
|
|
|
|
if Map.get(attributes, Trace.http_status_code()) >= 500 do
|
|
|
|
OpenTelemetry.Tracer.set_status(OpenTelemetry.status(:error))
|
|
|
|
end
|
2024-02-18 22:36:01 +00:00
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
OpentelemetryTelemetry.end_telemetry_span(@tracer_id, meta)
|
2024-02-18 22:36:01 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def handle_request_exception(_event, _measurements, meta, _config) do
|
2024-04-24 01:14:29 +00:00
|
|
|
OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, meta)
|
2024-02-18 22:36:01 +00:00
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
OpenTelemetry.Tracer.set_status(OpenTelemetry.status(:error))
|
|
|
|
OpenTelemetry.Tracer.record_exception(meta.exception, meta.stacktrace)
|
2024-02-18 22:36:01 +00:00
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
OpentelemetryTelemetry.end_telemetry_span(@tracer_id, meta)
|
|
|
|
end
|
2024-02-18 22:36:01 +00:00
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
def handle_websocket_start(_event, measurements, meta, _config) do
|
2024-02-18 22:36:01 +00:00
|
|
|
attributes = %{
|
|
|
|
Trace.net_transport() => :websocket
|
|
|
|
}
|
|
|
|
|
|
|
|
span_kind = if Map.has_key?(meta, :error), do: :error, else: :server
|
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
OpentelemetryTelemetry.start_telemetry_span(@tracer_id, "Websocket", meta, %{
|
2024-02-18 22:36:01 +00:00
|
|
|
attributes: attributes,
|
2024-04-24 01:14:29 +00:00
|
|
|
start_time: measurements.monotonic_time,
|
2024-02-18 22:36:01 +00:00
|
|
|
kind: span_kind
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
def handle_websocket_stop(_event, measurements, meta, _config) do
|
|
|
|
OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, meta)
|
|
|
|
|
|
|
|
attributes = %{
|
|
|
|
:"websocket.recv.binary.frame.bytes" => Map.get(measurements, :send_binary_frame_bytes, 0),
|
|
|
|
:"websocket.send.binary.frame.bytes" => Map.get(measurements, :recv_binary_frame_bytes, 0),
|
|
|
|
:duration => measurements.duration
|
|
|
|
}
|
|
|
|
|
|
|
|
OpenTelemetry.Tracer.set_attributes(attributes)
|
|
|
|
|
|
|
|
if Map.has_key?(meta, :error) do
|
|
|
|
OpenTelemetry.Tracer.set_status(OpenTelemetry.status(:error, meta.error))
|
|
|
|
end
|
2024-02-18 22:36:01 +00:00
|
|
|
|
2024-04-24 01:14:29 +00:00
|
|
|
OpentelemetryTelemetry.end_telemetry_span(@tracer_id, meta)
|
2024-02-18 22:36:01 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
defp extract_url(%{error: _} = meta, _conn) do
|
|
|
|
case Map.get(meta, :request_target) do
|
|
|
|
nil -> ""
|
|
|
|
{scheme, host, port, path} -> build_url(scheme, host, port, path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp extract_url(_meta, conn) do
|
|
|
|
build_url(conn.scheme, conn.host, conn.port, conn.request_path)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp extract_request_path(%{error: _} = meta, _conn) do
|
|
|
|
case Map.get(meta, :request_target) do
|
|
|
|
nil -> ""
|
|
|
|
{_, _, _, path} -> path || ""
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp extract_request_path(_meta, conn) do
|
|
|
|
conn.request_path
|
|
|
|
end
|
|
|
|
|
|
|
|
defp build_url(scheme, host, port, path), do: "#{scheme}://#{host}:#{port}#{path}"
|
|
|
|
|
|
|
|
defp user_agent(conn) do
|
|
|
|
case Plug.Conn.get_req_header(conn, "user-agent") do
|
|
|
|
[] -> ""
|
|
|
|
[head | _] -> head
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp client_ip(%{remote_ip: remote_ip} = conn) do
|
|
|
|
case Plug.Conn.get_req_header(conn, "x-forwarded-for") do
|
|
|
|
[] ->
|
|
|
|
remote_ip
|
|
|
|
|> :inet_parse.ntoa()
|
|
|
|
|> to_string()
|
|
|
|
|
|
|
|
[ip_address | _] ->
|
|
|
|
ip_address
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|