Extract all HAProxy stats, return pretty HTML on web
This commit is contained in:
parent
ee541dad0a
commit
a51b59aa4c
7 changed files with 219 additions and 26 deletions
|
@ -1,7 +1,20 @@
|
|||
defmodule HAHandler do
|
||||
@moduledoc false
|
||||
@moduledoc """
|
||||
This top-level module only contains generic configuration helpers.
|
||||
"""
|
||||
|
||||
def http_port, do: Application.get_env(:ha_handler, :http_port)
|
||||
def haproxy_socket, do: Application.get_env(:ha_handler, :haproxy_socket)
|
||||
def acme_challenge_path, do: Application.get_env(:ha_handler, :acme_challenge_path)
|
||||
# Mix is not available in releases, and these things are static
|
||||
# anyway (@variables are evaluated at compile time).
|
||||
@otp_app Mix.Project.config()[:app]
|
||||
@version Mix.Project.config[:version]
|
||||
@env Mix.env
|
||||
|
||||
def http_port, do: Application.get_env(@otp_app, :http_port)
|
||||
def haproxy_socket, do: Application.get_env(@otp_app, :haproxy_socket)
|
||||
def acme_challenge_path, do: Application.get_env(@otp_app, :acme_challenge_path)
|
||||
def static_path(), do: Application.app_dir(@otp_app, "priv/static/")
|
||||
|
||||
def otp_app(), do: @otp_app
|
||||
def version(), do: @version
|
||||
def env(), do: @env
|
||||
end
|
||||
|
|
|
@ -34,28 +34,52 @@ defmodule HAHandler.HAProxy do
|
|||
end
|
||||
end
|
||||
|
||||
defp extract_stats(data) do
|
||||
extracted = for entry <- data do
|
||||
for mapping <- entry do
|
||||
case mapping do
|
||||
%{
|
||||
"id" => id,
|
||||
"objType" => type,
|
||||
"field" => %{"name" => name},
|
||||
"value" => %{"value" => value},
|
||||
} ->
|
||||
%{:id => id, :type => type, :field => name, :value => value}
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
extracted
|
||||
|> List.flatten()
|
||||
|> Enum.group_by(
|
||||
fn mapping -> {mapping.type, mapping.id} end,
|
||||
fn mapping -> %{mapping.field => mapping.value} end
|
||||
)
|
||||
|> Enum.map(
|
||||
fn {{type, id}, grouped_mappings} ->
|
||||
grouped_mappings
|
||||
|> Enum.reduce(fn l, r -> Map.merge(l,r) end)
|
||||
|> Map.put("type", type)
|
||||
|> Map.put("id", id)
|
||||
end
|
||||
)
|
||||
|> Enum.group_by(
|
||||
fn entry -> Map.get(entry, "type") end
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Executes and parse the output of the `show stats` HAProxy command, returning
|
||||
a list of Maps.
|
||||
|
||||
TODO: extract more informations from stats output.
|
||||
"""
|
||||
def get_stats() do
|
||||
case execute("show stat json") do
|
||||
{:ok, raw} ->
|
||||
case Poison.decode(raw) do
|
||||
{:ok, data} ->
|
||||
for entry <- data do
|
||||
attrs =
|
||||
entry
|
||||
|> Enum.filter(fn m -> get_in(m, ["field", "name"]) == "pxname" end)
|
||||
|> Enum.at(0)
|
||||
|
||||
%{
|
||||
type: attrs |> Map.get("objType"),
|
||||
name: get_in(attrs, ["value", "value"])
|
||||
}
|
||||
end
|
||||
extract_stats(data)
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
|
|
@ -3,16 +3,30 @@ defmodule HAHandler.Web.Controller do
|
|||
|
||||
alias HAHandler.HAProxy
|
||||
|
||||
def index(conn) do
|
||||
{:ok, hostname} = :inet.gethostname()
|
||||
@templates_dir "lib/ha_handler/web/templates"
|
||||
|
||||
stats = HAProxy.get_stats()
|
||||
reply = "OK #{hostname}"
|
||||
<> "\nPROXY: "
|
||||
<> inspect(stats)
|
||||
defp render(conn, template, assigns) do
|
||||
template_path = Path.join(@templates_dir, template)
|
||||
quoted = EEx.compile_file(template_path)
|
||||
{body, _binding} = Code.eval_quoted(quoted, assigns)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/plain")
|
||||
|> send_resp(200, reply)
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, body)
|
||||
end
|
||||
|
||||
def index(conn) do
|
||||
{:ok, hostname} = :net_adm.dns_hostname(:net_adm.localhost)
|
||||
|
||||
stats = HAProxy.get_stats()
|
||||
|
||||
assigns = [
|
||||
haproxy_stats: stats,
|
||||
hostname: hostname,
|
||||
otp_app: HAHandler.otp_app(),
|
||||
version: HAHandler.version(),
|
||||
env: HAHandler.env()
|
||||
]
|
||||
render(conn, "index.html.eex", assigns)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule HAHandler.Web.Router do
|
|||
|
||||
use Plug.Router
|
||||
|
||||
import HAHandler, only: [acme_challenge_path: 0]
|
||||
import HAHandler, only: [acme_challenge_path: 0, static_path: 0]
|
||||
|
||||
alias HAHandler.Web.Controller
|
||||
|
||||
|
@ -24,6 +24,10 @@ defmodule HAHandler.Web.Router do
|
|||
at: "/.well-known/acme-challenge/",
|
||||
from: acme_challenge_path()
|
||||
|
||||
plug Plug.Static,
|
||||
at: "/static",
|
||||
from: static_path()
|
||||
|
||||
plug :match
|
||||
plug :dispatch
|
||||
|
||||
|
|
103
lib/ha_handler/web/templates/index.html.eex
Normal file
103
lib/ha_handler/web/templates/index.html.eex
Normal file
|
@ -0,0 +1,103 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>[HA] <%= hostname %></title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div>
|
||||
<img id="logo" src="/static/logo.svg" />
|
||||
</div>
|
||||
|
||||
<h1>Recycled Cloud HA handler</h1>
|
||||
|
||||
<p>
|
||||
This service supervises the various components of
|
||||
the Recycled Cloud's High Availability
|
||||
infrastruture. Documentation and source code can be
|
||||
found on <a
|
||||
href="https://code.recycled.cloud/RecycledCloud/ha-handler">our
|
||||
software forge</a>.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>Handler</h2>
|
||||
|
||||
<%= otp_app %> <b>v<%= version %></b> (<%= env %>) running on <b><%= hostname %></b>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>HAProxy</h2>
|
||||
|
||||
<h3>Frontends</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Bytes in</th>
|
||||
<th>Bytes out</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for entry <- Map.get(haproxy_stats, "Frontend") do %>
|
||||
<tr>
|
||||
<td><%= entry["pxname"] %></td>
|
||||
<td><%= entry["status"] %></td>
|
||||
<td><%= entry["bin"] %></td>
|
||||
<td><%= entry["bout"] %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Backends</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>algo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for entry <- Map.get(haproxy_stats, "Backend") do %>
|
||||
<tr>
|
||||
<td><%= entry["iid"] %></td>
|
||||
<td><%= entry["pxname"] %></td>
|
||||
<td><%= entry["status"] %></td>
|
||||
<td><%= entry["algo"] %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Servers</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Mode</th>
|
||||
<th>Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for entry <- Map.get(haproxy_stats, "Server") do %>
|
||||
<tr>
|
||||
<td><%= entry["iid"] %></td>
|
||||
<td><%= entry["pxname"] %></td>
|
||||
<td><%= entry["status"] %></td>
|
||||
<td><%= entry["mode"] %></td>
|
||||
<td><%= entry["addr"] %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
34
priv/static/app.css
Normal file
34
priv/static/app.css
Normal file
|
@ -0,0 +1,34 @@
|
|||
main {
|
||||
width: 500px;
|
||||
margin-top: 50px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 5px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background-color: #D6EED6;
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: 200px;
|
||||
}
|
||||
|
1
priv/static/logo.svg
Normal file
1
priv/static/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 274.75 150.78"><defs><style>.cls-1{fill:#1e3c6c;}.cls-2{fill:#fff;stroke:#1e3c6c;stroke-miterlimit:10;}.cls-2,.cls-3{stroke-width:2px;}.cls-3{fill:#aecf70;stroke:#1a408c;stroke-miterlimit:6.4;}</style></defs><g id="logo_final_vectorisé_rvb" data-name="logo final vectorisé rvb"><path class="cls-1" d="M277.56,46.18a38.32,38.32,0,0,0-5.87-18.81c-4.49-6.83-11.08-10.75-18.09-10.78h0A19.6,19.6,0,0,0,243,19.71C229.31,11.29,211.1,6.52,191.93,6.45h-.35c-17.89,0-35.15,4.13-48.64,11.63-10.85,6-18.73,14-23,22.86a22.47,22.47,0,0,0-3.84-.34h0c-7.07,0-23.49,3.64-29.34,14.92l-.41.85a56.35,56.35,0,0,0-16.92-2.6h-.19c-25,0-44.69,15.27-44.76,34.82,0,9.7,5.08,19,14,25.6,8,5.86,18.35,9.2,29.38,9.48,12.81,20.14,45.36,33.42,83.62,33.56h.37c27.21,0,52.12-6.76,68.78-18.4a25.1,25.1,0,0,0,17.45,7.56h0c11.92,0,17.47-9.93,20.52-19.16a21.33,21.33,0,0,0,6.59,1.38h.09c18.62,0,33.82-19.05,33.9-42.52C299.3,67.93,290.51,52.31,277.56,46.18ZM265.64,112c-.93-.19-3.18-.95-8.58-3.33l-9.74-4.26-2.3,9.69c-3.25,13.65-5.7,15.69-6.84,15.69h0c-3.18,0-6.46-2.54-8.77-6.75l-5.32-9.26h0l-8.83-15.87-13.9,7.73,10.59,19c-13.68,9.95-36,16-60,16h-.32c-33.83-.12-63.77-12.21-71.21-28.75L78,106.53l-5.79.41c-1,.07-1.94.1-2.9.1h0c-7.88,0-15.49-2.31-20.88-6.26-2.71-2-7.25-6.22-7.23-12.13,0-9.9,12.9-18.23,28.12-18.23h.13c8.11,0,16.61,1.71,22,5.87l11.12-10.71c-.21-.3-1.27-1.84-1-2.4,1.38-2.67,9.4-5.94,14.5-5.94h0a7.69,7.69,0,0,1,4.33,1.52l10.77,7.48,2.18-12.94c2.86-17,28.4-30.21,58.22-30.21h.29c14.9.05,29.35,3.54,40.22,9.52l-11,14.93,13.35,9.82,12.39-18,3.22-3.9c1.18-1.43,2.46-2.25,3.49-2.25h0c1.3,0,2.88,1.23,4.22,3.27A22.82,22.82,0,0,1,261,48.75c0,.84-.06,1.68-.14,2.51l-.76,7.55,7.45,1.45c8.49,1.65,15.1,13,15.07,25.78C282.57,100,274.81,111.74,265.64,112Z" transform="translate(-24.5 -6.45)"/><path class="cls-2" d="M272,48.8a36.86,36.86,0,0,0-5.65-17.88c-4.34-6.61-10.74-10.41-17.55-10.44h0a19.11,19.11,0,0,0-10,2.9c-13.1-8-30.44-12.49-48.67-12.56h-.33c-17.1,0-33.61,3.95-46.52,11.13-10.29,5.73-17.8,13.23-21.89,21.68a23.23,23.23,0,0,0-3.43-.29h0C108.05,43.34,99.26,49.73,94,60a53.29,53.29,0,0,0-20.63-4.12h-.17c-24.08,0-43,14.71-43,33.55,0,9.37,4.9,18.38,13.52,24.71,7.6,5.58,17.49,8.79,28,9.09,12.36,19.2,43.41,31.84,79.86,32h.35c25.86,0,49.54-6.39,65.47-17.4A24.21,24.21,0,0,0,234,144.89h0c10,0,18.88-6.58,23.78-17q1,.07,2,.09H260c17.95,0,32.61-18.32,32.68-40.89C292.7,69.71,284.33,54.78,272,48.8Zm-12,62.51h0a10.53,10.53,0,0,1-4.13-.9l-9.13-4-2.3,9.69c-1.68,7-6,12.15-10.31,12.15h0c-2.89,0-5.88-2.34-8-6.22l-5.33-9.27-6.7-12L226,94.08a3,3,0,0,0-.8-5.49l-27.83-6.16a3,3,0,0,0-3.42,1.84l-10.44,27.44a3,3,0,0,0,4.22,3.65l12.44-6.92,8.45,15.19c-13,9.32-34,14.94-56.67,14.94h-.31c-32.07-.12-60.43-11.54-67.45-27.15l-2.38-5.3-5.8.41c-.92.06-1.88.11-2.76.09-7.43,0-14.59-2.16-19.66-5.89C51,98.88,46.82,95,46.84,89.5c0-9.21,12.1-17,26.38-17h.12c7.65,0,15,2.27,20,6.18l8.73,6.7,8.15,6.34-9.46,9.08a3,3,0,0,0,2.06,5.11H103l28.49-1.12a3,3,0,0,0,2.84-2.65L137.41,73a3,3,0,0,0-5-2.46L121.85,80.64l-14-10.89C110.18,63.9,114,60,117.79,60h0a7,7,0,0,1,3.9,1.38l10.77,7.48,2.18-12.93c2.69-16,26.84-28.45,55-28.45H190c13.95.05,27.49,3.26,37.73,8.79l-8.49,11.53L206,40.38a3,3,0,0,0-4.25,3.56l9.38,26.93a3,3,0,0,0,2.8,2,3.64,3.64,0,0,0,.56,0l28.83-5.54a3,3,0,0,0,.89-5.5l-11-6.15,9.2-12.5,3.29-4a4.62,4.62,0,0,1,3-2h0c1.09,0,2.51,1.13,3.7,2.94a21.21,21.21,0,0,1,3,11.44c0,.78,0,1.57-.12,2.34l-.77,7.56L262,62.86C269.88,64.39,276,75,276,87,276,100.2,268.62,111.31,260,111.31Z" transform="translate(-24.5 -6.45)"/></g><g id="cloud_txt_around" data-name="cloud txt around"><path class="cls-3" d="M-105.48-657.69" transform="translate(-24.5 -6.45)"/><path class="cls-3" d="M-1014.59-679.48" transform="translate(-24.5 -6.45)"/></g></svg>
|
After Width: | Height: | Size: 3.7 KiB |
Loading…
Reference in a new issue