defmodule OpentelemetryLiveView do @moduledoc """ OpentelemetryLiveView uses [telemetry](https://hexdocs.pm/telemetry/) handlers to create `OpenTelemetry` spans for LiveView *mount*, *handle_params*, and *handle_event*. The LiveView telemetry events that are used are documented [here](https://hexdocs.pm/phoenix_live_view/telemetry.html). ## Usage Add in your application start function a call to `setup/0`: def start(_type, _args) do # this configures the liveview tracing OpentelemetryLiveView.setup() children = [ ... ] ... end """ require OpenTelemetry.Tracer alias OpenTelemetry.Span alias OpentelemetryLiveView.Reason @tracer_id __MODULE__ @event_names [ {:live_view, :mount}, {:live_view, :handle_params}, {:live_view, :handle_event}, {:live_component, :handle_event} ] |> Enum.flat_map(fn {kind, callback_name} -> Enum.map([:start, :stop, :exception], fn event_name -> [:phoenix, kind, callback_name, event_name] end) end) @doc """ Initializes and configures the telemetry handlers. """ @spec setup() :: :ok def setup do :telemetry.attach_many(__MODULE__, @event_names, &__MODULE__.process_event/4, %{}) end defguardp live_view_or_component?(source) when source in [:live_view, :live_component] @doc false def process_event([:phoenix, source, callback_name, :start], _measurements, meta, _config) when live_view_or_component?(source) do module = case {source, meta} do {:live_view, _} -> module_to_string(meta.socket.view) {:live_component, %{component: component}} -> module_to_string(component) end base_attributes = [ "liveview.module": module, "liveview.callback": Atom.to_string(callback_name) ] attributes = Enum.reduce(meta, base_attributes, fn {:uri, uri}, acc -> Keyword.put(acc, :"liveview.uri", uri) {:component, component}, acc -> Keyword.put(acc, :"liveview.module", module_to_string(component)) {:event, event}, acc -> Keyword.put(acc, :"liveview.event", event) _, acc -> acc end) span_name = case Keyword.fetch(attributes, :"liveview.event") do {:ok, event} -> "#{module}.#{event}" :error -> "#{module}.#{callback_name}" end OpentelemetryTelemetry.start_telemetry_span(@tracer_id, span_name, meta, %{kind: :internal}) |> Span.set_attributes(attributes) end @doc false def process_event([:phoenix, source, _kind, :stop], %{duration: duration}, meta, _config) when live_view_or_component?(source) do ctx = OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, meta) set_duration(ctx, duration) OpentelemetryTelemetry.end_telemetry_span(@tracer_id, meta) end @doc false def process_event( [:phoenix, source, _kind, :exception], %{duration: duration}, %{kind: exception_kind, reason: reason, stacktrace: stacktrace} = meta, _config ) when live_view_or_component?(source) do ctx = OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, meta) set_duration(ctx, duration) {[reason: reason], attrs} = Reason.normalize(reason) |> Keyword.split([:reason]) exception = Exception.normalize(exception_kind, reason, stacktrace) message = Exception.message(exception) Span.record_exception(ctx, exception, stacktrace, attrs) Span.set_status(ctx, OpenTelemetry.status(:error, message)) OpentelemetryTelemetry.end_telemetry_span(@tracer_id, meta) end defp set_duration(ctx, duration) do duration_ms = System.convert_time_unit(duration, :native, :millisecond) Span.set_attribute(ctx, :duration_ms, duration_ms) end defp module_to_string(module) when is_atom(module) do case to_string(module) do "Elixir." <> name -> name erlang_module -> ":#{erlang_module}" end end end