Skip to content

Commit

Permalink
Merge pull request #147 from phoenixframework/cm-web-logger
Browse files Browse the repository at this point in the history
Add web console logger and open file from client support
  • Loading branch information
chrismccord committed Feb 29, 2024
2 parents 13d89ca + 80e6b5d commit f434a13
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 52 deletions.
63 changes: 62 additions & 1 deletion README.md
Expand Up @@ -6,7 +6,7 @@ You can use `phoenix_live_reload` in your projects by adding it to your `mix.exs

```elixir
def deps do
[{:phoenix_live_reload, "~> 1.3"}]
[{:phoenix_live_reload, "~> 1.5"}]
end
```

Expand All @@ -23,6 +23,67 @@ config :my_app, MyAppWeb.Endpoint,

The default interval is 100ms.


## Streaming serving logs to the web console

Streaming server logs that you see in the terminal when running `mix phx.server` can be useful to have on the client during development, especially when debugging with SPA fetch callbacks, GraphQL queries, or LiveView actions in the browsers web console. You can enable log streaming to collocate client and server logs in the web console with the `web_console_logger` configuration in your `config/dev.exs`:

```elixir
config :my_app, MyAppWeb.Endpoint,
live_reload: [
interval: 1000,
patterns: [...],
web_console_logger: true
]
```

Next, you'll need to listen for the `"phx:live_reload:attached"` event and enable client logging by calling the reloader's `enableServerLogs()` function, for example:

```javascript
window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
// enable server log streaming to client.
// disable with reloader.disableServerLogs()
reloader.enableServerLogs()
})
```

## Jumping to HEEx function definitions

Many times it's useful to inspect the HTML DOM tree to find where markup is being rendered from within your application. HEEx supports annotating rendered HTML with HTML comments that give you the file/line of a HEEx function component and caller. `:phoenix_live_reload` will look for the `PLUG_EDITOR` environment export (used by the plug debugger page to link to source code) to launch a configured URL of your choice to open your code editor to the file-line of the HTML annotation. For example, the following export on your system would open vscode at the correct file/line:

```
export PLUG_EDITOR="vscode://file/__FILE__:__LINE__"
```

The `vscode://` protocol URL will open vscode with placeholders of `__FILE__:__LINE__` substited at runtime. Check your editor's documentation on protocol URL support. To open your configured editor URL when an element is clicked, say with alt-click, you can wire up an event listener within your `"phx:live_reload:attached"` callback and make use of the reloader's `openEditorAtCaller` and `openEditorAtDef` functions, passing the event target as the DOM node to reference for HEEx file:line annotation information. For example:

```javascript
window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
// Enable server log streaming to client. Disable with reloader.disableServerLogs()
reloader.enableServerLogs()

// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
//
// * click with "c" key pressed to open at caller location
// * click with "d" key pressed to open at function component definition location
let keyDown
window.addEventListener("keydown", e => keyDown = e.key)
window.addEventListener("keyup", e => keyDown = null)
window.addEventListener("click", e => {
if(keyDown === "c"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtCaller(e.target)
} else if(keyDown === "d"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtDef(e.target)
}
}, true)
window.liveReloader = reloader
})
```

## Backends

This project uses [`FileSystem`](https://github.com/falood/file_system) as a dependency to watch your filesystem whenever there is a change and it supports the following operating systems:
Expand Down
12 changes: 11 additions & 1 deletion lib/phoenix_live_reload/application.ex
Expand Up @@ -2,8 +2,18 @@ defmodule Phoenix.LiveReloader.Application do
use Application
require Logger

alias Phoenix.LiveReloader.WebConsoleLogger

def start(_type, _args) do
children = [%{id: __MODULE__, start: {__MODULE__, :start_link, []}}]
# note we always attach and start the logger as :phoenix_live_reload should only
# be started in dev via user's `only: :dev` entry.
WebConsoleLogger.attach_logger()

children = [
WebConsoleLogger,
%{id: __MODULE__, start: {__MODULE__, :start_link, []}}
]

Supervisor.start_link(children, strategy: :one_for_one)
end

Expand Down
57 changes: 54 additions & 3 deletions lib/phoenix_live_reload/channel.ex
Expand Up @@ -5,20 +5,30 @@ defmodule Phoenix.LiveReloader.Channel do
use Phoenix.Channel
require Logger

alias Phoenix.LiveReloader.WebConsoleLogger

@logs :logs

def join("phoenix:live_reload", _msg, socket) do
{:ok, _} = Application.ensure_all_started(:phoenix_live_reload)

if Process.whereis(:phoenix_live_reload_file_monitor) do
FileSystem.subscribe(:phoenix_live_reload_file_monitor)

if web_console_logger_enabled?(socket) do
WebConsoleLogger.subscribe(@logs)
end

config = socket.endpoint.config(:live_reload)

socket =
socket
|> assign(:patterns, config[:patterns] || [])
|> assign(:debounce, config[:debounce] || 0)
|> assign(:notify_patterns, config[:notify] || [])
|> assign(:deps_paths, deps_paths())

{:ok, socket}
{:ok, join_info(), socket}
else
{:error, %{message: "live reload backend not running"}}
end
Expand All @@ -28,7 +38,7 @@ defmodule Phoenix.LiveReloader.Channel do
%{
patterns: patterns,
debounce: debounce,
notify_patterns: notify_patterns,
notify_patterns: notify_patterns
} = socket.assigns

if matches_any_pattern?(path, patterns) do
Expand All @@ -47,13 +57,34 @@ defmodule Phoenix.LiveReloader.Channel do
socket.pubsub_server,
to_string(topic),
{:phoenix_live_reload, topic, path}
)
)
end
end

{:noreply, socket}
end

def handle_info({@logs, %{level: level, msg: msg, meta: meta}}, socket) do
push(socket, "log", %{
level: to_string(level),
msg: msg,
file: meta[:file],
line: meta[:line]
})

{:noreply, socket}
end

def handle_in("full_path", %{"rel_path" => rel_path, "app" => app}, socket) do
case socket.assigns.deps_paths do
%{^app => dep_path} ->
{:reply, {:ok, %{full_path: Path.join(dep_path, rel_path)}}, socket}

%{} ->
{:reply, {:ok, %{full_path: Path.join(File.cwd!(), rel_path)}}, socket}
end
end

defp debounce(0, _exts, _patterns), do: []

defp debounce(time, exts, patterns) when is_integer(time) and time > 0 do
Expand Down Expand Up @@ -87,4 +118,24 @@ defmodule Phoenix.LiveReloader.Channel do

defp remove_leading_dot("." <> rest), do: rest
defp remove_leading_dot(rest), do: rest

defp web_console_logger_enabled?(socket) do
socket.endpoint.config(:live_reload)[:web_console_logger] == true
end

defp join_info do
if url = System.get_env("PLUG_EDITOR") do
%{editor_url: url}
else
%{}
end
end

defp deps_paths do
if Code.loaded?(Mix.Project) do
for {app, path} <- Mix.Project.deps_paths(), into: %{}, do: {to_string(app), path}
else
%{}
end
end
end
5 changes: 5 additions & 0 deletions lib/phoenix_live_reload/live_reloader.ex
Expand Up @@ -70,6 +70,11 @@ defmodule Phoenix.LiveReloader do
Useful when class names are determined at runtime, for example when
working with CSS modules. Defaults to false.
* `:web_console_logger` - If true, the live reloader will log messages
to the web console in your browser. Defaults to false.
*Note*: your appplication javascript bundle will need to enable logs.
See the README for more information.
In an umbrella app, if you want to enable live reloading based on code
changes in sibling applications, set the `reloadable_apps` option on your
endpoint to ensure the code will be recompiled, then add the dirs to
Expand Down
35 changes: 35 additions & 0 deletions lib/phoenix_live_reload/web_console_logger.ex
@@ -0,0 +1,35 @@
defmodule Phoenix.LiveReloader.WebConsoleLogger do
@moduledoc false

@registry Phoenix.LiveReloader.WebConsoleLoggerRegistry

def attach_logger do
if function_exported?(Logger, :default_formatter, 0) do
:ok =
:logger.add_handler(__MODULE__, __MODULE__, %{
formatter: Logger.default_formatter(colors: [enabled: false])
})
end
end

def child_spec(_args) do
Registry.child_spec(name: @registry, keys: :duplicate)
end

def subscribe(prefix) do
{:ok, _} = Registry.register(@registry, :all, prefix)
:ok
end

# Erlang/OTP log handler
def log(%{meta: meta, level: level} = event, config) do
%{formatter: {formatter_mod, formatter_config}} = config
iodata = formatter_mod.format(event, formatter_config)
msg = IO.iodata_to_binary(iodata)

Registry.dispatch(@registry, :all, fn entries ->
for {pid, prefix} <- entries,
do: send(pid, {prefix, %{level: level, msg: msg, meta: meta}})
end)
end
end
2 changes: 1 addition & 1 deletion mix.lock
@@ -1,7 +1,7 @@
%{
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
"ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"},
"file_system": {:hex, :file_system, "0.2.1", "c4bec8f187d2aabace4beb890f0d4e468f65ca051593db768e533a274d0df587", [:mix], [], "hexpm", "ba49dc1647b30a1ae0ab320198b82dbe41f594c41eaaabd7a2ba14ac38faa578"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
Expand Down

0 comments on commit f434a13

Please sign in to comment.