Skip to content

Commit

Permalink
Add LiveView async wrappers for propagation
Browse files Browse the repository at this point in the history
  • Loading branch information
danschultzer committed Dec 16, 2024
1 parent a4ee9fe commit 4cd43c8
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 3 deletions.
56 changes: 56 additions & 0 deletions instrumentation/opentelemetry_phoenix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,59 @@ end
```

The [Phoenix endpoint.ex template](https://github.com/phoenixframework/phoenix/blob/v1.6.0/installer/templates/phx_web/endpoint.ex#L39) can be used as a reference

## Note on Phoenix LiveView

Phoenix LiveView async operations does not have automatic propagation. It is necessary to replace `Phoenix.LiveView.assign_async/4` and `Phoenix.LiveView.start_async/4` with `OpentelemetryPhoenix.LiveView.assign_async/4` and `OpentelemetryPhoenix.LiveView.start_async/4`.

Before:

```elixir
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign(:foo, "bar")
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)
|> assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}
end
```

After:

```elixir
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign(:foo, "bar")
|> OpentelemetryPhoenix.LiveView.assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)
|> OpentelemetryPhoenix.LiveView.assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}
end
```

`OpentelemetryPhoenix.LiveView` must be required in all the live vie wmodules where it is used, sucha s the `live_view` and `live_component` macros:

```elixir
defmodule MyAppWeb do
# ...
def live_view do
quote do
use Phoenix.LiveView,
layout: {MyAppWeb.Layouts, :app}

require OpentelemetryPhoenix.LiveView

unquote(html_helpers())
end
end

def live_component do
quote do
use Phoenix.LiveComponent

require OpentelemetryPhoenix.LiveView

unquote(html_helpers())
end
end
end
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
if Code.ensure_loaded(Phoenix.LiveView) do
defmodule OpentelemetryPhoenix.LiveView do
@moduledoc """
`OpentelemetryPhoenix.LiveView` provides a extensions to the async functions
in the `Phoenix.LiveView` to reduce boilerplate in propagating OpenTelemetry
contexts across process boundaries.
> #### Module Redefinement {: .info}
>
> This module does not redefine the `Phoenix.Liveview` module, instead
> it provides wrappers for async functions, so this functionality will
> not globally modify the default behavior of the `Phoenix.Liveview` module.
## Usage
Require `OpentelemetryPhoenix.LiveView` in your `live_view` and
`live_component` macros:
```elixir
defmodule MyAppWeb do
# ...
def live_view do
quote do
use Phoenix.LiveView,
layout: {MyAppWeb.Layouts, :app}
require OpentelemetryPhoenix.LiveView
unquote(html_helpers())
end
end
def live_component do
quote do
use Phoenix.LiveComponent
require OpentelemetryPhoenix.LiveView
unquote(html_helpers())
end
end
end
```
Update the references to `assign_async` and `start_async` to use this module:application
```elixir
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign(:foo, "bar")
|> OpentelemetryPhoenix.LiveView.assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)
|> OpentelemetryPhoenix.LiveView.assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}
end
```
"""
require Phoenix.LiveView

defmacro assign_async(socket, key_or_keys, func, opts \\ []) do
quote do
require OpenTelemetry.Tracer

ctx = OpenTelemetry.Ctx.get_current()

Phoenix.LiveView.assign_async(
unquote(socket),
unquote(key_or_keys),
fn ->
OpenTelemetry.Ctx.attach(ctx)

unquote(func).()
end,
unquote(opts)
)
end
end

defmacro start_async(socket, name, func, opts \\ []) do
quote do
require OpenTelemetry.Tracer

ctx = OpenTelemetry.Ctx.get_current()

Phoenix.LiveView.start_async(
unquote(socket),
unquote(name),
fn ->
OpenTelemetry.Ctx.attach(ctx)

unquote(func).()
end,
unquote(opts)
)
end
end
end
end
6 changes: 3 additions & 3 deletions instrumentation/opentelemetry_phoenix/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,18 @@ defmodule OpentelemetryPhoenix.MixProject do
{:otel_http, "~> 0.2"},
{:telemetry, "~> 1.0"},
{:plug, ">= 1.11.0"},
{:phoenix_live_view, "~> 1.0", optional: true},
{:cowboy_telemetry, "~> 0.4", only: [:dev, :test]},
{:opentelemetry_exporter, "~> 1.8", only: [:dev, :test]},
{:opentelemetry, "~> 1.5", only: [:dev, :test]},
{:opentelemetry_bandit, "~> 0.2.0", only: [:dev, :test]},
{:opentelemetry_cowboy, "~> 1.0.0", only: [:dev, :test]},
{:ex_doc, "~> 0.35", only: [:dev], runtime: false},
{:phoenix, "~> 1.7", only: [:dev, :test]},
{:phoenix_html, "~> 4.1", only: [:dev, :test]},
{:plug_cowboy, "~> 2.5", only: [:dev, :test]},
{:bandit, "~> 1.5", only: [:dev, :test]},
{:req, "~> 0.5", only: [:dev, :test]},
{:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}
{:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false},
{:floki, ">= 0.30.0", only: :test}
]
end
end
2 changes: 2 additions & 0 deletions instrumentation/opentelemetry_phoenix/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_doc": {:hex, :ex_doc, "0.35.0", "14dcaac6ee0091d1e6938a7ddaf62a4a8c6c0d0b0002e6a9252997a08df719a0", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d69a789ea0248a108c80eef509ec88ffe277f74828169c33f6f7ddaef89c98a5"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
"gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"},
"grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"},
"hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"},
Expand All @@ -36,6 +37,7 @@
"otel_http": {:hex, :otel_http, "0.2.0", "b17385986c7f1b862f5d577f72614ecaa29de40392b7618869999326b9a61d8a", [:rebar3], [], "hexpm", "f2beadf922c8cfeb0965488dd736c95cc6ea8b9efce89466b3904d317d7cc717"},
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
"phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.1", "5389a30658176c0de816636ce276567478bffd063c082515a6e8368b8fc9a0db", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c0f517e6f290f10dbb94343ac22e0109437fb1fa6f0696e7c73967b789c1c285"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
defmodule OpentelemetryPhoenix.LiveViewTest do
defmodule ErrorHTML do
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end

defmodule TestLive do
use Phoenix.LiveView, layout: false

require OpenTelemetry.Tracer
require OpentelemetryPhoenix.LiveView

@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end

@impl true
def handle_params(_params, _url, socket) do
socket =
OpenTelemetry.Tracer.with_span "parent span" do
socket
|> OpentelemetryPhoenix.LiveView.assign_async(:assign_async, fn ->
OpenTelemetry.Tracer.with_span "assign_async span" do
{:ok, %{assign_async: "assign_async.loaded"}}
end
end)
|> OpentelemetryPhoenix.LiveView.start_async(:start_async, fn ->
OpenTelemetry.Tracer.with_span "start_async span" do
"start_async.loaded"
end
end)
end

{:noreply, socket}
end

@impl true
def handle_async(:start_async, {:ok, value}, socket) do
{:noreply, assign(socket, :start_async, Phoenix.LiveView.AsyncResult.ok(value))}
end

@impl true
def render(assigns) do
~H"""
<%= @assign_async.ok? && @assign_async.result %>
<%= assigns[:start_async] && @start_async.ok? && @start_async.result %>
"""
end
end

defmodule Router do
use Phoenix.Router, helpers: false

import Phoenix.LiveView.Router

live "/test", TestLive, :show
end

defmodule Endpoint do
use Phoenix.Endpoint, otp_app: :opentelemetry_phoenix

plug(Router)
end

use ExUnit.Case, async: false

import Phoenix.ConnTest
import Phoenix.LiveViewTest

require Record

for {name, spec} <- Record.extract_all(from_lib: "opentelemetry/include/otel_span.hrl") do
Record.defrecord(name, spec)
end

@endpoint Endpoint

setup do
Application.put_env(
:opentelemetry_phoenix,
Endpoint,
[
secret_key_base: "secret_key_base",
live_view: [signing_salt: "signing_salt"],
render_errors: [formats: [html: ErrorHTML]]
]
)
:otel_simple_processor.set_exporter(:otel_exporter_pid, self())

{:ok, _} = start_supervised(Endpoint)

{:ok, conn: Phoenix.ConnTest.build_conn()}
end

@tag capture_log: true
test "render_async", %{conn: conn} do
{:ok, view, _html} = live(conn, "/test")

assert html = render_async(view)
assert html =~ "assign_async.loaded"
assert html =~ "start_async.loaded"

# Initial parent span from the REST request
assert_receive {:span, span(name: "parent span")}

# Parent span from the socket
assert_receive {:span,
span(
name: "parent span",
trace_id: trace_id,
span_id: process_span_id
)}

assert_receive {:span,
span(
name: "assign_async span",
trace_id: ^trace_id,
parent_span_id: ^process_span_id
)}


assert_receive {:span,
span(
name: "start_async span",
trace_id: ^trace_id,
parent_span_id: ^process_span_id
)}
end
end

0 comments on commit 4cd43c8

Please sign in to comment.