Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LiveView async wrapper functions for process propagation in OpentelemetryPhoenix #439

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading