defmodule Teleplug do @moduledoc """ Simple opentelementry instrumented plug. """ alias Plug.Conn require Logger require OpenTelemetry.SemanticConventions.Trace, as: Conventions require OpenTelemetry.Tracer, as: Tracer require Record @http_client_ip Atom.to_string(Conventions.http_client_ip()) @http_flavor Atom.to_string(Conventions.http_flavor()) @http_method Atom.to_string(Conventions.http_method()) @http_route Atom.to_string(Conventions.http_route()) @http_scheme Atom.to_string(Conventions.http_scheme()) @http_status_code Atom.to_string(Conventions.http_status_code()) @http_target Atom.to_string(Conventions.http_target()) @http_user_agent Atom.to_string(Conventions.http_user_agent()) @net_host_name Atom.to_string(Conventions.net_host_name()) @net_host_port Atom.to_string(Conventions.net_host_port()) @net_sock_peer_port Atom.to_string(Conventions.net_sock_peer_port()) @net_sock_peer_addr Atom.to_string(Conventions.net_sock_peer_addr()) @behaviour Plug defdelegate setup, to: Teleplug.Instrumentation @impl true def init(opts), do: opts @impl true def call(conn, _opts) do :otel_propagator_text_map.extract(conn.req_headers) attributes = http_common_attributes(conn) ++ http_server_attributes(conn) ++ network_attributes(conn) parent_ctx = Tracer.current_span_ctx() new_ctx = conn |> span_name() |> Tracer.start_span(%{kind: :server, attributes: attributes}) Tracer.set_current_span(new_ctx) Conn.register_before_send(conn, fn conn -> # https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status if conn.status >= 500 do Tracer.set_status(:error, "") end Tracer.set_attribute(@http_status_code, conn.status) Tracer.end_span() Tracer.set_current_span(parent_ctx) conn end) end # see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#name defp span_name(conn), do: "#{conn.method} #{Teleplug.Instrumentation.get_route(conn.request_path)}" # see https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#common-attributes defp http_common_attributes( %Conn{ adapter: adapter, method: method, scheme: scheme } = conn ) do route = Teleplug.Instrumentation.get_route(conn.request_path) [ {@http_method, method}, {@http_route, route}, {@http_target, http_target(conn)}, {@http_scheme, scheme}, {@http_flavor, http_flavor(adapter)}, {@http_user_agent, header_value(conn, "user-agent")} ] end # see https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions defp http_server_attributes(conn), do: [{@http_client_ip, client_ip(conn)}] # see https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/span-general.md#general-network-connection-attributes defp network_attributes( %Conn{ host: host, port: port } = conn ) do peer_data = Plug.Conn.get_peer_data(conn) [ {@net_sock_peer_addr, peer_data |> Map.get(:address) |> :inet_parse.ntoa() |> to_string()}, {@net_sock_peer_port, Map.get(peer_data, :port)}, {@net_host_name, host}, {@net_host_port, port} ] end defp http_target(%Conn{request_path: request_path, query_string: ""}), do: request_path defp http_target(%Conn{request_path: request_path, query_string: query_string}), do: "#{request_path}?#{query_string}" defp header_value(conn, header), do: conn |> Plug.Conn.get_req_header(header) |> List.first() |> to_string() defp http_flavor({_adapter_name, meta}), do: meta |> Map.get(:version) |> to_string() |> String.trim_leading("HTTP/") defp client_ip(%Conn{remote_ip: remote_ip} = conn) do case Plug.Conn.get_req_header(conn, "x-forwarded-for") do [] -> to_string(:inet_parse.ntoa(remote_ip)) [client | _] -> client end end end