From c49b4e8fe9ba76533216d8c6c09720e3beac2904 Mon Sep 17 00:00:00 2001 From: David Gil Date: Mon, 14 May 2018 10:18:53 +0200 Subject: [PATCH 1/5] Add RingLogger and RingLogger Client unchanged --- fw/config/config.exs | 4 + fw/mix.exs | 1 + fw/mix.lock.host | 1 + ui/lib/ui/client.ex | 245 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 ui/lib/ui/client.ex diff --git a/fw/config/config.exs b/fw/config/config.exs index 05b6b3e..0cbb8fd 100644 --- a/fw/config/config.exs +++ b/fw/config/config.exs @@ -6,3 +6,7 @@ use Mix.Config import_config "#{Mix.Project.config()[:target]}.exs" + +config :logger, backends: [RingLogger] + +config :logger, RingLogger, max_size: 100 diff --git a/fw/mix.exs b/fw/mix.exs index e3b4a4e..09814bf 100644 --- a/fw/mix.exs +++ b/fw/mix.exs @@ -45,6 +45,7 @@ defmodule Fw.Mixfile do {:poison, "~> 3.1"}, {:timex, "~> 3.0"}, {:aps, path: "../aps"}, + {:ring_logger, "~> 0.4"}, {:ui, path: "../ui"} ] ++ deps(@target) end diff --git a/fw/mix.lock.host b/fw/mix.lock.host index d0ffc3f..b2be89b 100644 --- a/fw/mix.lock.host +++ b/fw/mix.lock.host @@ -34,6 +34,7 @@ "pummpcomm": {:hex, :pummpcomm, "2.5.1", "e02e1c24b4e0929fe44e5cd52ea4546220008f66140de09ebf6f5af50f8101cd", [:mix], [{:rfm69, "~> 0.2", [hex: :rfm69, repo: "hexpm", optional: false]}, {:subg_rfspy, "~> 2.0", [hex: :subg_rfspy, repo: "hexpm", optional: false]}, {:timex, "~> 3.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, "rfm69": {:hex, :rfm69, "0.2.0", "ab6582881d2108439c79e85c731216fd1e7dc7c1c627b595960950388f8c42ba", [:mix], [{:elixir_ale, "~> 1.0", [hex: :elixir_ale, repo: "hexpm", optional: false]}], "hexpm"}, + "ring_logger": {:hex, :ring_logger, "0.4.1", "db972365bfda705288d7629e80af5704a1aafdbe9da842712c3cdd587639c72e", [:mix], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, "subg_rfspy": {:hex, :subg_rfspy, "2.0.0", "404cabf89eeb139cd6f43ce0ff597d20832393933aa492ca2b1ff73c846ab816", [:mix], [{:csv, "~> 2.0.0", [hex: :csv, repo: "hexpm", optional: false]}, {:elixir_ale, "~> 1.0.1", [hex: :elixir_ale, repo: "hexpm", optional: false]}, {:nerves_uart, "~> 0.1.1", [hex: :nerves_uart, repo: "hexpm", optional: false]}], "hexpm"}, "timex": {:hex, :timex, "3.2.1", "639975eac45c4c08c2dbf7fc53033c313ff1f94fad9282af03619a3826493612", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/ui/lib/ui/client.ex b/ui/lib/ui/client.ex new file mode 100644 index 0000000..63ca971 --- /dev/null +++ b/ui/lib/ui/client.ex @@ -0,0 +1,245 @@ +defmodule RingLogger.Client do + use GenServer + + @moduledoc """ + Interact with the RingLogger + """ + + alias RingLogger.Server + + defmodule State do + @moduledoc false + defstruct io: nil, + colors: nil, + metadata: nil, + format: nil, + level: nil, + index: 0 + end + + @doc """ + Start up a client GenServer. Except for just getting the contents of the ring buffer, you'll + need to create one of these. See `configure/2` for information on options. + """ + def start_link(config \\ []) do + GenServer.start_link(__MODULE__, config) + end + + @doc """ + Stop a client. + """ + def stop(client_pid) do + GenServer.stop(client_pid) + end + + @doc """ + Update the client configuration. + + Options include: + * `:io` - Defaults to `:stdio` + * `:colors` - + * `:metadata` - A KV list of additional metadata + * `:format` - A custom format string, or a {module, function} tuple (see + https://hexdocs.pm/logger/master/Logger.html#module-custom-formatting) + * `:level` - The minimum log level to report. + """ + @spec configure(GenServer.server(), [RingLogger.client_option()]) :: :ok + def configure(client_pid, config) do + GenServer.call(client_pid, {:config, config}) + end + + @doc """ + Attach the current IEx session to the logger. It will start printing log messages. + """ + @spec attach(GenServer.server()) :: :ok + def attach(client_pid) do + GenServer.call(client_pid, :attach) + end + + @doc """ + Detach the current IEx session from the logger. + """ + @spec detach(GenServer.server()) :: :ok + def detach(client_pid) do + GenServer.call(client_pid, :detach) + end + + @doc """ + Tail the messages in the log. + """ + @spec tail(GenServer.server()) :: :ok + def tail(client_pid) do + GenServer.call(client_pid, :tail) + end + + @doc """ + Reset the index into the log for `tail/1` to the oldest entry. + """ + @spec reset(GenServer.server()) :: :ok + def reset(client_pid) do + GenServer.call(client_pid, :reset) + end + + @doc """ + Helper method for formatting log messages per the current client's + configuration. + """ + @spec format(GenServer.server(), RingLogger.entry()) :: :ok + def format(client_pid, message) do + GenServer.call(client_pid, {:format, message}) + end + + @doc """ + Run a regular expression on each entry in the log and print out the matchers. + """ + @spec grep(GenServer.server(), Regex.t()) :: :ok + def grep(client_pid, regex) do + GenServer.call(client_pid, {:grep, regex}) + end + + def init(config) do + state = %State{ + io: Keyword.get(config, :io, :stdio), + colors: configure_colors(config), + metadata: Keyword.get(config, :metadata, []) |> configure_metadata(), + format: Keyword.get(config, :format) |> configure_formatter(), + level: Keyword.get(config, :level, :debug) + } + + {:ok, state} + end + + def handle_info({:log, msg}, state) do + maybe_print(msg, state) + {:noreply, state} + end + + def handle_call({:config, config}, _from, state) do + new_io = Keyword.get(config, :io, state.io) + new_level = Keyword.get(config, :level, state.level) + + new_state = %State{state | io: new_io, level: new_level} + + {:reply, :ok, new_state} + end + + def handle_call(:attach, _from, state) do + {:reply, Server.attach_client(self()), state} + end + + def handle_call(:detach, _from, state) do + {:reply, Server.detach_client(self()), state} + end + + def handle_call(:tail, _from, state) do + messages = Server.get(state.index) + + case List.last(messages) do + nil -> + # No messages + {:reply, :ok, state} + + last_message -> + Enum.each(messages, fn msg -> maybe_print(msg, state) end) + next_index = message_index(last_message) + 1 + {:reply, :ok, %{state | index: next_index}} + end + end + + def handle_call(:reset, _from, state) do + {:reply, :ok, %{state | index: 0}} + end + + def handle_call({:grep, regex}, _from, state) do + Server.get() + |> Enum.each(fn msg -> maybe_print(msg, regex, state) end) + + {:reply, :ok, state} + end + + def handle_call({:format, msg}, _from, state) do + item = format_message(msg, state) + {:reply, item, state} + end + + defp message_index({_level, {_, _msg, _ts, md}}), do: Keyword.get(md, :index) + + defp format_message({level, {_, msg, ts, md}}, state) do + metadata = take_metadata(md, state.metadata) + + state.format + |> apply_format(level, msg, ts, metadata) + |> color_event(level, state.colors, md) + end + + ## Helpers + + defp apply_format({mod, fun}, level, msg, ts, metadata) do + apply(mod, fun, [level, msg, ts, metadata]) + end + + defp apply_format(format, level, msg, ts, metadata) do + Logger.Formatter.format(format, level, msg, ts, metadata) + end + + defp configure_metadata(:all), do: :all + defp configure_metadata(metadata), do: Enum.reverse(metadata) + + defp configure_colors(config) do + colors = Keyword.get(config, :colors, []) + + %{ + debug: Keyword.get(colors, :debug, :cyan), + info: Keyword.get(colors, :info, :normal), + warn: Keyword.get(colors, :warn, :yellow), + error: Keyword.get(colors, :error, :red), + enabled: Keyword.get(colors, :enabled, IO.ANSI.enabled?()) + } + end + + defp meet_level?(_lvl, nil), do: true + + defp meet_level?(lvl, min) do + Logger.compare_levels(lvl, min) != :lt + end + + defp take_metadata(metadata, :all), do: metadata + + defp take_metadata(metadata, keys) do + Enum.reduce(keys, [], fn key, acc -> + case Keyword.fetch(metadata, key) do + {:ok, val} -> [{key, val} | acc] + :error -> acc + end + end) + end + + defp color_event(data, _level, %{enabled: false}, _md), do: data + + defp color_event(data, level, %{enabled: true} = colors, md) do + color = md[:ansi_color] || Map.fetch!(colors, level) + [IO.ANSI.format_fragment(color, true), data | IO.ANSI.reset()] + end + + defp configure_formatter({mod, fun}), do: {mod, fun} + + defp configure_formatter(format) do + Logger.Formatter.compile(format) + end + + defp maybe_print({level, _} = msg, state) do + if meet_level?(level, state.level) do + item = format_message(msg, state) + IO.binwrite(state.io, item) + end + end + + defp maybe_print({level, {_, text, _, _}} = msg, r, state) do + flattened_text = IO.iodata_to_binary(text) + + if meet_level?(level, state.level) && Regex.match?(r, flattened_text) do + item = format_message(msg, state) + IO.binwrite(state.io, item) + end + end +end From a5dd0a950b527177ade964b9c4f1ee6e360deb38 Mon Sep 17 00:00:00 2001 From: David Gil Date: Mon, 14 May 2018 11:56:46 +0200 Subject: [PATCH 2/5] Name the GenServer to make its API easier to use Add InfinityAPS.UI.Client to the supervisor. --- ui/lib/ui.ex | 4 +++- ui/lib/ui/client.ex | 50 ++++++++++++++++++++++----------------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/ui/lib/ui.ex b/ui/lib/ui.ex index 7b2f95b..069f311 100644 --- a/ui/lib/ui.ex +++ b/ui/lib/ui.ex @@ -5,13 +5,15 @@ defmodule InfinityAPS.UI do alias InfinityAPS.UI.GlucoseBroker alias InfinityAPS.UI.Endpoint + alias InfinityAPS.UI.Client def start(_type, _args) do import Supervisor.Spec children = [ supervisor(Endpoint, []), - GlucoseBroker.child_spec(nil) + GlucoseBroker.child_spec(nil), + Client ] opts = [strategy: :one_for_one, name: InfinityAPS.UI.Supervisor] diff --git a/ui/lib/ui/client.ex b/ui/lib/ui/client.ex index 63ca971..e9bc88c 100644 --- a/ui/lib/ui/client.ex +++ b/ui/lib/ui/client.ex @@ -1,4 +1,4 @@ -defmodule RingLogger.Client do +defmodule InfinityAPS.UI.Client do use GenServer @moduledoc """ @@ -22,14 +22,14 @@ defmodule RingLogger.Client do need to create one of these. See `configure/2` for information on options. """ def start_link(config \\ []) do - GenServer.start_link(__MODULE__, config) + GenServer.start_link(__MODULE__, config, name: __MODULE__) end @doc """ Stop a client. """ - def stop(client_pid) do - GenServer.stop(client_pid) + def stop() do + GenServer.stop(__MODULE__) end @doc """ @@ -43,58 +43,58 @@ defmodule RingLogger.Client do https://hexdocs.pm/logger/master/Logger.html#module-custom-formatting) * `:level` - The minimum log level to report. """ - @spec configure(GenServer.server(), [RingLogger.client_option()]) :: :ok - def configure(client_pid, config) do - GenServer.call(client_pid, {:config, config}) + @spec configure([RingLogger.client_option()]) :: :ok + def configure(config) do + GenServer.call(__MODULE__, {:config, config}) end @doc """ Attach the current IEx session to the logger. It will start printing log messages. """ - @spec attach(GenServer.server()) :: :ok - def attach(client_pid) do - GenServer.call(client_pid, :attach) + @spec attach() :: :ok + def attach() do + GenServer.call(__MODULE__, :attach) end @doc """ Detach the current IEx session from the logger. """ - @spec detach(GenServer.server()) :: :ok - def detach(client_pid) do - GenServer.call(client_pid, :detach) + @spec detach() :: :ok + def detach() do + GenServer.call(__MODULE__, :detach) end @doc """ Tail the messages in the log. """ - @spec tail(GenServer.server()) :: :ok - def tail(client_pid) do - GenServer.call(client_pid, :tail) + @spec tail() :: :ok + def tail() do + GenServer.call(__MODULE__, :tail) end @doc """ Reset the index into the log for `tail/1` to the oldest entry. """ - @spec reset(GenServer.server()) :: :ok - def reset(client_pid) do - GenServer.call(client_pid, :reset) + @spec reset() :: :ok + def reset() do + GenServer.call(__MODULE__, :reset) end @doc """ Helper method for formatting log messages per the current client's configuration. """ - @spec format(GenServer.server(), RingLogger.entry()) :: :ok - def format(client_pid, message) do - GenServer.call(client_pid, {:format, message}) + @spec format(RingLogger.entry()) :: :ok + def format(message) do + GenServer.call(__MODULE__, {:format, message}) end @doc """ Run a regular expression on each entry in the log and print out the matchers. """ - @spec grep(GenServer.server(), Regex.t()) :: :ok - def grep(client_pid, regex) do - GenServer.call(client_pid, {:grep, regex}) + @spec grep(Regex.t()) :: :ok + def grep(regex) do + GenServer.call(__MODULE__, {:grep, regex}) end def init(config) do From 95ac459050393db8773b2c452f15b755773ca282 Mon Sep 17 00:00:00 2001 From: David Gil Date: Tue, 15 May 2018 12:13:44 +0200 Subject: [PATCH 3/5] Create channel that sends logs to client --- ui/assets/js/socket.js | 10 +++++++ ui/lib/ui/client.ex | 41 ++++++++++++++++++++++++--- ui/lib/ui_web/channels/log_channel.ex | 10 +++++++ ui/lib/ui_web/channels/user_socket.ex | 1 + 4 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 ui/lib/ui_web/channels/log_channel.ex diff --git a/ui/assets/js/socket.js b/ui/assets/js/socket.js index 1aba26d..385de6e 100644 --- a/ui/assets/js/socket.js +++ b/ui/assets/js/socket.js @@ -2,4 +2,14 @@ import {Socket} from "phoenix"; let socket = new Socket("/socket", {params: {}}); // token: window.userToken}}); socket.connect(); + +let channel = socket.channel("logs", {}); +channel.join() + .receive("ok", resp => { console.log("Joined successfully to logs channel", resp); }) + .receive("error", resp => { console.log("Unable to join to logs channel", resp); }); + +channel.on("log:new", msg => { + console.log("New log message:", msg); + // Things to do when receiving a log message +}); export default socket; diff --git a/ui/lib/ui/client.ex b/ui/lib/ui/client.ex index e9bc88c..6797d89 100644 --- a/ui/lib/ui/client.ex +++ b/ui/lib/ui/client.ex @@ -1,6 +1,8 @@ defmodule InfinityAPS.UI.Client do use GenServer + alias InfinityAPS.UI.Endpoint + @moduledoc """ Interact with the RingLogger """ @@ -110,7 +112,7 @@ defmodule InfinityAPS.UI.Client do end def handle_info({:log, msg}, state) do - maybe_print(msg, state) + maybe_send(msg, state) {:noreply, state} end @@ -140,7 +142,7 @@ defmodule InfinityAPS.UI.Client do {:reply, :ok, state} last_message -> - Enum.each(messages, fn msg -> maybe_print(msg, state) end) + Enum.each(messages, fn msg -> maybe_send(msg, state) end) next_index = message_index(last_message) + 1 {:reply, :ok, %{state | index: next_index}} end @@ -152,7 +154,7 @@ defmodule InfinityAPS.UI.Client do def handle_call({:grep, regex}, _from, state) do Server.get() - |> Enum.each(fn msg -> maybe_print(msg, regex, state) end) + |> Enum.each(fn msg -> maybe_send(msg, regex, state) end) {:reply, :ok, state} end @@ -167,9 +169,24 @@ defmodule InfinityAPS.UI.Client do defp format_message({level, {_, msg, ts, md}}, state) do metadata = take_metadata(md, state.metadata) + """ + Note: A log can be converted to a map with a single string (as done here) + or they can be a map with multiple fields (commented out). + + {d, t} = ts + date = Logger.Formatter.format_date(d) + |> IO.chardata_to_string + time = Logger.Formatter.format_time(t) + |> IO.chardata_to_string + state.format + |> apply_format(level, msg, ts, metadata) + %{level: level |> Atom.to_string, msg: msg |> IO.chardata_to_string, date: date, time: time, metadata: metadata} + """ + state.format |> apply_format(level, msg, ts, metadata) - |> color_event(level, state.colors, md) + |> IO.chardata_to_string() + |> String.trim() end ## Helpers @@ -242,4 +259,20 @@ defmodule InfinityAPS.UI.Client do IO.binwrite(state.io, item) end end + + defp maybe_send({level, _} = msg, state) do + if meet_level?(level, state.level) do + item = format_message(msg, state) + Endpoint.broadcast!("logs", "log:new", %{msg: item}) + end + end + + defp maybe_send({level, {_, text, _, _}} = msg, r, state) do + flattened_text = IO.iodata_to_binary(text) + + if meet_level?(level, state.level) && Regex.match?(r, flattened_text) do + item = format_message(msg, state) + Endpoint.broadcast!("logs", "log:new", %{msg: item}) + end + end end diff --git a/ui/lib/ui_web/channels/log_channel.ex b/ui/lib/ui_web/channels/log_channel.ex new file mode 100644 index 0000000..4200ded --- /dev/null +++ b/ui/lib/ui_web/channels/log_channel.ex @@ -0,0 +1,10 @@ +defmodule InfinityAPS.UI.LogChannel do + @moduledoc false + + use Phoenix.Channel + + def join("logs", _message, socket) do + InfinityAPS.UI.Client.attach + {:ok, socket} + end +end diff --git a/ui/lib/ui_web/channels/user_socket.ex b/ui/lib/ui_web/channels/user_socket.ex index c58f75a..48ced41 100644 --- a/ui/lib/ui_web/channels/user_socket.ex +++ b/ui/lib/ui_web/channels/user_socket.ex @@ -2,6 +2,7 @@ defmodule InfinityAPS.UI.UserSocket do use Phoenix.Socket channel("loop_status:*", InfinityAPS.UI.LoopStatusChannel) + channel("logs", InfinityAPS.UI.LogChannel) transport(:websocket, Phoenix.Transports.WebSocket) From 51e5caac0be7635440a119489db24eaf9274ed89 Mon Sep 17 00:00:00 2001 From: David Gil Date: Tue, 15 May 2018 13:08:45 +0200 Subject: [PATCH 4/5] Fix credo issues --- ui/lib/ui/client.ex | 19 ++++++++++--------- ui/lib/ui_web/channels/log_channel.ex | 4 +++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ui/lib/ui/client.ex b/ui/lib/ui/client.ex index 6797d89..413134a 100644 --- a/ui/lib/ui/client.ex +++ b/ui/lib/ui/client.ex @@ -2,6 +2,7 @@ defmodule InfinityAPS.UI.Client do use GenServer alias InfinityAPS.UI.Endpoint + alias Logger.Formatter @moduledoc """ Interact with the RingLogger @@ -30,7 +31,7 @@ defmodule InfinityAPS.UI.Client do @doc """ Stop a client. """ - def stop() do + def stop do GenServer.stop(__MODULE__) end @@ -54,7 +55,7 @@ defmodule InfinityAPS.UI.Client do Attach the current IEx session to the logger. It will start printing log messages. """ @spec attach() :: :ok - def attach() do + def attach do GenServer.call(__MODULE__, :attach) end @@ -62,7 +63,7 @@ defmodule InfinityAPS.UI.Client do Detach the current IEx session from the logger. """ @spec detach() :: :ok - def detach() do + def detach do GenServer.call(__MODULE__, :detach) end @@ -70,7 +71,7 @@ defmodule InfinityAPS.UI.Client do Tail the messages in the log. """ @spec tail() :: :ok - def tail() do + def tail do GenServer.call(__MODULE__, :tail) end @@ -78,7 +79,7 @@ defmodule InfinityAPS.UI.Client do Reset the index into the log for `tail/1` to the oldest entry. """ @spec reset() :: :ok - def reset() do + def reset do GenServer.call(__MODULE__, :reset) end @@ -103,8 +104,8 @@ defmodule InfinityAPS.UI.Client do state = %State{ io: Keyword.get(config, :io, :stdio), colors: configure_colors(config), - metadata: Keyword.get(config, :metadata, []) |> configure_metadata(), - format: Keyword.get(config, :format) |> configure_formatter(), + metadata: config |> Keyword.get(:metadata, []) |> configure_metadata(), + format: config |> Keyword.get(:format) |> configure_formatter(), level: Keyword.get(config, :level, :debug) } @@ -196,7 +197,7 @@ defmodule InfinityAPS.UI.Client do end defp apply_format(format, level, msg, ts, metadata) do - Logger.Formatter.format(format, level, msg, ts, metadata) + Formatter.format(format, level, msg, ts, metadata) end defp configure_metadata(:all), do: :all @@ -241,7 +242,7 @@ defmodule InfinityAPS.UI.Client do defp configure_formatter({mod, fun}), do: {mod, fun} defp configure_formatter(format) do - Logger.Formatter.compile(format) + Formatter.compile(format) end defp maybe_print({level, _} = msg, state) do diff --git a/ui/lib/ui_web/channels/log_channel.ex b/ui/lib/ui_web/channels/log_channel.ex index 4200ded..da94b59 100644 --- a/ui/lib/ui_web/channels/log_channel.ex +++ b/ui/lib/ui_web/channels/log_channel.ex @@ -3,8 +3,10 @@ defmodule InfinityAPS.UI.LogChannel do use Phoenix.Channel + alias InfinityAPS.UI.Client + def join("logs", _message, socket) do - InfinityAPS.UI.Client.attach + Client.attach() {:ok, socket} end end From 399c234f264abd05bcf4874d5d1e96c901eb2a7e Mon Sep 17 00:00:00 2001 From: David Gil Date: Tue, 15 May 2018 13:10:38 +0200 Subject: [PATCH 5/5] Fix losing log messages --- ui/lib/ui_web/channels/log_channel.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/lib/ui_web/channels/log_channel.ex b/ui/lib/ui_web/channels/log_channel.ex index da94b59..2344f4d 100644 --- a/ui/lib/ui_web/channels/log_channel.ex +++ b/ui/lib/ui_web/channels/log_channel.ex @@ -6,7 +6,13 @@ defmodule InfinityAPS.UI.LogChannel do alias InfinityAPS.UI.Client def join("logs", _message, socket) do - Client.attach() + send(self(), {:after_join_attach}) {:ok, socket} end + + def handle_info({:after_join_attach}, socket) do + Client.tail() + Client.attach() + {:noreply, socket} + end end