Mix.install(
[
{:opentelemetry_api, "~> 1.2"},
{:phoenix, "~> 1.7"},
{:bandit, "~> 0.7.7"},
{:jason, "~> 1.4"},
{:opentelemetry_phoenix, "~> 1.1"},
{:opentelemetry_exporter, "~> 1.4"},
{:opentelemetry_logger_metadata, "~> 0.1.0"},
{:opentelemetry_sentry, "~> 0.1.1"}
],
config: [
monocle: [
{Monocle.Endpoint,
[
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64),
adapter: Bandit.PhoenixAdapter
]}
],
logger: [level: :info],
opentelemetry: [
text_map_propagators: [OpentelemetrySentry.Propagator],
span_processor: :batch,
traces_exporter: :otlp,
sampler: {:otel_sampler_parent_based, %{root: {:otel_sampler_trace_id_ratio_based, 0.1}}},
resource: %{"service.name" => "myservice", "service.version" => "2.0"},
processors: [
otel_batch_processor: %{
# Using `localhost` here since we are starting outside docker-compose where
# otel would refer to the hostname of the OpenCollector,
#
# If you are running in docker compose, kindly change it to the correct
# hostname: `otel`
exporter:
{:opentelemetry_exporter,
%{
endpoints: [{:http, "localhost", 4318, []}]
}}
}
]
]
]
)
A cute and terrible decorator to add tracing to your Elixir code and see what's going on ...
@_o "🐢"
def turtles() do
Process.sleep(100)
"🐢"
end
Will send a space trace to your OpenTelemetry
backend tagged with 🐢
. Any calls the function calls that are also decorated with monocle @_o
will be nested beneath the parent span.
This is done with the Monocle.Decorator
and you can use it like so:
use Monocle.Decorator
@_o "I see"
def great_mysteries(), do: :math.pi
Here's the code for the decorator, it finds all functions annotated with the @_o
monocle decorator and then rewrites them to have OpenTelemetry
tracing wrapped around it.
defmodule Monocle.Decorator do
defmacro __using__(_opts) do
quote do
@monocles []
Module.register_attribute(__MODULE__, :_o, accumulate: true)
@on_definition Monocle.Decorator
@before_compile Monocle.Decorator
end
end
def __on_definition__(env, :def, name, args, guards, body),
do: annotate_method(env.module, name, args, guards, body)
def __on_definition__(_env, _kind, _name, _args, _guards, _body), do: nil
def annotate_method(module, function, args, guards, body) do
if monocle = Module.delete_attribute(module, :_o) do
update_monocles(module, function, args, guards, body, monocle)
end
end
def update_monocles(_module, _function, _args, _guards, _body, []), do: nil
def update_monocles(module, function, args, guards, body, monocle) do
monocles = Module.get_attribute(module, :monocles)
Module.put_attribute(module, :monocles, [
{function, args, guards, body, monocle}
| monocles
])
end
# Thanks @decorator!
def implied_arities(args) do
arity = Enum.count(args)
default_count =
args
|> Enum.filter(fn
{:\\, _, _} -> true
_ -> false
end)
|> Enum.count()
:lists.seq(arity, arity - default_count, -1)
end
defmacro __before_compile__(env) do
monocles = Module.get_attribute(env.module, :monocles)
requires = [
quote do
require OpenTelemetry.Tracer, as: Tracer
end
]
overriden_functions =
Enum.flat_map(monocles, fn {function_name, args, _guards, [{:do, body}], [monocle_name]} ->
arities = implied_arities(args)
overrideables =
Enum.map(arities, fn arity ->
quote do
defoverridable [{unquote(function_name), unquote(arity)}]
end
end)
overrideables ++
[
quote do
def unquote(function_name)(unquote_splicing(args)) do
Tracer.with_span unquote(monocle_name) do
unquote(body)
end
end
end
]
end)
requires ++ overriden_functions
end
end
Below is a single file Phoenix application where a PageController
is traced with a monocle.
defmodule Monocle.PageController do
use Phoenix.Controller
use Monocle.Decorator
@_o "index"
def index(conn, %{}) do
conn
|> delay()
|> delay()
|> delay()
|> send_resp(200, "Hello, World!")
end
@_o "delaying"
def delay(conn) do
Process.sleep(100 * Enum.random(1..10))
conn
end
end
defmodule Router do
use Phoenix.Router
pipeline :browser do
plug(:accepts, ["html"])
end
scope "/", Monocle do
pipe_through(:browser)
get("/", PageController, :index)
# Prevent a horrible error because ErrorView is missing
get("/favicon.ico", PageController, :index)
end
end
defmodule Monocle.Endpoint do
use Phoenix.Endpoint, otp_app: :monocle
plug(Router)
end
:ok = OpentelemetryLoggerMetadata.setup()
{:ok, _} = Supervisor.start_link([Monocle.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)