Skip to content

Instantly share code, notes, and snippets.

@smn
Last active June 13, 2024 14:26
Show Gist options
  • Save smn/f32955944bb7a0011b11e59133d3d9d3 to your computer and use it in GitHub Desktop.
Save smn/f32955944bb7a0011b11e59133d3d9d3 to your computer and use it in GitHub Desktop.

Monocle

Run in Livebook

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, []}]
             }}
        }
      ]
    ]
  ]
)

Section

@_o

A cute and terrible decorator to add tracing to your Elixir code and see what's going on ...

monocle

@_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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment