├── test ├── test_helper.exs └── live_dashboard_history_test.exs ├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── lib ├── application.ex ├── live_dashboard_history.ex └── history_supervisor.ex ├── LICENSE.md ├── mix.exs ├── dev.exs ├── README.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | locals_without_parens = [live_dashboard_history: 1, live_dashboard_history: 2] 3 | 4 | [ 5 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 6 | locals_without_parens: locals_without_parens, 7 | export: [ 8 | locals_without_parens: locals_without_parens 9 | ] 10 | ] 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: mix test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}}) 12 | strategy: 13 | matrix: 14 | otp: [21.x, 22.x] 15 | elixir: [1.8.x, 1.9.x, 1.10.x] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-elixir@v1 20 | with: 21 | otp-version: ${{matrix.otp}} 22 | elixir-version: ${{matrix.elixir}} 23 | - name: Install Dependencies 24 | run: mix deps.get 25 | - name: Run Tests 26 | run: mix test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | live_dashboard_history-*.tar 24 | 25 | -------------------------------------------------------------------------------- /lib/application.ex: -------------------------------------------------------------------------------- 1 | defmodule LiveDashboardHistory.Application do 2 | use Application 3 | 4 | alias LiveDashboardHistory.HistorySupervisor 5 | 6 | def start(_type, _args) do 7 | # List all child processes to be supervised 8 | children = [ 9 | # Starts a worker by calling: Podder.Worker.start_link(arg) 10 | # {Podder.Worker, arg}, 11 | {Registry, keys: :unique, name: LiveDashboardHistory.Registry}, 12 | %{ 13 | id: HistorySupervisor, 14 | start: {HistorySupervisor, :start_link, []} 15 | } 16 | ] 17 | 18 | # See https://hexdocs.pm/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: LiveDashboardHistory.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2020 Brian Glusman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LiveDashboardHistory.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "http://github.com/bglusman/live_dashboard_history" 5 | @version "0.1.5" 6 | 7 | def project do 8 | [ 9 | app: :live_dashboard_history, 10 | version: @version, 11 | elixir: "~> 1.11", 12 | start_permanent: Mix.env() == :prod, 13 | aliases: aliases(), 14 | name: "LiveDashboardHistory", 15 | docs: docs(), 16 | package: package(), 17 | description: "Ephemeral metrics history storage for Phoenix LiveDashboard", 18 | deps: deps() 19 | ] 20 | end 21 | 22 | # Run "mix help compile.app" to learn about applications. 23 | def application do 24 | [ 25 | mod: {LiveDashboardHistory.Application, []}, 26 | extra_applications: [:logger] 27 | ] 28 | end 29 | 30 | defp docs do 31 | [ 32 | extras: [ 33 | "LICENSE.md": [title: "License"], 34 | "README.md": [title: "Overview"] 35 | ], 36 | main: "readme", 37 | homepage_url: @source_url, 38 | source_ref: "v#{@version}", 39 | source_url: @source_url, 40 | nest_modules_by_prefix: [LiveDashboardHistory], 41 | formatters: ["html"], 42 | api_reference: false 43 | ] 44 | end 45 | 46 | defp aliases do 47 | [ 48 | setup: ["deps.get", "cmd npm install --prefix assets"], 49 | no_halt: "run --no-halt dev.exs", 50 | put_config: &put_config/1, 51 | dev: ["put_config", "no_halt"] 52 | ] 53 | end 54 | 55 | defp package do 56 | [ 57 | maintainers: ["Brian Glusman"], 58 | licenses: ["MIT"], 59 | links: %{github: "https://github.com/bglusman/live_dashboard_history"}, 60 | files: ~w(lib LICENSE.md mix.exs README.md) 61 | ] 62 | end 63 | 64 | defp put_config(_) do 65 | Application.put_env(:live_dashboard_history, LiveDashboardHistory, 66 | router: DemoWeb.Router, 67 | metrics: DemoWeb.Telemetry 68 | ) 69 | end 70 | 71 | # Run "mix help deps" to learn about dependencies. 72 | defp deps do 73 | [ 74 | {:phoenix_live_dashboard, "~> 0.6"}, 75 | {:cbuf, "~> 0.7"}, 76 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 77 | {:norm, git: "https://github.com/keathley/norm.git", only: [:test]}, 78 | {:stream_data, "~> 0.5", only: [:test]} 79 | ] 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/live_dashboard_history_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LiveDashboardHistoryTest do 2 | use ExUnit.Case, async: false 3 | import ExUnitProperties, only: [check: 2, property: 2] 4 | 5 | import Norm 6 | 7 | alias LiveDashboardHistory.HistorySupervisor 8 | 9 | test "config validation" do 10 | assert HistorySupervisor.config_state(router: Router, metrics: Metrics) == 11 | {:ok, [router: Router, metrics: Metrics]} 12 | 13 | assert HistorySupervisor.config_state([%{router: Router, metrics: Metrics}]) == 14 | {:ok, [%{router: Router, metrics: Metrics}]} 15 | end 16 | 17 | test "config errors" do 18 | assert HistorySupervisor.config_state(rotuer: Router, metrics: Metrics) == 19 | {:error, :bad_config} 20 | 21 | assert HistorySupervisor.config_state([%{router: Router, metics: Metrics}]) == 22 | {:error, :bad_config} 23 | end 24 | 25 | @cast_sleep_length 5 26 | property "events are recorded" do 27 | telemetry_schema = 28 | schema(%{ 29 | name: 30 | coll_of(spec(is_atom() and fn atom -> !Regex.match?(~r/\./, to_string(atom)) end), 31 | min_count: 2, 32 | max_count: 5 33 | ), 34 | measurement: map_of(spec(is_atom()), spec(is_number()), min_count: 1, max_count: 3) 35 | }) 36 | 37 | metric_spec = one_of([:counter, :sum, :last_value, :summary, :distribution]) 38 | 39 | buffer_type = one_of([Cbuf.Queue, Cbuf.ETS, Cbuf.Map]) 40 | 41 | check all( 42 | telemetry <- gen(telemetry_schema), 43 | metric_fn <- gen(metric_spec), 44 | buffer <- gen(buffer_type) 45 | ) do 46 | router = :"router_#{System.monotonic_time()}" 47 | measures = Map.keys(telemetry.measurement) 48 | 49 | metrics = 50 | Enum.map(measures, fn measure -> 51 | apply(Telemetry.Metrics, metric_fn, ["#{Enum.join(telemetry.name, ".")}.#{measure}"]) 52 | end) 53 | 54 | {:ok, _pid} = LiveDashboardHistory.HistorySupervisor.start_child(metrics, 5, buffer, router) 55 | 56 | # allow time for handle_cast({:metrics, ...}) 57 | Process.sleep(@cast_sleep_length) 58 | :telemetry.execute(telemetry.name, telemetry.measurement, %{}) 59 | # allow time for handle_cast({:telemetry_metric, ...}) 60 | Process.sleep(@cast_sleep_length) 61 | 62 | Enum.map(metrics, fn metric -> 63 | assert LiveDashboardHistory.metrics_history(metric, router) != [] 64 | end) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/live_dashboard_history.ex: -------------------------------------------------------------------------------- 1 | defmodule LiveDashboardHistory do 2 | @external_resource "README.md" 3 | @moduledoc @external_resource 4 | |> File.read!() 5 | |> String.split("") 6 | |> Enum.fetch!(1) 7 | 8 | use GenServer 9 | alias Phoenix.LiveDashboard 10 | 11 | def metrics_history(metric, router_module) do 12 | case process_id(router_module) do 13 | nil -> [] 14 | pid -> GenServer.call(pid, {:data, metric}) 15 | end 16 | end 17 | 18 | def start_link([metrics, buffer_size, buffer_type, router_module]) do 19 | {:ok, pid} = 20 | GenServer.start_link(__MODULE__, [metrics, buffer_size, buffer_type, router_module]) 21 | 22 | Registry.register(LiveDashboardHistory.Registry, router_module, pid) 23 | {:ok, pid} 24 | end 25 | 26 | defp process_id(router_module) do 27 | case Registry.lookup(LiveDashboardHistory.Registry, router_module) do 28 | [{_supervisor_pid, child_pid}] -> child_pid 29 | [] -> nil 30 | end 31 | end 32 | 33 | def init([metrics, buffer_size, buffer_type, router_module]) do 34 | GenServer.cast(self(), {:metrics, metrics, buffer_size, buffer_type, router_module}) 35 | {:ok, %{}} 36 | end 37 | 38 | defp attach_handler(%{name: name_list} = metric, id, router_module) do 39 | :telemetry.attach( 40 | "#{inspect(name_list)}-history-#{id}-#{inspect(self())}", 41 | event(name_list), 42 | &__MODULE__.handle_event/4, 43 | {metric, router_module} 44 | ) 45 | end 46 | 47 | defp event(name_list) do 48 | Enum.slice(name_list, 0, length(name_list) - 1) 49 | end 50 | 51 | def handle_event(_event_name, data, metadata, {metric, router_module}) do 52 | if data = LiveDashboard.extract_datapoint_for_metric(metric, data, metadata) do 53 | case process_id(router_module) do 54 | nil -> 55 | :noop 56 | 57 | pid -> 58 | GenServer.cast(pid, {:telemetry_metric, data, metric}) 59 | end 60 | end 61 | end 62 | 63 | def handle_cast({:metrics, metrics, buffer_size, buffer_type, router_module}, _state) do 64 | metric_histories_map = 65 | metrics 66 | |> Enum.with_index() 67 | |> Enum.map(fn {metric, id} -> 68 | attach_handler(metric, id, router_module) 69 | {metric, buffer_type.new(buffer_size)} 70 | end) 71 | |> Map.new() 72 | 73 | {:noreply, {buffer_type, metric_histories_map}} 74 | end 75 | 76 | def handle_cast({:telemetry_metric, data, metric}, {buffer_type, state}) do 77 | {:noreply, {buffer_type, update_in(state[metric], &buffer_type.insert(&1, data))}} 78 | end 79 | 80 | def handle_call({:data, metric}, _from, {buffer_type, state}) do 81 | if history = state[metric] do 82 | {:reply, buffer_type.to_list(history), {buffer_type, state}} 83 | else 84 | {:reply, [], {buffer_type, state}} 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/history_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule LiveDashboardHistory.HistorySupervisor do 2 | use DynamicSupervisor 3 | require Logger 4 | 5 | @default_buffer_size 50 6 | @default_buffer_type Cbuf.Queue 7 | @env Mix.env() 8 | 9 | def start_link() do 10 | {:ok, pid} = DynamicSupervisor.start_link(__MODULE__, :ok, name: __MODULE__) 11 | config = Application.get_env(:live_dashboard_history, LiveDashboardHistory) 12 | 13 | with {:ok, raw_config} <- config_state(config), 14 | map_list_config <- normalize_config(raw_config) do 15 | for %{ 16 | router: router_module, 17 | metrics: metrics, 18 | buffer_size: buffer_size, 19 | buffer_type: buffer_type, 20 | skip_metrics: skip_metrics 21 | } <- map_list_config do 22 | history_metrics = get_metrics(metrics) -- skip_metrics 23 | start_child(history_metrics, buffer_size, buffer_type, router_module) 24 | end 25 | else 26 | {:error, :bad_config} -> 27 | unless @env == :test do 28 | Logger.warning( 29 | "WARNING: router and metrics config must be present for live_dashboard_history, router first if using keyword list" 30 | ) 31 | end 32 | 33 | {:error, :no_config} -> 34 | unless @env == :test do 35 | Logger.warning( 36 | "WARNING: router and metrics configuration required for live_dashboard_history" 37 | ) 38 | end 39 | end 40 | 41 | {:ok, pid} 42 | end 43 | 44 | def config_state(config) do 45 | case config do 46 | nil -> {:error, :no_config} 47 | [tuple | _] when is_tuple(tuple) -> validate(config, :tuple) 48 | [map | _] when is_map(map) -> validate(config, :map) 49 | _ -> {:error, :bad_config} 50 | end 51 | end 52 | 53 | def validate(config, :tuple) do 54 | if Keyword.has_key?(config, :router) and Keyword.has_key?(config, :metrics) do 55 | {:ok, config} 56 | else 57 | {:error, :bad_config} 58 | end 59 | end 60 | 61 | def validate(config, :map) do 62 | if Enum.all?(config, fn map -> Map.has_key?(map, :router) and Map.has_key?(map, :metrics) end) do 63 | {:ok, config} 64 | else 65 | {:error, :bad_config} 66 | end 67 | end 68 | 69 | defp get_metrics(metrics) when is_atom(metrics), do: apply(metrics, :metrics, []) 70 | 71 | defp get_metrics({metrics, function}) when is_atom(metrics) and is_atom(function), 72 | do: apply(metrics, function, []) 73 | 74 | defp get_metrics(metrics_fn) when is_function(metrics_fn), 75 | do: metrics_fn.() 76 | 77 | defp normalize_config([tuple | _rest] = config) when is_tuple(tuple) do 78 | [ 79 | %{ 80 | router: Keyword.fetch!(config, :router), 81 | metrics: Keyword.fetch!(config, :metrics), 82 | buffer_size: Keyword.get(config, :buffer_size, @default_buffer_size), 83 | buffer_type: Keyword.get(config, :buffer_type, @default_buffer_type), 84 | skip_metrics: Keyword.get(config, :skip_metrics, []) 85 | } 86 | ] 87 | end 88 | 89 | defp normalize_config([%{router: router, metrics: metrics} = current_config | rest_configs]) do 90 | list_item = fn -> 91 | %{ 92 | router: router, 93 | metrics: metrics, 94 | buffer_size: Map.get(current_config, :buffer_size, @default_buffer_size), 95 | buffer_type: Map.get(current_config, :buffer_type, @default_buffer_type), 96 | skip_metrics: Map.get(current_config, :skip_metrics, []) 97 | } 98 | end 99 | 100 | case rest_configs do 101 | [] -> 102 | [list_item.()] 103 | 104 | non_empty_configs -> 105 | [list_item.() | normalize_config(non_empty_configs)] 106 | end 107 | end 108 | 109 | def start_child(metrics, buffer_size, buffer_type, router_module) do 110 | DynamicSupervisor.start_child( 111 | __MODULE__, 112 | {LiveDashboardHistory, [metrics, buffer_size, buffer_type, router_module]} 113 | ) 114 | end 115 | 116 | @impl true 117 | def init(_args) do 118 | DynamicSupervisor.init(strategy: :one_for_one) 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /dev.exs: -------------------------------------------------------------------------------- 1 | # iex -S mix run dev.exs 2 | Logger.configure(level: :debug) 3 | 4 | # Configures the endpoint 5 | Application.put_env(:phoenix_live_dashboard, DemoWeb.Endpoint, 6 | url: [host: "localhost"], 7 | secret_key_base: "Hu4qQN3iKzTV4fJxhorPQlA/osH9fAMtbtjVS58PFgfw3ja5Z18Q/WSNR9wP4OfW", 8 | live_view: [signing_salt: "hMegieSe"], 9 | http: [port: System.get_env("PORT") || 4000], 10 | debug_errors: true, 11 | check_origin: false, 12 | pubsub_server: Demo.PubSub, 13 | watchers: [ 14 | node: [ 15 | "node_modules/webpack/bin/webpack.js", 16 | "--mode", 17 | "production", 18 | "--watch-stdin", 19 | cd: "assets" 20 | ] 21 | ], 22 | live_reload: [ 23 | patterns: [ 24 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 25 | ~r"lib/phoenix/live_dashboard/(live|views)/.*(ex)$", 26 | ~r"lib/phoenix/live_dashboard/templates/.*(ex)$" 27 | ] 28 | ] 29 | ) 30 | 31 | defmodule DemoWeb.Telemetry do 32 | import Telemetry.Metrics 33 | 34 | def metrics do 35 | [ 36 | # Phoenix Metrics 37 | last_value("phoenix.endpoint.stop.duration", 38 | description: "Last value of phoenix.endpoint response time", 39 | unit: {:native, :millisecond} 40 | ), 41 | counter("phoenix.endpoint.stop.duration", 42 | unit: {:native, :millisecond} 43 | ), 44 | summary("phoenix.endpoint.stop.duration", 45 | unit: {:native, :microsecond} 46 | ), 47 | last_value("phoenix.router_dispatch.stop.duration", 48 | tags: [:route], 49 | unit: {:native, :millisecond} 50 | ), 51 | counter("phoenix.router_dispatch.stop.duration", 52 | tags: [:route], 53 | unit: {:native, :millisecond} 54 | ), 55 | summary("phoenix.router_dispatch.stop.duration", 56 | tags: [:route], 57 | unit: {:native, :millisecond} 58 | ), 59 | 60 | # VM Metrics 61 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 62 | summary("vm.total_run_queue_lengths.total"), 63 | summary("vm.total_run_queue_lengths.cpu"), 64 | summary("vm.total_run_queue_lengths.io") 65 | ] 66 | end 67 | end 68 | 69 | defmodule DemoWeb.PageController do 70 | import Plug.Conn 71 | 72 | def init(opts), do: opts 73 | 74 | def call(conn, :index) do 75 | content(conn, """ 76 |
Hello, #{name}!
") 84 | end 85 | 86 | defp content(conn, content) do 87 | conn 88 | |> put_resp_header("content-type", "text/html") 89 | |> send_resp(200, "#{content}") 90 | end 91 | end 92 | 93 | defmodule DemoWeb.Router do 94 | use Phoenix.Router 95 | import Phoenix.LiveDashboard.Router 96 | 97 | pipeline :browser do 98 | plug(:fetch_session) 99 | end 100 | 101 | scope "/" do 102 | pipe_through(:browser) 103 | get("/", DemoWeb.PageController, :index) 104 | get("/hello", DemoWeb.PageController, :hello) 105 | get("/hello/:name", DemoWeb.PageController, :hello) 106 | 107 | live_dashboard("/dashboard", 108 | metrics: DemoWeb.Telemetry, 109 | env_keys: ["USER", "ROOTDIR"], 110 | metrics_history: {LiveDashboardHistory, :data, [__MODULE__]} 111 | ) 112 | end 113 | end 114 | 115 | defmodule DemoWeb.Endpoint do 116 | use Phoenix.Endpoint, otp_app: :phoenix_live_dashboard 117 | 118 | socket("/live", Phoenix.LiveView.Socket) 119 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 120 | 121 | plug(Phoenix.LiveReloader) 122 | plug(Phoenix.CodeReloader) 123 | 124 | plug(Phoenix.LiveDashboard.RequestLogger, 125 | param_key: "request_logger", 126 | cookie_key: "request_logger" 127 | ) 128 | 129 | plug(Plug.Session, 130 | store: :cookie, 131 | key: "_live_view_key", 132 | signing_salt: "/VEDsdfsffMnp5" 133 | ) 134 | 135 | plug(Plug.RequestId) 136 | plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) 137 | plug(DemoWeb.Router) 138 | end 139 | 140 | Application.ensure_all_started(:os_mon) 141 | Application.put_env(:phoenix, :serve_endpoints, true) 142 | 143 | Task.start(fn -> 144 | children = [ 145 | {Phoenix.PubSub, [name: Demo.PubSub, adapter: Phoenix.PubSub.PG2]}, 146 | DemoWeb.Endpoint, 147 | LiveDashboardHistory 148 | ] 149 | 150 | {:ok, _} = Supervisor.start_link(children, strategy: :one_for_one) 151 | Process.sleep(:infinity) 152 | end) 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveDashboardHistory 2 | 3 | [](https://github.com/bglusman/live_dashboard_history/actions/workflows/ci.yml) 4 | [](https://hex.pm/packages/live_dashboard_history) 5 | [](https://hexdocs.pm/live_dashboard_history/) 6 | [](https://hex.pm/packages/live_dashboard_history) 7 | [](https://github.com/bglusman/live_dashboard_history/blob/master/LICENSE) 8 | [](https://github.com/bglusman/live_dashboard_history/commits/master) 9 | 10 | 11 | Storage and integration layer to add recent metrics history on each client connection to Phoenix LiveDashboard 12 | 13 | LiveDashboard provides real-time performance monitoring and debugging tools for Phoenix developers. [See their docs here](https://hexdocs.pm/phoenix_live_dashboard) 14 | for details on using and configuring it in general, but if you're using it or know how, and want to have recent history for metrics charts, this library provides an ephemeral storage mechanism and integration with (a fork of, for now) Phoenix LiveDashboard. (once the fork, which provides a hook to allow providing history, is merged and released via Hex.pm, this library should be updated to not rely on fork and to work with any version of LiveDashboard after that. Until then, this library relies on the fork and you should remove or comment any explicit dependency on `phoenix_live_dashboard` in your `mix.exs` and only rely on this library). 15 | 16 | For an example working phoenix application with integration set up, see [live_dashboard_history_demo](https://github.com/bglusman/live_dashboard_history_demo) 17 | 18 | ## Installation 19 | 20 | The package can be installed via hex.pm by adding `live_dashboard_history` to your list of dependencies in `mix.exs`: 21 | 22 | ```elixir 23 | def deps do 24 | [ 25 | {:live_dashboard_history, "~> 0.1.0"} 26 | ] 27 | end 28 | ``` 29 | 30 | ## Configuration 31 | 32 | Only two pieces of configuration are needed; first, in each router you wish to expose live_dashboard, follow the normal guidelines for configuration like so: 33 | 34 | ```elixir 35 | live_dashboard "/dashboard", 36 | metrics: MyAppWeb.Telemetry, 37 | metrics_history: {LiveDashboardHistory, :metrics_history, [__MODULE__]} 38 | ``` 39 | (you may also pass in `:env_keys` and/or `:live_socket_path` config if you wish, but `:metrics` and `:metrics_history` are the minimum config that make sense with this library) 40 | 41 | Assuming you only have one router in your Phoenix app (or only one you want to expose LiveDashboard with history in), you can then add config into your `config.exs` like so: 42 | 43 | ```elixir 44 | config :live_dashboard_history, LiveDashboardHistory, 45 | router: MyAppWeb.Router, 46 | metrics: MyAppWeb.Telemetry 47 | ``` 48 | The router argument must evaluate to the same module as `__MODULE__` in the router where live_dashboard is configured. 49 | 50 | The metrics argument may match the metrics argument passed in live_dashboard configuration, a module as above, a tuple of `{module, function}`, or you may also pass an inline 0-arity anonymous function directly in config which must return metrics. 51 | 52 | You may also pass optional arguments `:buffer_size`, `:buffer_type` and/or `:skip_metrics` 53 | 54 | * `buffer_size` defaults to 50 55 | * `buffer_type` defaults to `Cbuf.Queue` 56 | * `skip_metrics` defaults to `[]` 57 | 58 | `:buffer_size` configures how many of each telemetry event are saved for each metric. 59 | 60 | `:buffer_type` is the module used for inserting chart data and transforming current state back to a list. It should implement the [Cbuf behavior](https://hexdocs.pm/cbuf/Cbuf.html) in theory, though in practice all that matters is that it implements `new/1`, `insert/2` and `to_list/1`. `new/1` will recieve the buffer size specified above, and insert will receive each chart data point prepared as a map. If you wished to store all chart data in Redis, for example, you could implement a module that does this, and returns as much data for each entry on `to_list/2` as desired based on your own logic, disregarding buffer_size. 61 | 62 | `:skip_metrics` allows you to save memory by filtering out metrics you don't care to have history available on in LiveDashboard. You may also just use a different metrics function than the one passed to live_dashboard config that returns only the metrics for which you wish to retain history. 63 | 64 | If you have multiple Routers and wish to expose LiveDashboard in each of them, you may pass in a list of maps instead of using a Keyword list as above, e.g. 65 | 66 | ```elixir 67 | config :live_dashboard_history, LiveDashboardHistory, [ 68 | %{ 69 | router: MyAppWeb.Router1, 70 | metrics: MyAppWeb.Telemetry1 71 | }, 72 | %{ 73 | router: MyAppWeb.Router2, 74 | metrics: MyAppWeb.Telemetry2, 75 | } 76 | ] 77 | ``` 78 | Each map may also have the optional keys `:buffer_size`, `:buffer_type` and `:skip_metrics`, and each metrics key may take any of the values outlines above. 79 | 80 | 81 | ## Contributing 82 | 83 | For those planning to contribute to this project, you can run a dev version of the dashboard with the following commands: 84 | 85 | $ mix setup 86 | $ mix dev 87 | 88 | Alternatively, run `iex -S mix dev` if you also want a shell. 89 | 90 | ## Copyright and License 91 | 92 | Copyright (c) 2020 Brian Glusman. 93 | 94 | This work is free. You can redistribute it and/or modify it under the 95 | terms of the MIT License. See the [LICENSE.md](./LICENSE.md) file for more details. 96 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "cbuf": {:hex, :cbuf, "0.7.1", "7d17cbf5ec7f11db1b80f1f2ee68cb1dd31e30e8ba738c9cb8065a5f966244b1", [:mix], [], "hexpm", "23181abfa84c78abd16f3274b696b022693ea6a9a675155c57404fe7f3dd2435"}, 3 | "earmark": {:hex, :earmark, "1.4.9", "837e4c1c5302b3135e9955f2bbf52c6c52e950c383983942b68b03909356c0d9", [:mix], [{:earmark_parser, ">= 1.4.9", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "0d72df7d13a3dc8422882bed5263fdec5a773f56f7baeb02379361cb9e5b0d8e"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, 5 | "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [: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", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, 6 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 7 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 8 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 9 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.1", "264fc6864936b59fedb3ceb89998c64e9bb91945faf1eb115d349b96913cc2ef", [:mix], [], "hexpm", "23c31d0ec38c97bf9adde35bc91bc8e1181ea5202881f48a192f4aa2d2cf4d59"}, 11 | "norm": {:git, "https://github.com/keathley/norm.git", "4d9ac55e0c0711227f9befc872a9f758458eaefd", []}, 12 | "phoenix": {:hex, :phoenix, "1.6.4", "bc9a757f0a4eac88e1e3501245a6259e74d30970df8c072836d755608dbc4c7d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b6cb3f31e3ea1049049852703eca794f7afdb0c1dc111d8f166ba032c103a80"}, 13 | "phoenix_html": {:hex, :phoenix_html, "3.1.0", "0b499df05aad27160d697a9362f0e89fa0e24d3c7a9065c2bd9d38b4d1416c09", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0c0a98a2cefa63433657983a2a594c7dee5927e4391e0f1bfd3a151d1def33fc"}, 14 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.2", "0769470265eb13af01b5001b29cb935f4710d6adaa1ffc18417a570a337a2f0f", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5bc6c6b38a2ca8b5020b442322fcee6afd5e641637a0b1fb059d4bd89bc58e7b"}, 15 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.5", "63f52a6f9f6983f04e424586ff897c016ecc5e4f8d1e2c22c2887af1c57215d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c5586e6a3d4df71b8214c769d4f5eb8ece2b4001711a7ca0f97323c36958b0e3"}, 16 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, 17 | "phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"}, 18 | "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, 19 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 20 | "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, 21 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, 22 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, 23 | } 24 | --------------------------------------------------------------------------------