Extract all HAProxy stats, return pretty HTML on web

This commit is contained in:
Timothée Floure 2022-02-18 17:52:38 +01:00
parent ee541dad0a
commit a51b59aa4c
Signed by: tfloure
GPG key ID: 4502C902C00A1E12
7 changed files with 219 additions and 26 deletions

View file

@ -1,7 +1,20 @@
defmodule HAHandler do 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) # Mix is not available in releases, and these things are static
def haproxy_socket, do: Application.get_env(:ha_handler, :haproxy_socket) # anyway (@variables are evaluated at compile time).
def acme_challenge_path, do: Application.get_env(:ha_handler, :acme_challenge_path) @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 end

View file

@ -34,28 +34,52 @@ defmodule HAHandler.HAProxy do
end end
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 """ @doc """
Executes and parse the output of the `show stats` HAProxy command, returning Executes and parse the output of the `show stats` HAProxy command, returning
a list of Maps. a list of Maps.
TODO: extract more informations from stats output.
""" """
def get_stats() do def get_stats() do
case execute("show stat json") do case execute("show stat json") do
{:ok, raw} -> {:ok, raw} ->
case Poison.decode(raw) do case Poison.decode(raw) do
{:ok, data} -> {:ok, data} ->
for entry <- data do extract_stats(data)
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
{:error, err} -> {:error, err} ->
{:error, err} {:error, err}

View file

@ -3,16 +3,30 @@ defmodule HAHandler.Web.Controller do
alias HAHandler.HAProxy alias HAHandler.HAProxy
def index(conn) do @templates_dir "lib/ha_handler/web/templates"
{:ok, hostname} = :inet.gethostname()
stats = HAProxy.get_stats() defp render(conn, template, assigns) do
reply = "OK #{hostname}" template_path = Path.join(@templates_dir, template)
<> "\nPROXY: " quoted = EEx.compile_file(template_path)
<> inspect(stats) {body, _binding} = Code.eval_quoted(quoted, assigns)
conn conn
|> put_resp_content_type("text/plain") |> put_resp_content_type("text/html")
|> send_resp(200, reply) |> 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
end end

View file

@ -8,7 +8,7 @@ defmodule HAHandler.Web.Router do
use Plug.Router use Plug.Router
import HAHandler, only: [acme_challenge_path: 0] import HAHandler, only: [acme_challenge_path: 0, static_path: 0]
alias HAHandler.Web.Controller alias HAHandler.Web.Controller
@ -24,6 +24,10 @@ defmodule HAHandler.Web.Router do
at: "/.well-known/acme-challenge/", at: "/.well-known/acme-challenge/",
from: acme_challenge_path() from: acme_challenge_path()
plug Plug.Static,
at: "/static",
from: static_path()
plug :match plug :match
plug :dispatch plug :dispatch

View 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
View 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
View 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