defmodule Appsignal.Plug do
require Appsignal.Utils
@span Appsignal.Utils.compile_env(:appsignal, :appsignal_span, Appsignal.Span)
@moduledoc """
AppSignal's Plug instrumentation instruments calls to Plug applications to
gain performance insights and error reporting.
## Installation
To install `Appsignal.Plug` into your Plug application, `use Appsignal.Plug`
in your application's router module:
defmodule AppsignalPlugExample do
use Plug.Router
use Appsignal.Plug
plug(:match)
plug(:dispatch)
get "/" do
send_resp(conn, 200, "Welcome")
end
end
"""
@doc false
defmacro __using__(_) do
quote do
require Logger
require Appsignal.Utils
Appsignal.IntegrationLogger.debug("AppSignal.Plug attached to #{__MODULE__}")
@tracer Appsignal.Utils.compile_env(:appsignal, :appsignal_tracer, Appsignal.Tracer)
@span Appsignal.Utils.compile_env(:appsignal, :appsignal_span, Appsignal.Span)
use Plug.ErrorHandler
def call(%Plug.Conn{private: %{appsignal_plug_instrumented: true}} = conn, opts) do
Logger.warn(
"Appsignal.Plug was included twice, disabling Appsignal.Plug. Please only `use Appsignal.Plug` once."
)
super(conn, opts)
end
def call(conn, opts) do
Appsignal.instrument(fn span ->
_ = @span.set_namespace(span, "http_request")
try do
super(conn, opts)
catch
kind, reason ->
stack = __STACKTRACE__
_ =
span
|> Appsignal.Plug.handle_error(kind, reason, stack, conn)
|> @tracer.close_span()
@tracer.ignore()
:erlang.raise(kind, reason, stack)
else
conn ->
_ = Appsignal.Plug.set_conn_data(span, conn)
Plug.Conn.put_private(conn, :appsignal_plug_instrumented, true)
end
end)
end
defoverridable call: 2
end
end
@doc """
Adds an `:appsignal_name` to the `Plug.Conn`, to overwrite the root
`Appsignal.Span`'s name.
## Examples
iex> Appsignal.Plug.put_name(%Plug.Conn{}, "AppsignalPlugExample#index")
%Plug.Conn{private: %{appsignal_name: "AppsignalPlugExample#index"}}
In a Plug app, call `Appsignal.Plug.put_name/2` on the returned `Plug.Conn`
struct:
defmodule AppsignalPlugExample do
use Plug.Router
use Appsignal.Plug
plug(:match)
plug(:dispatch)
get "/" do
conn
|> Appsignal.Plug.put_name("AppsignalPlugExample#index")
|> send_resp(200, "Welcome")
end
end
"""
def put_name(%Plug.Conn{} = conn, name) do
Plug.Conn.put_private(conn, :appsignal_name, name)
end
@doc false
def set_conn_data(span, conn) do
span
|> @span.set_name(Appsignal.Metadata.name(conn))
|> @span.set_attribute("appsignal:category", Appsignal.Metadata.category(conn))
|> @span.set_sample_data("params", Appsignal.Metadata.params(conn))
|> @span.set_sample_data("environment", Appsignal.Metadata.metadata(conn))
|> @span.set_sample_data("session_data", Appsignal.Metadata.session(conn))
end
@doc false
def handle_error(
span,
:error,
%Plug.Conn.WrapperError{conn: conn, reason: wrapped_reason, stack: stack},
_stack,
_conn
) do
handle_error(span, :error, wrapped_reason, stack, conn)
end
@doc false
def handle_error(span, _kind, %{plug_status: status}, _stack, _conn) when status < 500 do
span
end
@doc false
def handle_error(span, kind, reason, stack, conn) do
conn_with_status = Plug.Conn.put_status(conn, Plug.Exception.status(reason))
span
|> @span.add_error(kind, reason, stack)
|> set_conn_data(conn_with_status)
end
end