defmodule Teleplug do @moduledoc """ Simple opentelementry instrumented plug. """ alias Plug.Conn require Logger require OpenTelemetry.Tracer, as: Tracer require Record @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() route = Teleplug.Instrumentation.get_route(conn.request_path) new_ctx = Tracer.start_span(route, %{kind: :server, attributes: attributes}) Tracer.set_current_span(new_ctx) set_logger_metadata(new_ctx) Conn.register_before_send(conn, fn conn -> Tracer.set_attribute("http.status_code", conn.status) Tracer.end_span() Tracer.set_current_span(parent_ctx) set_logger_metadata(parent_ctx) conn end) end # 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.host", header_value(conn, "host")}, {"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.peer.ip", peer_data |> Map.get(:address) |> :inet_parse.ntoa() |> to_string()}, {"net.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 defp set_logger_metadata(:undefined), do: Logger.metadata(trace_id: nil, span_id: nil, dd: nil) defp set_logger_metadata(span_ctx) do trace_id = :otel_span.trace_id(span_ctx) span_id = :otel_span.span_id(span_ctx) Logger.metadata( trace_id: trace_id, span_id: span_id, dd: [ trace_id: datadog_trace_id(trace_id), span_id: datadog_span_id(span_id) ] ) end # converts trace_id to a datadog understandable format (taking the 2nd half of the 128 bits string) defp datadog_trace_id(trace_id) do <<_::64, datadog_trace_id::64>> = <> to_string(datadog_trace_id) end defp datadog_span_id(span_id), do: to_string(span_id) end