#include "app.h"
#include <chrono>
#include <exception>
#include <functional>
#include <string>
#include <string_view>
#include "embedded_assets.h"
#include <inja/inja.hpp>
#include <nlohmann/json.hpp>
namespace
{
enum class UiStatus
{
GOOD,
BAD,
OTHER,
NA
};
struct StatusPresentation
{
std::string class_name;
std::string label;
};
StatusPresentation present(UiStatus status)
{
switch(status)
{
case UiStatus::GOOD:
return {"good", "Good"};
case UiStatus::BAD:
return {"bad", "Bad"};
case UiStatus::OTHER:
return {"other", "Other"};
case UiStatus::NA:
return {"na", "N/A"};
}
std::terminate();
}
std::int64_t nowUnixSeconds()
{
return std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
bool isStale(const ServiceConfig& config, std::int64_t timestamp,
std::int64_t now)
{
if(timestamp >= now)
{
return false;
}
auto interval = std::chrono::duration_cast<std::chrono::seconds>(
config.interval).count();
if(interval <= 0)
{
interval = 1;
}
const auto age = now - timestamp;
return age > interval && age - interval > interval;
}
UiStatus deriveStatus(const ServiceConfig& config,
const std::optional<StatusRecord>& record,
std::int64_t now)
{
if(!record || isStale(config, record->timestamp, now))
{
return UiStatus::NA;
}
switch(record->status)
{
case ProbeStatus::GOOD:
return UiStatus::GOOD;
case ProbeStatus::BAD:
return UiStatus::BAD;
case ProbeStatus::OTHER:
return UiStatus::OTHER;
}
return UiStatus::OTHER;
}
const EmbeddedAsset* findAsset(std::string_view path)
{
for(const auto& asset : embeddedAssets())
{
if(asset.path == path)
{
return &asset;
}
}
return nullptr;
}
mw::E<nlohmann::json> makePageData(const Configuration& configuration,
DataSourceInterface& data_source)
{
const auto now = nowUnixSeconds();
nlohmann::json data = {
{"groups", nlohmann::json::array()},
{"no_groups", configuration.groups.empty()}
};
for(const auto& group : configuration.groups)
{
nlohmann::json group_data = {
{"name", group.name},
{"services", nlohmann::json::array()},
{"no_services", group.services.empty()}
};
for(const auto& config : group.services)
{
auto latest = data_source.latest(config.id);
if(!latest)
{
return std::unexpected(latest.error());
}
const auto& record = *latest;
const auto status = deriveStatus(config, record, now);
const auto presentation = present(status);
nlohmann::json service_data = {
{"id", config.id},
{"name", config.name},
{"description", config.description},
{"status_class", presentation.class_name},
{"status_label", presentation.label},
{"has_url", config.url.has_value() && !config.url->empty()},
{"url", config.url.value_or("")},
{"has_result", record.has_value()},
{"timestamp", record ? record->timestamp : 0},
{"duration_microsecond", record
? record->duration_microsecond : 0}
};
group_data["services"].push_back(std::move(service_data));
}
data["groups"].push_back(std::move(group_data));
}
return data;
}
}
App::App(const ListenAddress& listen, Configuration configuration,
DataSourceInterface& data_source)
: mw::HTTPServer(listen), configuration(std::move(configuration)),
data_source(data_source)
{}
void App::setup()
{
server.set_exception_handler(unexpectedException);
server.Get("/", std::bind_front(&App::serveIndex, this));
server.Get("/static/.*", serveStatic);
}
void App::serveIndex([[maybe_unused]] const Request& request,
Response& response) const
{
auto data = makePageData(configuration, data_source);
if(!data)
{
response.status = 500;
response.set_content("Failed to read service status: " +
data.error().msg() + "\n",
"text/plain; charset=utf-8");
return;
}
const auto* index = findAsset("/");
if(index == nullptr)
{
std::terminate();
}
inja::Environment environment;
environment.set_html_autoescape(true);
const auto html = environment.render(std::string(index->content), *data);
response.set_content(html, "text/html; charset=utf-8");
}
void App::serveStatic(const Request& request, Response& response)
{
for(const auto& asset : embeddedAssets())
{
if(request.path == asset.path)
{
response.set_content(asset.content.data(), asset.content.size(),
std::string(asset.content_type));
return;
}
}
response.status = 404;
response.set_content("Not found\n", "text/plain; charset=utf-8");
}
void App::unexpectedException([[maybe_unused]] const Request& request,
[[maybe_unused]] Response& response,
[[maybe_unused]] std::exception_ptr exception)
noexcept
{
std::terminate();
}