Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c683bfc
Add RefreshPolicy and Refresh() to IProbe interface
Kicer86 Mar 15, 2026
6edce9e
Add per-probe refresh orchestration and SSE connect callback
Kicer86 Mar 15, 2026
e8042d3
Use journalctl with cursor-file for incremental kernel log reading
Kicer86 Mar 15, 2026
aaa3847
Remove automatic POST /refresh from monitor observe flow
Kicer86 Mar 15, 2026
2d94b06
Always publish cached status to new SSE clients
Kicer86 Apr 6, 2026
d102ba4
Make SSE connect callback non-blocking by deferring refresh to backgr…
Kicer86 Apr 6, 2026
7476c93
Use XDG_RUNTIME_DIR for journalctl cursor file instead of /tmp
Kicer86 Apr 6, 2026
e62f25c
Document intentional permanent error accumulation in journalctl mode
Kicer86 Apr 6, 2026
d543f60
Document lock acquisition hierarchy in agent main
Kicer86 Apr 6, 2026
66d5b97
Add unit test for SmartHealthAnalyzer::GetRefreshPolicy()
Kicer86 Apr 6, 2026
77ab20c
Make g_server an atomic pointer for signal-handler safety
Kicer86 Apr 6, 2026
01ce322
Add const and [[nodiscard]] to IProbe getter methods
Kicer86 Apr 7, 2026
2dba3f8
Replace thread-unsafe std::gmtime with gmtime_r
Kicer86 Apr 7, 2026
1f79f96
Use early return on BAD in NVMe health checks for consistency
Kicer86 Apr 7, 2026
a5a27ec
Use std::string_view for SmartHealthAnalyzer::profileFor parameter
Kicer86 Apr 7, 2026
e164898
Take HttpServer agentName by value and move into member
Kicer86 Apr 7, 2026
0c1c11b
Modernize main.cpp: ranges::transform, aggregate init, simplify chron…
Kicer86 Apr 7, 2026
7fe0667
Use std::ranges::all_of and std::ranges::find in HttpServer
Kicer86 Apr 7, 2026
13933be
Use std::ranges::any_of in SmartHealthAnalyzer
Kicer86 Apr 7, 2026
2fd9262
Use std::map::contains() in LinGeneralAnalyzer::GetStatus
Kicer86 Apr 7, 2026
937a331
Add defaulted operator== to RefreshPolicy
Kicer86 Apr 7, 2026
cbee8a6
Use gmtime_s on Windows for MSVC compatibility
Kicer86 Apr 7, 2026
1c4f423
Simplify a bit
Kicer86 May 1, 2026
bc02c07
Add Utils files
Kicer86 May 2, 2026
751f76d
Add todo
Kicer86 May 2, 2026
8d9762b
Print disks info in a nice form
Kicer86 May 2, 2026
7e611d2
Add log for publish
Kicer86 May 2, 2026
ca561ec
Use string_view
Kicer86 May 2, 2026
16c124a
Add missing include
Kicer86 May 2, 2026
c8c39a1
Drop too new constructions for Debian
Kicer86 May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions src/agent/HttpServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <iomanip>
#include <list>
#include <condition_variable>
#include <ranges>
#include <sstream>
#include <thread>

Expand All @@ -21,7 +22,7 @@ namespace
bool isValidDiskName(const std::string& name)
{
return !name.empty() && name.size() <= 64 &&
std::all_of(name.begin(), name.end(), [](char c) {
std::ranges::all_of(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_';
});
}
Expand All @@ -40,6 +41,7 @@ struct HttpServer::Impl
std::string lastRefreshed;

std::function<void()> refreshCallback;
std::function<void()> onClientConnectedCallback;

// Refresh cooldown
static constexpr int RefreshCooldownSeconds = 600; // 10 minutes
Expand Down Expand Up @@ -93,7 +95,7 @@ struct HttpServer::Impl
};


HttpServer::HttpServer(const std::string& agentName, unsigned int port)
HttpServer::HttpServer(std::string_view agentName, unsigned int port)
: m_impl(std::make_unique<Impl>())
{
m_impl->agentName = agentName;
Expand Down Expand Up @@ -134,14 +136,12 @@ HttpServer::HttpServer(const std::string& agentName, unsigned int port)

std::lock_guard lock(m_impl->dataMutex);

for (const auto& d : m_impl->disks)
auto it = std::ranges::find(m_impl->disks, name, &DiskInfo::GetName);
if (it != m_impl->disks.end())
{
if (d.GetName() == name)
{
nlohmann::json j = d;
res.set_content(j.dump(), "application/json");
return;
}
nlohmann::json j = *it;
res.set_content(j.dump(), "application/json");
return;
}
res.status = 404;
res.set_content(R"({"error":"disk not found"})", "application/json");
Expand Down Expand Up @@ -190,6 +190,10 @@ HttpServer::HttpServer(const std::string& agentName, unsigned int port)
m_impl->sseClients.push_back(client);
}

// Notify that a new monitor connected — may trigger conditional refresh
if (m_impl->onClientConnectedCallback)
m_impl->onClientConnectedCallback();

res.set_header("Cache-Control", "no-cache");
res.set_header("Connection", "keep-alive");

Expand Down Expand Up @@ -234,8 +238,14 @@ void HttpServer::setStatusData(GeneralHealth::Health overallHealth, std::vector<

const auto now = std::chrono::system_clock::now();
const auto time_t = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf{};
#ifdef _WIN32
gmtime_s(&tm_buf, &time_t);
#else
gmtime_r(&time_t, &tm_buf);
#endif
std::ostringstream oss;
oss << std::put_time(std::gmtime(&time_t), "%Y-%m-%dT%H:%M:%SZ");
oss << std::put_time(&tm_buf, "%Y-%m-%dT%H:%M:%SZ");
m_impl->lastRefreshed = oss.str();
}

Expand Down Expand Up @@ -269,3 +279,9 @@ void HttpServer::setRefreshCallback(std::function<void()> cb)
{
m_impl->refreshCallback = std::move(cb);
}


void HttpServer::setOnClientConnectedCallback(std::function<void()> cb)
{
m_impl->onClientConnectedCallback = std::move(cb);
}
7 changes: 5 additions & 2 deletions src/agent/HttpServer.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#pragma once

#include <string>
#include <string_view>
#include <vector>
#include <mutex>
#include <memory>
Expand All @@ -13,7 +13,7 @@
class HttpServer
{
public:
HttpServer(const std::string& agentName, unsigned int port);
HttpServer(std::string_view agentName, unsigned int port);
~HttpServer();

void setStatusData(GeneralHealth::Health overallHealth, std::vector<DiskInfo> disks);
Expand All @@ -25,6 +25,9 @@ class HttpServer
// Called to trigger data refresh; the callback does collection and calls setStatusData
void setRefreshCallback(std::function<void()> cb);

// Called when a new SSE client connects
void setOnClientConnectedCallback(std::function<void()> cb);

private:
struct Impl;
std::unique_ptr<Impl> m_impl;
Expand Down
20 changes: 18 additions & 2 deletions src/agent/IProbe.h
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
#pragma once

#include <chrono>
#include <vector>

#include <nlohmann/json.hpp>

#include "common/GeneralHealth.h"
#include "agent/Disk.h"


struct RefreshPolicy
{
std::chrono::seconds interval{0};
bool proactiveCollection = false;

bool operator==(const RefreshPolicy&) const = default;
};


class IProbe
{
public:
virtual ~IProbe() = default;
virtual GeneralHealth::Health GetStatus(const Disk& _disk) = 0;
virtual nlohmann::json GetRawData(const Disk& _disk) = 0;

[[nodiscard]] virtual RefreshPolicy GetRefreshPolicy() const = 0;
virtual void Refresh(const std::vector<Disk>& disks) = 0;

[[nodiscard]] virtual GeneralHealth::Health GetStatus(const Disk& _disk) const = 0;
[[nodiscard]] virtual nlohmann::json GetRawData(const Disk& _disk) const = 0;
};
54 changes: 40 additions & 14 deletions src/agent/SmartHealthAnalyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <algorithm>
#include <array>
#include <ranges>

namespace
{
Expand All @@ -27,7 +28,7 @@ namespace

bool isCriticalAta(uint8_t id, const std::string& name)
{
return std::any_of(criticalAttrs.begin(), criticalAttrs.end(),
return std::ranges::any_of(criticalAttrs,
[&](const CriticalAttr& ca) { return ca.id == id && name == ca.canonicalName; });
}

Expand Down Expand Up @@ -59,7 +60,23 @@ SmartHealthAnalyzer::SmartHealthAnalyzer(std::unique_ptr<ISmartReader> reader)
SmartHealthAnalyzer::~SmartHealthAnalyzer() = default;


const IVendorProfile& SmartHealthAnalyzer::profileFor(const std::string& vendor)
RefreshPolicy SmartHealthAnalyzer::GetRefreshPolicy() const
{
return {std::chrono::hours(4), false};
}


void SmartHealthAnalyzer::Refresh(const std::vector<Disk>& disks)
{
for (const auto& disk : disks)
{
m_cachedSmartData[disk.GetDeviceId()] = m_reader->ReadSMARTData(disk);
m_cachedTestStatus[disk.GetDeviceId()] = m_reader->ReadTestStatus(disk);
}
}


const IVendorProfile& SmartHealthAnalyzer::profileFor(std::string_view vendor)
{
static const GenericProfile generic;
static const SamsungProfile samsung;
Expand All @@ -74,9 +91,13 @@ const IVendorProfile& SmartHealthAnalyzer::profileFor(const std::string& vendor)
}


GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk)
GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk) const
{
const auto smart = m_reader->ReadSMARTData(disk);
auto it = m_cachedSmartData.find(disk.GetDeviceId());
if (it == m_cachedSmartData.end())
return GeneralHealth::UNKNOWN;

const auto& smart = it->second;
const auto& profile = profileFor(disk.GetVendor());

auto worst = GeneralHealth::GOOD;
Expand All @@ -98,18 +119,15 @@ GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk)
}

// Layer 2b: NVMe critical fields — non-zero raw means trouble
if (isCriticalNvme(attr.name))
{
if (attr.rawVal > 0)
worst = std::max(worst, GeneralHealth::BAD);
}
if (isCriticalNvme(attr.name) && attr.rawVal > 0)
return GeneralHealth::BAD;

// Layer 2c: NVMe wear indicator
if (isNvmePercentageUsed(attr.name))
{
if (attr.rawVal >= 100)
worst = std::max(worst, GeneralHealth::BAD);
else if (attr.rawVal >= nvmeWearWarningPercent)
return GeneralHealth::BAD;
if (attr.rawVal >= nvmeWearWarningPercent)
worst = std::max(worst, GeneralHealth::CHECK_STATUS);
}

Expand All @@ -126,9 +144,13 @@ GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk)
}


nlohmann::json SmartHealthAnalyzer::GetRawData(const Disk& disk)
nlohmann::json SmartHealthAnalyzer::GetRawData(const Disk& disk) const
{
const auto smart = m_reader->ReadSMARTData(disk);
auto it = m_cachedSmartData.find(disk.GetDeviceId());
if (it == m_cachedSmartData.end())
return nlohmann::json{{"type", "smart"}, {"attributes", nlohmann::json::array()}};

const auto& smart = it->second;

nlohmann::json attrs = nlohmann::json::array();
for (const auto& attr : smart.attributes)
Expand All @@ -145,7 +167,11 @@ nlohmann::json SmartHealthAnalyzer::GetRawData(const Disk& disk)

auto j = nlohmann::json{{"type", "smart"}, {"attributes", attrs}};

const auto testStatus = m_reader->ReadTestStatus(disk);
auto testIt = m_cachedTestStatus.find(disk.GetDeviceId());
SmartTestStatus testStatus;
if (testIt != m_cachedTestStatus.end())
testStatus = testIt->second;

j["selfTestStatus"] = {
{"running", testStatus.running},
{"percentRemaining", testStatus.percentRemaining},
Expand Down
17 changes: 14 additions & 3 deletions src/agent/SmartHealthAnalyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
#include "IProbe.h"
#include "ISmartReader.h"

#include <map>
#include <memory>
#include <string>
#include <string_view>

#include "common/SmartData.h"

class IVendorProfile;

Expand All @@ -13,11 +18,17 @@ class SmartHealthAnalyzer : public IProbe
explicit SmartHealthAnalyzer(std::unique_ptr<ISmartReader> reader);
~SmartHealthAnalyzer() override;

GeneralHealth::Health GetStatus(const Disk& disk) override;
nlohmann::json GetRawData(const Disk& disk) override;
RefreshPolicy GetRefreshPolicy() const override;
void Refresh(const std::vector<Disk>& disks) override;

GeneralHealth::Health GetStatus(const Disk& disk) const override;
nlohmann::json GetRawData(const Disk& disk) const override;

private:
std::unique_ptr<ISmartReader> m_reader;

static const IVendorProfile& profileFor(const std::string& vendor);
std::map<std::string, SmartData> m_cachedSmartData;
std::map<std::string, SmartTestStatus> m_cachedTestStatus;

static const IVendorProfile& profileFor(std::string_view vendor);
};
Loading
Loading