Current section

9 Versions

Jump to

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…