Current section
9 Versions
Jump to
Current section
9 Versions
Compare versions
22
files changed
+352
additions
-462
deletions
| @@ -1,6 +1,8 @@ | |
| 1 1 | # RogerUi |
| 2 2 | |
| 3 | - **TODO: Add description** |
| 3 | + RogerUI provides a UI dashboard for [Exq](https://github.com/bettyblocks/roger), a job processing library which use [RabbitMQ](https://www.rabbitmq.com) for the [Elixir](http://elixir-lang.org) language. |
| 4 | + |
| 5 | + RogerUI allow you to see nodes, partitions and queues with various details levels. |
| 4 6 | |
| 5 7 | ## Installation |
| 6 8 | |
| @@ -10,7 +12,7 @@ by adding `roger_ui` to your list of dependencies in `mix.exs`: | |
| 10 12 | ```elixir |
| 11 13 | def deps do |
| 12 14 | [ |
| 13 | - {:roger_ui, "~> 0.1.0"} |
| 15 | + {:roger_ui, "~> 0.1.4"} |
| 14 16 | ] |
| 15 17 | end |
| 16 18 | ``` |
| @@ -18,4 +20,3 @@ end | |
| 18 20 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) |
| 19 21 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can |
| 20 22 | be found at [https://hexdocs.pm/roger_ui](https://hexdocs.pm/roger_ui). |
| 21 | - |
| @@ -5,12 +5,18 @@ | |
| 5 5 | {<<"elixir">>,<<"~> 1.5">>}. |
| 6 6 | {<<"files">>, |
| 7 7 | [<<"lib/roger_ui.ex">>,<<"lib/roger_ui/application.ex">>, |
| 8 | - <<"test/roger_ui_test.exs">>,<<"test/test_helper.exs">>,<<"web/router.ex">>, |
| 9 | - <<"priv/static/assets/app.js">>,<<"priv/static/index.html">>, |
| 10 | - <<"priv/static/templates/partitions.html">>, |
| 11 | - <<"priv/static/templates/queued.html">>, |
| 12 | - <<"priv/static/templates/running.html">>,<<"LICENSE">>,<<"mix.exs">>, |
| 13 | - <<"README.md">>]}. |
| 8 | + <<"lib/tests/roger_info_in_memory.ex">>,<<"lib/web/router.ex">>, |
| 9 | + <<"test/router_test.exs">>,<<"test/test_helper.exs">>, |
| 10 | + <<"priv/static/css/app.1587ebcf6808a73578aa9c20bcdf0e40.css">>, |
| 11 | + <<"priv/static/css/app.1587ebcf6808a73578aa9c20bcdf0e40.css.map">>, |
| 12 | + <<"priv/static/index.html">>, |
| 13 | + <<"priv/static/js/app.c59fd1bb1ebf78df8453.js">>, |
| 14 | + <<"priv/static/js/app.c59fd1bb1ebf78df8453.js.map">>, |
| 15 | + <<"priv/static/js/manifest.fe07ab5b9da24d850ba0.js">>, |
| 16 | + <<"priv/static/js/manifest.fe07ab5b9da24d850ba0.js.map">>, |
| 17 | + <<"priv/static/js/vendor.658653677545d353f4df.js">>, |
| 18 | + <<"priv/static/js/vendor.658653677545d353f4df.js.map">>,<<"LICENSE">>, |
| 19 | + <<"mix.exs">>,<<"README.md">>]}. |
| 14 20 | {<<"licenses">>,[<<"MIT">>]}. |
| 15 21 | {<<"links">>,[{<<"Github">>,<<"https://github.com/Spadavecchia/roger_ui">>}]}. |
| 16 22 | {<<"maintainers">>,[<<"Antonio Abella">>,<<"Paul Engel">>]}. |
| @@ -19,7 +25,7 @@ | |
| 19 25 | [[{<<"app">>,<<"roger">>}, |
| 20 26 | {<<"name">>,<<"roger">>}, |
| 21 27 | {<<"optional">>,false}, |
| 22 | - {<<"requirement">>,<<"~> 1.3">>}], |
| 28 | + {<<"requirement">>,<<"~> 1.3.0">>}], |
| 23 29 | [{<<"app">>,<<"plug">>}, |
| 24 30 | {<<"name">>,<<"plug">>}, |
| 25 31 | {<<"optional">>,false}, |
| @@ -28,4 +34,4 @@ | |
| 28 34 | {<<"name">>,<<"cowboy">>}, |
| 29 35 | {<<"optional">>,false}, |
| 30 36 | {<<"requirement">>,<<"~> 1.0">>}]]}. |
| 31 | - {<<"version">>,<<"0.1.4">>}. |
| 37 | + {<<"version">>,<<"0.1.6">>}. |
| @@ -9,7 +9,7 @@ defmodule RogerUi.Application do | |
| 9 9 | run_server? = Application.get_env(:roger_ui, :server, true) |
| 10 10 | web_port = Application.get_env(:roger_ui, :web_port, 4040) |
| 11 11 | if run_server? do |
| 12 | - case Plug.Adapters.Cowboy.http(RogerUi.RouterPlug, [], port: web_port) do |
| 12 | + case Plug.Adapters.Cowboy.http(RogerUi.Web.RouterPlug, [], port: web_port) do |
| 13 13 | {:ok, _} -> |
| 14 14 | IO.puts("Starting RogerUi server on port #{web_port}") |
| 15 15 | {:error, :eaddrinuse} -> |
| @@ -0,0 +1,42 @@ | |
| 1 | + defmodule RogerUi.Tests.RogerInfoInMemory do |
| 2 | + @moduledoc """ |
| 3 | + Mocks calls to Roger.Info API |
| 4 | + """ |
| 5 | + |
| 6 | + def partitions do |
| 7 | + [ |
| 8 | + "server@127.0.0.1": %{ |
| 9 | + running: %{ |
| 10 | + "roger_test_partition_1" => %{ |
| 11 | + default: %{consumer_count: 1, max_workers: 10, message_count: 740, paused: false}, |
| 12 | + fast: %{consumer_count: 1, max_workers: 10, message_count: 740, paused: false}, |
| 13 | + other: %{consumer_count: 1, max_workers: 2, message_count: 0, paused: false} |
| 14 | + } |
| 15 | + }, |
| 16 | + waiting: %{ |
| 17 | + "roger_test_partition_2" => %{ |
| 18 | + default: %{consumer_count: 1, max_workers: 10, message_count: 740, paused: false}, |
| 19 | + fast: %{consumer_count: 1, max_workers: 10, message_count: 740, paused: false}, |
| 20 | + other: %{consumer_count: 1, max_workers: 2, message_count: 0, paused: false} |
| 21 | + } |
| 22 | + } |
| 23 | + }, |
| 24 | + "watcher@127.0.0.1": %{ |
| 25 | + running: %{ |
| 26 | + "roger_test_partition_3" => %{ |
| 27 | + default: %{consumer_count: 1, max_workers: 10, message_count: 740, paused: false}, |
| 28 | + fast: %{consumer_count: 1, max_workers: 10, message_count: 740, paused: false}, |
| 29 | + other: %{consumer_count: 1, max_workers: 2, message_count: 0, paused: false} |
| 30 | + } |
| 31 | + }, |
| 32 | + waiting: %{ |
| 33 | + "roger_test_partition_4" => %{ |
| 34 | + default: %{consumer_count: 1, max_workers: 10, message_count: 740, paused: false}, |
| 35 | + fast: %{consumer_count: 1, max_workers: 10, message_count: 740, paused: false}, |
| 36 | + other: %{consumer_count: 1, max_workers: 2, message_count: 0, paused: false} |
| 37 | + } |
| 38 | + } |
| 39 | + } |
| 40 | + ] |
| 41 | + end |
| 42 | + end |
| @@ -0,0 +1,187 @@ | |
| 1 | + defmodule RogerUi.Web.RouterPlug do |
| 2 | + @moduledoc """ |
| 3 | + Plug to expose RogerUi API |
| 4 | + """ |
| 5 | + |
| 6 | + require Logger |
| 7 | + alias RogerUi.Web.RouterPlug.Router |
| 8 | + |
| 9 | + alias Roger.Info |
| 10 | + |
| 11 | + def init(opts), do: opts |
| 12 | + |
| 13 | + def call(conn, opts) do |
| 14 | + Router.call(conn, Router.init(opts)) |
| 15 | + end |
| 16 | + |
| 17 | + defmodule Router do |
| 18 | + @roger_info_api Application.get_env(:roger_ui, :roger_info_api, Roger.Info) |
| 19 | + |
| 20 | + @moduledoc """ |
| 21 | + Plug Router extension |
| 22 | + """ |
| 23 | + |
| 24 | + import Plug.Conn |
| 25 | + use Plug.Router |
| 26 | + |
| 27 | + plug( |
| 28 | + Plug.Static, |
| 29 | + at: "/", |
| 30 | + from: :roger_ui, |
| 31 | + only: ~w(css js) |
| 32 | + ) |
| 33 | + |
| 34 | + plug(:match) |
| 35 | + plug(:dispatch) |
| 36 | + |
| 37 | + defp no_content_response(ncr_conn) do |
| 38 | + ncr_conn |
| 39 | + |> put_resp_header("access-control-allow-origin", "*") |
| 40 | + |> send_resp(204, "") |
| 41 | + |> halt() |
| 42 | + end |
| 43 | + |
| 44 | + defp json_response(j_conn, json) do |
| 45 | + j_conn |
| 46 | + |> put_resp_header("access-control-allow-origin", "*") |
| 47 | + |> put_resp_header("content-type", "application/json") |
| 48 | + |> send_resp(200, json) |
| 49 | + |> halt() |
| 50 | + end |
| 51 | + |
| 52 | + defp named_queues(partition, name) do |
| 53 | + queues = partition[name] |
| 54 | + |
| 55 | + queues |
| 56 | + |> Map.keys() |
| 57 | + |> Enum.map(fn qn -> |
| 58 | + %{ |
| 59 | + qualified_queue_name: Roger.Queue.make_name(name, qn), |
| 60 | + queue_name: qn, |
| 61 | + partition_name: name, |
| 62 | + paused: if(partition[name][qn].paused, do: "paused", else: "running"), |
| 63 | + count: partition[name][qn].message_count |
| 64 | + } |
| 65 | + end) |
| 66 | + end |
| 67 | + |
| 68 | + defp queues_partition(partitions, name) do |
| 69 | + partition = partitions[name] |
| 70 | + |
| 71 | + partition |
| 72 | + |> Map.keys() |
| 73 | + |> Enum.reduce([], fn k, l -> [named_queues(partition, k) | l] end) |
| 74 | + end |
| 75 | + |
| 76 | + defp extract_queues(node) do |
| 77 | + partitions = elem(node, 1) |
| 78 | + |
| 79 | + partitions |
| 80 | + |> Map.keys() |
| 81 | + |> Enum.reduce([], fn k, l -> [queues_partition(partitions, k) | l] end) |
| 82 | + end |
| 83 | + |
| 84 | + def paginated_queues(nodes, page_size, page_number, filter \\ "") do |
| 85 | + page_size = if page_size > 100, do: 100, else: page_size |
| 86 | + |
| 87 | + queues = |
| 88 | + nodes |
| 89 | + |> Enum.map(fn node -> extract_queues(node) end) |
| 90 | + |> List.flatten() |
| 91 | + |
| 92 | + queues = |
| 93 | + if filter == "" do |
| 94 | + queues |
| 95 | + else |
| 96 | + Enum.filter(queues, fn q -> String.contains?(q.qualified_queue_name, filter) end) |
| 97 | + end |
| 98 | + |
| 99 | + %{ |
| 100 | + queues: Enum.slice(queues, page_size * (page_number - 1), page_size), |
| 101 | + total: Enum.count(queues) |
| 102 | + } |
| 103 | + end |
| 104 | + |
| 105 | + # {nodes: {:node_name_1 {partition_name_1: {queue_name_1: {...}}}}}} |
| 106 | + get "/api/nodes" do |
| 107 | + nodes = |
| 108 | + Info.partitions() |
| 109 | + |> Enum.into(%{}) |
| 110 | + |
| 111 | + {:ok, json} = Poison.encode(%{nodes: nodes}) |
| 112 | + json_response(conn, json) |
| 113 | + end |
| 114 | + |
| 115 | + get "/api/jobs/:partition_name/:queue_name" do |
| 116 | + roger_now = Roger.now() |
| 117 | + queued_jobs = Info.queued_jobs(partition_name, queue_name) |
| 118 | + |
| 119 | + running_jobs = |
| 120 | + partition_name |
| 121 | + |> Info.running_jobs() |
| 122 | + |> Enum.into(%{}) |
| 123 | + |
| 124 | + {:ok, json} = |
| 125 | + Poison.encode(%{ |
| 126 | + roger_now: roger_now, |
| 127 | + queued_jobs: queued_jobs, |
| 128 | + running_jobs: running_jobs |
| 129 | + }) |
| 130 | + |
| 131 | + json_response(conn, json) |
| 132 | + end |
| 133 | + |
| 134 | + get "/api/queues/:page_size/:page_number" do |
| 135 | + queues = |
| 136 | + @roger_info_api.partitions() |
| 137 | + |> paginated_queues(page_size |> String.to_integer(), page_number |> String.to_integer()) |
| 138 | + |
| 139 | + {:ok, json} = Poison.encode(queues) |
| 140 | + json_response(conn, json) |
| 141 | + end |
| 142 | + |
| 143 | + get "/api/queues/:page_size/:page_number/:filter" do |
| 144 | + queues = |
| 145 | + @roger_info_api.partitions() |
| 146 | + |> paginated_queues( |
| 147 | + page_size |> String.to_integer(), |
| 148 | + page_number |> String.to_integer(), |
| 149 | + filter |
| 150 | + ) |
| 151 | + |
| 152 | + {:ok, json} = Poison.encode(queues) |
| 153 | + json_response(conn, json) |
| 154 | + end |
| 155 | + |
| 156 | + # NOTE atoms are not garbage collected, maybe an issue, maybe not: |
| 157 | + # https://engineering.klarna.com/monitoring-erlang-atoms-c1d6a741328e |
| 158 | + put "api/queues/pause/:partition_name/:queue_name" do |
| 159 | + Roger.Partition.Global.queue_pause(partition_name, String.to_atom(queue_name)) |
| 160 | + no_content_response(conn) |
| 161 | + end |
| 162 | + |
| 163 | + put "api/queues/resume/:partition_name/:queue_name" do |
| 164 | + Roger.Partition.Global.queue_resume(partition_name, String.to_atom(queue_name)) |
| 165 | + no_content_response(conn) |
| 166 | + end |
| 167 | + |
| 168 | + delete "api/queues/:partition_name/:queue_name" do |
| 169 | + Roger.Queue.purge(partition_name, queue_name) |
| 170 | + no_content_response(conn) |
| 171 | + end |
| 172 | + |
| 173 | + delete "api/jobs/:partition_name/:job_id" do |
| 174 | + Roger.Partition.Global.cancel_job(partition_name, job_id) |
| 175 | + no_content_response(conn) |
| 176 | + end |
| 177 | + |
| 178 | + match _ do |
| 179 | + index_path = Path.join([Application.app_dir(:roger_ui), "priv/static/index.html"]) |
| 180 | + |
| 181 | + conn |
| 182 | + |> put_resp_header("content-type", "text/html") |
| 183 | + |> send_file(200, index_path) |
| 184 | + |> halt() |
| 185 | + end |
| 186 | + end |
| 187 | + end |
Loading more files…