You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dvmhost/src/fne/network/TrafficNetwork.MetricsLoggi...

1719 lines
61 KiB

// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "common/Thread.h"
#include "network/TrafficNetwork.h"
#include <atomic>
#include <chrono>
#include <cstring>
#include <limits>
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
using namespace network;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define SQLITE_BATCH_SIZE 256U
#define SQLITE_MAX_QUEUE_DEPTH 32768U
#define SQLITE_BATCH_WAIT_MS 10U
static const uint64_t NS_PER_SECOND = 1000000000ULL;
static const uint64_t NS_PER_MINUTE = 60ULL * NS_PER_SECOND;
static const uint64_t NS_PER_DAY = 86400ULL * NS_PER_SECOND;
// ---------------------------------------------------------------------------
// Static Class Members
// ---------------------------------------------------------------------------
std::atomic<int32_t> TrafficNetwork::MetricsLogging::s_totalActiveCalls {0};
std::atomic<uint64_t> TrafficNetwork::MetricsLogging::s_totalCallsProcessed {0U};
std::atomic<uint64_t> TrafficNetwork::MetricsLogging::s_totalCallCollisions {0U};
std::atomic<uint64_t> TrafficNetwork::MetricsLogging::s_totalCallSwitches {0U};
namespace {
static const char* SQLITE_COUNTER_CALLS_PROCESSED = "total_calls_processed";
static const char* SQLITE_COUNTER_CALL_COLLISIONS = "total_call_collisions";
static const char* SQLITE_COUNTER_CALL_SWITCHES = "total_call_switches";
/**
* @brief Get the current time in nanoseconds since the epoch.
* @return The current time in nanoseconds since the epoch.
*/
static inline uint64_t nowNs()
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
/**
* @brief Types of metric events.
*/
enum class MetricEventType {
ACTIVITY,
DIAG,
CALL_EVENT,
CALL_ERROR_EVENT,
CALL_COLLISION_EVENT,
TSBK_EVENT,
CSBK_EVENT,
COUNTER_EVENT
};
// ---------------------------------------------------------------------------
// Structure Declaration
// ---------------------------------------------------------------------------
/**
* @brief Represents a metric event with various attributes.
*/
struct MetricEvent {
MetricEventType type;
uint64_t tsNs;
uint32_t peerId;
uint32_t streamId;
uint32_t srcId;
uint32_t dstId;
uint64_t durationMs;
uint8_t slotNo;
bool hasSlot;
std::string mode;
std::string identity;
std::string message;
std::string raw;
std::string lco;
std::string rawDescription;
std::string counterKey;
uint64_t counterValue;
uint32_t rxPeerId;
uint32_t rxStreamId;
uint32_t rxSrcId;
uint32_t rxDstId;
uint8_t rxSlot;
bool hasRxSlot;
/**
* @brief Default constructor for MetricEvent. Initializes all members to default values.
*/
MetricEvent() :
type(MetricEventType::ACTIVITY),
tsNs(0U),
peerId(0U),
streamId(0U),
srcId(0U),
dstId(0U),
durationMs(0U),
slotNo(0U),
hasSlot(false),
counterKey(),
counterValue(0U),
rxPeerId(0U),
rxStreamId(0U),
rxSrcId(0U),
rxDstId(0U),
rxSlot(0U),
hasRxSlot(false)
{
/* stub */
}
};
// ---------------------------------------------------------------------------
// Structure Declaration
// ---------------------------------------------------------------------------
/**
* @brief Holds prepared SQLite statements for different metric event types.
*/
struct SQLiteStatements {
sqlite3_stmt* activity;
sqlite3_stmt* diag;
sqlite3_stmt* callEvent;
sqlite3_stmt* callErrorEvent;
sqlite3_stmt* callCollisionEvent;
sqlite3_stmt* tsbkEvent;
sqlite3_stmt* csbkEvent;
sqlite3_stmt* counterUpsert;
/**
* @brief Default constructor for SQLiteStatements. Initializes all statement pointers to nullptr.
*/
SQLiteStatements() :
activity(nullptr),
diag(nullptr),
callEvent(nullptr),
callErrorEvent(nullptr),
callCollisionEvent(nullptr),
tsbkEvent(nullptr),
csbkEvent(nullptr),
counterUpsert(nullptr)
{
/* stub */
}
};
// ---------------------------------------------------------------------------
// Structure Declaration
// ---------------------------------------------------------------------------
/**
* @brief Lock-free queue for metric events.
*/
struct LockFreeMetricQueue {
std::vector<std::atomic<MetricEvent*>> slots;
std::atomic<size_t> head;
std::atomic<size_t> tail;
std::atomic<size_t> pendingCount;
size_t maxDepth;
/**
* @brief Constructor for LockFreeMetricQueue.
* @param depth The maximum depth of the queue.
*/
explicit LockFreeMetricQueue(size_t depth) :
slots(depth),
head(0U),
tail(0U),
pendingCount(0U),
maxDepth(depth)
{
for (auto& slot : slots) {
slot.store(nullptr, std::memory_order_relaxed);
}
}
/**
* @brief Attempts to push a metric event into the queue.
* @param event The metric event to push.
* @return True if the event was successfully pushed, false otherwise.
*/
bool tryPush(MetricEvent&& event)
{
const size_t capacity = slots.size();
if (capacity == 0U || maxDepth == 0U) {
return false;
}
size_t tailValue = tail.load(std::memory_order_relaxed);
// attempt to reserve a slot in the queue
while (true) {
const size_t headValue = head.load(std::memory_order_acquire);
const size_t pending = tailValue - headValue;
if (pending >= maxDepth) {
return false;
}
if (tail.compare_exchange_weak(tailValue, tailValue + 1U, std::memory_order_relaxed, std::memory_order_relaxed)) {
const size_t slotIndex = tailValue % capacity;
MetricEvent* owned = new MetricEvent(std::move(event));
slots[slotIndex].store(owned, std::memory_order_release);
pendingCount.fetch_add(1U, std::memory_order_release);
return true;
}
}
}
/**
* @brief Attempts to pop a metric event from the queue.
* @param out The metric event to populate with the popped event.
* @return True if an event was successfully popped, false otherwise.
*/
bool tryPop(MetricEvent& out)
{
const size_t capacity = slots.size();
if (capacity == 0U) {
return false;
}
const size_t headValue = head.load(std::memory_order_relaxed);
const size_t tailValue = tail.load(std::memory_order_acquire);
if (headValue >= tailValue) {
return false;
}
const size_t slotIndex = headValue % capacity;
MetricEvent* item = slots[slotIndex].load(std::memory_order_acquire);
if (item == nullptr) {
return false;
}
out = std::move(*item);
delete item;
slots[slotIndex].store(nullptr, std::memory_order_release);
head.store(headValue + 1U, std::memory_order_release);
pendingCount.fetch_sub(1U, std::memory_order_release);
return true;
}
};
// ---------------------------------------------------------------------------
// Structure Declaration
// ---------------------------------------------------------------------------
/**
* @brief Holds the state for the SQLite writer, including the queue, worker thread, and prepared statements.
*/
struct SQLiteWriterState {
LockFreeMetricQueue queue;
thread_t workerThread;
std::atomic<bool> workerStarted;
sqlite3* db;
SQLiteStatements statements;
std::atomic<bool> stopRequested;
uint64_t droppedCount;
uint32_t retentionDays;
uint32_t pruneIntervalMinutes;
uint64_t nextPruneNs;
/**
* @brief Default constructor for SQLiteWriterState. Initializes all members to default values.
*/
SQLiteWriterState() :
queue(SQLITE_MAX_QUEUE_DEPTH),
workerThread(),
workerStarted(false),
db(nullptr),
statements(),
stopRequested(false),
droppedCount(0U),
retentionDays(0U),
pruneIntervalMinutes(0U),
nextPruneNs(0U)
{
/* stub */
}
};
static std::atomic<SQLiteWriterState*> g_sqliteWriterState;
static std::shared_ptr<SQLiteWriterState> g_sqliteWriterStateHolder;
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
/**
* @brief Binds a text value to a SQLite statement at the specified index.
* @param stmt The SQLite statement.
* @param idx The index of the parameter to bind.
* @param value The text value to bind.
* @return SQLITE_OK if the binding was successful, otherwise an error code.
*/
static inline int bindText(sqlite3_stmt* stmt, int idx, const std::string& value)
{
return sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
}
/**
* @brief Finalizes all prepared SQLite statements and sets their pointers to nullptr.
* @param stmts The SQLiteStatements structure containing the prepared statements.
*/
static void finalizeStatements(SQLiteStatements& stmts)
{
if (stmts.activity != nullptr) {
sqlite3_finalize(stmts.activity);
stmts.activity = nullptr;
}
if (stmts.diag != nullptr) {
sqlite3_finalize(stmts.diag);
stmts.diag = nullptr;
}
if (stmts.callEvent != nullptr) {
sqlite3_finalize(stmts.callEvent);
stmts.callEvent = nullptr;
}
if (stmts.callErrorEvent != nullptr) {
sqlite3_finalize(stmts.callErrorEvent);
stmts.callErrorEvent = nullptr;
}
if (stmts.callCollisionEvent != nullptr) {
sqlite3_finalize(stmts.callCollisionEvent);
stmts.callCollisionEvent = nullptr;
}
if (stmts.tsbkEvent != nullptr) {
sqlite3_finalize(stmts.tsbkEvent);
stmts.tsbkEvent = nullptr;
}
if (stmts.csbkEvent != nullptr) {
sqlite3_finalize(stmts.csbkEvent);
stmts.csbkEvent = nullptr;
}
if (stmts.counterUpsert != nullptr) {
sqlite3_finalize(stmts.counterUpsert);
stmts.counterUpsert = nullptr;
}
}
/**
* @brief Prepares a single SQLite statement.
* @param db The SQLite database connection.
* @param sql The SQL query string.
* @param stmt Pointer to the SQLite statement to be prepared.
* @return true if the statement was prepared successfully, false otherwise.
*/
static bool prepareStatement(sqlite3* db, const char* sql, sqlite3_stmt** stmt)
{
if (sqlite3_prepare_v2(db, sql, -1, stmt, nullptr) != SQLITE_OK) {
return false;
}
return true;
}
/**
* @brief Prepares all SQLite statements in the given SQLiteStatements structure.
* @param db The SQLite database connection.
* @param stmts The SQLiteStatements structure to be prepared.
* @return true if all statements were prepared successfully, false otherwise.
*/
static bool prepareStatements(sqlite3* db, SQLiteStatements& stmts)
{
finalizeStatements(stmts);
if (!prepareStatement(db, "INSERT INTO activity(ts_ns, peer_id, identity, msg) VALUES(?, ?, ?, ?);", &stmts.activity))
return false;
if (!prepareStatement(db, "INSERT INTO diag(ts_ns, peer_id, identity, msg) VALUES(?, ?, ?, ?);", &stmts.diag))
return false;
if (!prepareStatement(db, "INSERT INTO call_event(ts_ns, peer_id, mode, stream_id, src_id, dst_id, duration_ms, slot) VALUES(?, ?, ?, ?, ?, ?, ?, ?);", &stmts.callEvent))
return false;
if (!prepareStatement(db, "INSERT INTO call_error_event(ts_ns, peer_id, stream_id, src_id, dst_id, message, slot) VALUES(?, ?, ?, ?, ?, ?, ?);", &stmts.callErrorEvent))
return false;
if (!prepareStatement(db, "INSERT INTO call_collision_event(ts_ns, peer_id, stream_id, src_id, dst_id, slot, rx_peer_id, rx_stream_id, rx_src_id, rx_dst_id, rx_slot) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", &stmts.callCollisionEvent))
return false;
if (!prepareStatement(db, "INSERT INTO tsbk_event(ts_ns, peer_id, lco, tsbk, raw) VALUES(?, ?, ?, ?, ?);", &stmts.tsbkEvent))
return false;
if (!prepareStatement(db, "INSERT INTO csbk_event(ts_ns, peer_id, lco, csbk, raw) VALUES(?, ?, ?, ?, ?);", &stmts.csbkEvent))
return false;
if (!prepareStatement(db,
"INSERT INTO metrics_counters(key, value) VALUES(?, ?) "
"ON CONFLICT(key) DO UPDATE SET value = excluded.value;",
&stmts.counterUpsert))
return false;
return true;
}
/**
* @brief Executes a SQLite statement, resets it, and clears its bindings.
* @param stmt The SQLite statement to execute.
* @return true if the statement executed successfully, false otherwise.
*/
static bool execStepAndReset(sqlite3_stmt* stmt)
{
int rc = sqlite3_step(stmt);
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
return rc == SQLITE_DONE;
}
/**
* @brief Inserts a metric event into the appropriate SQLite statement.
* @param stmts The SQLiteStatements structure containing the prepared statements.
* @param e The metric event to insert.
* @return true if the event was inserted successfully, false otherwise.
*/
static bool insertEvent(SQLiteStatements& stmts, const MetricEvent& e)
{
sqlite3_stmt* stmt = nullptr;
switch (e.type) {
case MetricEventType::ACTIVITY:
stmt = stmts.activity;
if (stmt == nullptr)
return false;
if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)e.tsNs) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 2, (int)e.peerId) != SQLITE_OK)
return false;
if (bindText(stmt, 3, e.identity) != SQLITE_OK)
return false;
if (bindText(stmt, 4, e.message) != SQLITE_OK)
return false;
return execStepAndReset(stmt);
case MetricEventType::DIAG:
stmt = stmts.diag;
if (stmt == nullptr)
return false;
if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)e.tsNs) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 2, (int)e.peerId) != SQLITE_OK)
return false;
if (bindText(stmt, 3, e.identity) != SQLITE_OK)
return false;
if (bindText(stmt, 4, e.message) != SQLITE_OK)
return false;
return execStepAndReset(stmt);
case MetricEventType::CALL_EVENT:
stmt = stmts.callEvent;
if (stmt == nullptr) return false;
if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)e.tsNs) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 2, (int)e.peerId) != SQLITE_OK)
return false;
if (bindText(stmt, 3, e.mode) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 4, (int)e.streamId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 5, (int)e.srcId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 6, (int)e.dstId) != SQLITE_OK)
return false;
if (sqlite3_bind_int64(stmt, 7, (sqlite3_int64)e.durationMs) != SQLITE_OK)
return false;
if (e.hasSlot) {
if (sqlite3_bind_int(stmt, 8, (int)e.slotNo) != SQLITE_OK)
return false;
} else {
if (sqlite3_bind_null(stmt, 8) != SQLITE_OK)
return false;
}
return execStepAndReset(stmt);
case MetricEventType::CALL_ERROR_EVENT:
stmt = stmts.callErrorEvent;
if (stmt == nullptr) return false;
if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)e.tsNs) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 2, (int)e.peerId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 3, (int)e.streamId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 4, (int)e.srcId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 5, (int)e.dstId) != SQLITE_OK)
return false;
if (bindText(stmt, 6, e.message) != SQLITE_OK)
return false;
if (e.hasSlot) {
if (sqlite3_bind_int(stmt, 7, (int)e.slotNo) != SQLITE_OK)
return false;
} else {
if (sqlite3_bind_null(stmt, 7) != SQLITE_OK)
return false;
}
return execStepAndReset(stmt);
case MetricEventType::CALL_COLLISION_EVENT:
stmt = stmts.callCollisionEvent;
if (stmt == nullptr) return false;
if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)e.tsNs) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 2, (int)e.peerId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 3, (int)e.streamId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 4, (int)e.srcId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 5, (int)e.dstId) != SQLITE_OK)
return false;
if (e.hasSlot) {
if (sqlite3_bind_int(stmt, 6, (int)e.slotNo) != SQLITE_OK)
return false;
} else {
if (sqlite3_bind_null(stmt, 6) != SQLITE_OK)
return false;
}
if (sqlite3_bind_int(stmt, 7, (int)e.rxPeerId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 8, (int)e.rxStreamId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 9, (int)e.rxSrcId) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 10, (int)e.rxDstId) != SQLITE_OK)
return false;
if (e.hasRxSlot) {
if (sqlite3_bind_int(stmt, 11, (int)e.rxSlot) != SQLITE_OK)
return false;
} else {
if (sqlite3_bind_null(stmt, 11) != SQLITE_OK)
return false;
}
return execStepAndReset(stmt);
case MetricEventType::TSBK_EVENT:
stmt = stmts.tsbkEvent;
if (stmt == nullptr) return false;
if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)e.tsNs) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 2, (int)e.peerId) != SQLITE_OK)
return false;
if (bindText(stmt, 3, e.lco) != SQLITE_OK)
return false;
if (bindText(stmt, 4, e.rawDescription) != SQLITE_OK)
return false;
if (bindText(stmt, 5, e.raw) != SQLITE_OK)
return false;
return execStepAndReset(stmt);
case MetricEventType::CSBK_EVENT:
stmt = stmts.csbkEvent;
if (stmt == nullptr) return false;
if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)e.tsNs) != SQLITE_OK)
return false;
if (sqlite3_bind_int(stmt, 2, (int)e.peerId) != SQLITE_OK)
return false;
if (bindText(stmt, 3, e.lco) != SQLITE_OK)
return false;
if (bindText(stmt, 4, e.rawDescription) != SQLITE_OK)
return false;
if (bindText(stmt, 5, e.raw) != SQLITE_OK)
return false;
return execStepAndReset(stmt);
case MetricEventType::COUNTER_EVENT:
stmt = stmts.counterUpsert;
if (stmt == nullptr)
return false;
if (bindText(stmt, 1, e.counterKey) != SQLITE_OK)
return false;
if (sqlite3_bind_int64(stmt, 2, (sqlite3_int64)e.counterValue) != SQLITE_OK)
return false;
return execStepAndReset(stmt);
}
return false;
}
/**
* @brief Calculates the next prune time in nanoseconds from now for the given interval.
* @param intervalMinutes Prune interval in minutes.
* @return Next prune timestamp in nanoseconds since epoch, or 0 if interval is disabled.
*/
static uint64_t calculateNextPruneNs(uint32_t intervalMinutes)
{
if (intervalMinutes == 0U) {
return 0U;
}
uint64_t now = nowNs();
uint64_t delta = (uint64_t)intervalMinutes * NS_PER_MINUTE;
if (std::numeric_limits<uint64_t>::max() - now < delta) {
return std::numeric_limits<uint64_t>::max();
}
return now + delta;
}
/**
* @brief Prunes SQLite metric event rows older than the specified number of retention days.
* @param db The SQLite database handle.
* @param retentionDays Number of days to retain.
* @param prunedRows Optional pointer to receive the number of rows deleted.
* @return true if prune operation succeeded or is disabled, false on SQLite errors.
*/
static bool pruneSQLiteMetricEvents(sqlite3* db, uint32_t retentionDays, uint64_t* prunedRows)
{
if (prunedRows != nullptr) {
*prunedRows = 0U;
}
if (db == nullptr || retentionDays == 0U) {
return true;
}
uint64_t now = nowNs();
uint64_t windowNs = (uint64_t)retentionDays * NS_PER_DAY;
if (now <= windowNs) {
return true;
}
uint64_t cutoffNs = now - windowNs;
char* errMsg = nullptr;
if (sqlite3_exec(db, "BEGIN IMMEDIATE TRANSACTION;", nullptr, nullptr, &errMsg) != SQLITE_OK) {
LogError(LOG_MASTER, "SQLite metrics prune begin transaction failed: %s", (errMsg != nullptr) ? errMsg : "unknown error");
if (errMsg != nullptr) {
sqlite3_free(errMsg);
}
return false;
}
const char* tables[] = {
"activity",
"diag",
"call_event",
"call_error_event",
"call_collision_event",
"tsbk_event",
"csbk_event"
};
uint64_t deleted = 0U;
bool ok = true;
for (const char* table : tables) {
sqlite3_stmt* stmt = nullptr;
std::string sql = "DELETE FROM ";
sql += table;
sql += " WHERE ts_ns < ?;";
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
LogError(LOG_MASTER, "SQLite metrics prune prepare failed for table %s: %s", table, sqlite3_errmsg(db));
ok = false;
if (stmt != nullptr) {
sqlite3_finalize(stmt);
}
break;
}
if (sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoffNs) != SQLITE_OK) {
LogError(LOG_MASTER, "SQLite metrics prune bind failed for table %s: %s", table, sqlite3_errmsg(db));
ok = false;
sqlite3_finalize(stmt);
break;
}
int rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
LogError(LOG_MASTER, "SQLite metrics prune delete failed for table %s: %s", table, sqlite3_errmsg(db));
ok = false;
sqlite3_finalize(stmt);
break;
}
deleted += (uint64_t)sqlite3_changes(db);
sqlite3_finalize(stmt);
}
if (ok) {
if (sqlite3_exec(db, "COMMIT;", nullptr, nullptr, &errMsg) != SQLITE_OK) {
LogError(LOG_MASTER, "SQLite metrics prune commit failed: %s", (errMsg != nullptr) ? errMsg : "unknown error");
if (errMsg != nullptr) {
sqlite3_free(errMsg);
}
sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
return false;
}
} else {
sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
return false;
}
if (prunedRows != nullptr) {
*prunedRows = deleted;
}
return true;
}
/**
* @brief Enqueues a metric event to be written to the SQLite database by the writer thread.
* @param network Pointer to the TrafficNetwork instance.
* @param event The metric event to enqueue.
*/
static void enqueueSQLiteMetric(TrafficNetwork* network, MetricEvent&& event)
{
if (network == nullptr)
return;
SQLiteWriterState* state = g_sqliteWriterState.load(std::memory_order_acquire);
if (state == nullptr) {
return;
}
size_t queueDepth = 0U;
uint64_t droppedCount = 0U;
bool emitDropWarning = false;
if (state->stopRequested.load(std::memory_order_acquire)) {
return;
}
if (!state->queue.tryPush(std::move(event))) {
state->droppedCount++;
if ((state->droppedCount % 1000U) == 1U) {
queueDepth = state->queue.pendingCount.load(std::memory_order_acquire);
droppedCount = state->droppedCount;
emitDropWarning = true;
}
if (emitDropWarning) {
LogWarning(LOG_MASTER, "SQLite metrics queue full (%zu/%zu), dropped %llu events",
queueDepth, state->queue.maxDepth, (unsigned long long)droppedCount);
}
return;
}
}
/**
* @brief Entry point for the SQLite writer thread.
* @param arg Pointer to the thread object containing the SQLite writer state.
* @return Always returns nullptr.
*/
static void* sqliteWriterThreadMain(void* arg)
{
thread_t* th = (thread_t*)arg;
if (th == nullptr) {
return nullptr;
}
SQLiteWriterState* state = (SQLiteWriterState*)th->obj;
if (state == nullptr || state->db == nullptr)
return nullptr;
sqlite3* db = state->db;
std::vector<MetricEvent> batch;
batch.reserve(SQLITE_BATCH_SIZE);
// helper function to run the scheduled prune of old SQLite metric events
auto runScheduledPrune = [&]() {
if (state->retentionDays == 0U || state->pruneIntervalMinutes == 0U || state->nextPruneNs == 0U) {
return;
}
uint64_t now = nowNs();
if (now < state->nextPruneNs) {
return;
}
uint64_t deleted = 0U;
if (pruneSQLiteMetricEvents(db, state->retentionDays, &deleted)) {
if (deleted > 0U) {
LogInfoEx(LOG_MASTER, "SQLite metrics retention prune deleted %llu stale rows (retention=%u day(s))",
(unsigned long long)deleted, state->retentionDays);
}
} else {
LogWarning(LOG_MASTER, "SQLite metrics retention prune failed (retention=%u day(s))", state->retentionDays);
}
state->nextPruneNs = calculateNextPruneNs(state->pruneIntervalMinutes);
};
while (true) {
MetricEvent event;
// drain the queue into the batch up to the maximum batch size
while (state->queue.tryPop(event) && batch.size() < SQLITE_BATCH_SIZE) {
batch.push_back(std::move(event));
}
// check if we should exit the writer thread
if (state->stopRequested.load(std::memory_order_acquire) && batch.empty() && state->queue.pendingCount.load(std::memory_order_acquire) == 0U) {
break;
}
// if the batch is empty, run the scheduled prune and wait before trying again
if (batch.empty()) {
runScheduledPrune();
std::this_thread::sleep_for(std::chrono::milliseconds(SQLITE_BATCH_WAIT_MS));
continue;
}
// begin a new SQLite transaction
char* errMsg = nullptr;
if (sqlite3_exec(db, "BEGIN IMMEDIATE TRANSACTION;", nullptr, nullptr, &errMsg) != SQLITE_OK) {
LogError(LOG_MASTER, "SQLite metrics begin transaction failed: %s", (errMsg != nullptr) ? errMsg : "unknown error");
if (errMsg != nullptr) {
sqlite3_free(errMsg);
}
batch.clear();
continue;
}
bool ok = true;
for (const auto& event : batch) {
if (!insertEvent(state->statements, event)) {
ok = false;
LogError(LOG_MASTER, "SQLite metrics insert failed: %s", sqlite3_errmsg(db));
break;
}
}
if (ok) {
if (sqlite3_exec(db, "COMMIT;", nullptr, nullptr, &errMsg) != SQLITE_OK) {
LogError(LOG_MASTER, "SQLite metrics commit failed: %s", (errMsg != nullptr) ? errMsg : "unknown error");
if (errMsg != nullptr) {
sqlite3_free(errMsg);
}
sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
}
} else {
sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
}
batch.clear();
// run the scheduled prune after processing the current batch
runScheduledPrune();
}
return nullptr;
}
}
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes metric sinks for the specified TrafficNetwork instance. */
void TrafficNetwork::MetricsLogging::initialize(TrafficNetwork* network)
{
if (network == nullptr)
return;
if (!network->m_enableMetrics)
return;
// is SQLite metrics enabled?
if (network->m_enableSQLite) {
if (network->m_sqliteDB == nullptr) {
if (sqlite3_open(network->m_sqliteDBFile.c_str(), &network->m_sqliteDB) != SQLITE_OK) {
LogError(LOG_MASTER, "Failed to open SQLite metrics database file: %s", network->m_sqliteDBFile.c_str());
network->m_enableSQLite = false;
if (network->m_sqliteDB != nullptr) {
sqlite3_close(network->m_sqliteDB);
network->m_sqliteDB = nullptr;
}
return;
}
}
// ensure SQLite metrics schema is present for both new and existing databases
initializeSQLite(network);
if (network->m_sqliteDB != nullptr) {
// load existing SQLite metrics counters
loadSQLiteCounters(network);
// perform an initial prune of old SQLite metric events if retention is configured
if (network->m_sqlitePruneAfterDays > 0U) {
uint64_t deleted = 0U;
if (pruneSQLiteMetricEvents(network->m_sqliteDB, network->m_sqlitePruneAfterDays, &deleted)) {
if (deleted > 0U) {
LogInfoEx(LOG_MASTER, "SQLite metrics startup prune deleted %llu stale rows (retention=%u day(s))",
(unsigned long long)deleted, network->m_sqlitePruneAfterDays);
}
} else {
LogWarning(LOG_MASTER, "SQLite metrics startup prune failed (retention=%u day(s))", network->m_sqlitePruneAfterDays);
}
}
}
// initialize the SQLite writer thread if the database is available
if (network->m_sqliteDB != nullptr) {
SQLiteWriterState* existingState = g_sqliteWriterState.load(std::memory_order_acquire);
if (existingState == nullptr) {
auto state = std::make_shared<SQLiteWriterState>();
if (!prepareStatements(network->m_sqliteDB, state->statements)) {
LogError(LOG_MASTER, "Failed to prepare SQLite metrics statements: %s", sqlite3_errmsg(network->m_sqliteDB));
finalizeStatements(state->statements);
sqlite3_close(network->m_sqliteDB);
network->m_sqliteDB = nullptr;
network->m_enableSQLite = false;
return;
}
state->db = network->m_sqliteDB;
state->retentionDays = network->m_sqlitePruneAfterDays;
state->pruneIntervalMinutes = network->m_sqlitePruneIntervalMinutes;
state->nextPruneNs = calculateNextPruneNs(state->pruneIntervalMinutes);
// start the SQLite writer thread
if (!Thread::runAsThread(state.get(), sqliteWriterThreadMain, &state->workerThread)) {
LogError(LOG_MASTER, "Failed to start SQLite metrics writer thread");
finalizeStatements(state->statements);
sqlite3_close(network->m_sqliteDB);
network->m_sqliteDB = nullptr;
network->m_enableSQLite = false;
return;
}
state->workerStarted.store(true, std::memory_order_release);
SQLiteWriterState* rawState = state.get();
g_sqliteWriterStateHolder = std::move(state);
g_sqliteWriterState.store(rawState, std::memory_order_release);
}
}
}
}
/* Finalizes metric sinks for the specified TrafficNetwork instance. */
void TrafficNetwork::MetricsLogging::finalize(TrafficNetwork* network)
{
if (network == nullptr)
return;
SQLiteWriterState* state = g_sqliteWriterState.load(std::memory_order_acquire);
if (state != nullptr) {
state->stopRequested.store(true, std::memory_order_release);
// wait for the SQLite writer thread to finish if it was started
if (state->workerStarted.load(std::memory_order_acquire)) {
#if defined(_WIN32)
::WaitForSingleObject(state->workerThread.thread, INFINITE);
::CloseHandle(state->workerThread.thread);
#else
::pthread_join(state->workerThread.thread, nullptr);
#endif
state->workerStarted.store(false, std::memory_order_release);
}
// finalize the SQLite statements
finalizeStatements(state->statements);
}
// close the SQLite database if it is open
if (network->m_sqliteDB != nullptr) {
sqlite3_close(network->m_sqliteDB);
network->m_sqliteDB = nullptr;
}
}
/* Increments the active call counter. */
void TrafficNetwork::MetricsLogging::incrementActiveCalls(TrafficNetwork* network)
{
(void)network;
s_totalActiveCalls.fetch_add(1, std::memory_order_relaxed);
}
/* Decrements the active call counter with floor at zero. */
void TrafficNetwork::MetricsLogging::decrementActiveCalls(TrafficNetwork* network)
{
(void)network;
int32_t expected = s_totalActiveCalls.load(std::memory_order_relaxed);
while (expected > 0) {
if (s_totalActiveCalls.compare_exchange_weak(expected, expected - 1, std::memory_order_relaxed)) {
return;
}
}
}
/* Resets the active call counter to zero. */
void TrafficNetwork::MetricsLogging::resetActiveCalls()
{
s_totalActiveCalls.store(0, std::memory_order_relaxed);
}
/* Increments the total processed calls counter. */
void TrafficNetwork::MetricsLogging::incrementCallsProcessed(TrafficNetwork* network)
{
uint64_t value = s_totalCallsProcessed.fetch_add(1U, std::memory_order_relaxed) + 1U;
persistSQLiteCounter(network, SQLITE_COUNTER_CALLS_PROCESSED, value);
}
/* Increments the total call collisions counter. */
void TrafficNetwork::MetricsLogging::incrementCallCollisions(TrafficNetwork* network)
{
uint64_t value = s_totalCallCollisions.fetch_add(1U, std::memory_order_relaxed) + 1U;
persistSQLiteCounter(network, SQLITE_COUNTER_CALL_COLLISIONS, value);
}
/* Increments the total call switches counter. */
void TrafficNetwork::MetricsLogging::incrementCallSwitches(TrafficNetwork* network)
{
uint64_t value = s_totalCallSwitches.fetch_add(1U, std::memory_order_relaxed) + 1U;
persistSQLiteCounter(network, SQLITE_COUNTER_CALL_SWITCHES, value);
}
/* Resets the total processed calls counter to zero. */
void TrafficNetwork::MetricsLogging::resetCallsProcessed(TrafficNetwork* network)
{
s_totalCallsProcessed.store(0U, std::memory_order_relaxed);
persistSQLiteCounter(network, SQLITE_COUNTER_CALLS_PROCESSED, 0U);
}
/* Resets the total call collisions counter to zero. */
void TrafficNetwork::MetricsLogging::resetCallCollisions(TrafficNetwork* network)
{
s_totalCallCollisions.store(0U, std::memory_order_relaxed);
persistSQLiteCounter(network, SQLITE_COUNTER_CALL_COLLISIONS, 0U);
}
/* Resets the total call switches counter to zero. */
void TrafficNetwork::MetricsLogging::resetCallSwitches(TrafficNetwork* network)
{
s_totalCallSwitches.store(0U, std::memory_order_relaxed);
persistSQLiteCounter(network, SQLITE_COUNTER_CALL_SWITCHES, 0U);
}
/* Gets the active call counter. */
int32_t TrafficNetwork::MetricsLogging::getTotalActiveCalls()
{
return s_totalActiveCalls.load(std::memory_order_relaxed);
}
/* Gets the total processed calls counter. */
uint64_t TrafficNetwork::MetricsLogging::getTotalCallsProcessed()
{
return s_totalCallsProcessed.load(std::memory_order_relaxed);
}
/* Gets the total call collisions counter. */
uint64_t TrafficNetwork::MetricsLogging::getTotalCallCollisions()
{
return s_totalCallCollisions.load(std::memory_order_relaxed);
}
/* Gets the total call switches counter. */
uint64_t TrafficNetwork::MetricsLogging::getTotalCallSwitches()
{
return s_totalCallSwitches.load(std::memory_order_relaxed);
}
/* Logs a activity transfer event. */
void TrafficNetwork::MetricsLogging::logActivity(TrafficNetwork* network, uint32_t peerId, const std::string& identity, const std::string& msg)
{
if (network == nullptr || !network->m_enableMetrics)
return;
uint64_t ts = nowNs();
if (network->m_enableInfluxDB) {
influxdb::QueryBuilder()
.meas("activity")
.tag("peerId", std::to_string(peerId))
.field("identity", identity)
.field("msg", msg)
.timestamp(ts)
.requestAsync(network->m_influxServer);
}
if (network->m_enableSQLite && network->m_sqliteDB != nullptr) {
MetricEvent e;
e.type = MetricEventType::ACTIVITY;
e.tsNs = ts;
e.peerId = peerId;
e.identity = identity;
e.message = msg;
enqueueSQLiteMetric(network, std::move(e));
}
}
/* Logs a diagnostic transfer event. */
void TrafficNetwork::MetricsLogging::logDiag(TrafficNetwork* network, uint32_t peerId, const std::string& identity, const std::string& msg)
{
if (network == nullptr || !network->m_enableMetrics)
return;
uint64_t ts = nowNs();
if (network->m_enableInfluxDB) {
influxdb::QueryBuilder()
.meas("diag")
.tag("peerId", std::to_string(peerId))
.field("identity", identity)
.field("msg", msg)
.timestamp(ts)
.requestAsync(network->m_influxServer);
}
if (network->m_enableSQLite && network->m_sqliteDB != nullptr) {
MetricEvent e;
e.type = MetricEventType::DIAG;
e.tsNs = ts;
e.peerId = peerId;
e.identity = identity;
e.message = msg;
enqueueSQLiteMetric(network, std::move(e));
}
}
/* Logs a call event. */
void TrafficNetwork::MetricsLogging::logCallEvent(TrafficNetwork* network, const char* mode, uint32_t peerId, uint32_t streamId,
uint32_t srcId, uint32_t dstId, uint64_t durationMs)
{
if (network == nullptr || !network->m_enableMetrics)
return;
uint64_t ts = nowNs();
if (network->m_enableInfluxDB) {
influxdb::QueryBuilder()
.meas("call_event")
.tag("peerId", std::to_string(peerId))
.tag("mode", mode)
.tag("streamId", std::to_string(streamId))
.tag("srcId", std::to_string(srcId))
.tag("dstId", std::to_string(dstId))
.field("duration", durationMs)
.timestamp(ts)
.requestAsync(network->m_influxServer);
}
if (network->m_enableSQLite && network->m_sqliteDB != nullptr) {
MetricEvent e;
e.type = MetricEventType::CALL_EVENT;
e.tsNs = ts;
e.peerId = peerId;
e.streamId = streamId;
e.srcId = srcId;
e.dstId = dstId;
e.durationMs = durationMs;
e.mode = (mode != nullptr) ? std::string(mode) : std::string("unknown");
enqueueSQLiteMetric(network, std::move(e));
}
}
/* Logs a call event with slot number. */
void TrafficNetwork::MetricsLogging::logCallEvent(TrafficNetwork* network, const char* mode, uint32_t peerId, uint32_t streamId,
uint32_t srcId, uint32_t dstId, uint64_t durationMs, uint8_t slotNo)
{
if (network == nullptr || !network->m_enableMetrics)
return;
uint64_t ts = nowNs();
if (network->m_enableInfluxDB) {
influxdb::QueryBuilder()
.meas("call_event")
.tag("peerId", std::to_string(peerId))
.tag("mode", mode)
.tag("streamId", std::to_string(streamId))
.tag("srcId", std::to_string(srcId))
.tag("dstId", std::to_string(dstId))
.field("duration", durationMs)
.field("slot", slotNo)
.timestamp(ts)
.requestAsync(network->m_influxServer);
}
if (network->m_enableSQLite && network->m_sqliteDB != nullptr) {
MetricEvent e;
e.type = MetricEventType::CALL_EVENT;
e.tsNs = ts;
e.peerId = peerId;
e.streamId = streamId;
e.srcId = srcId;
e.dstId = dstId;
e.durationMs = durationMs;
e.mode = (mode != nullptr) ? std::string(mode) : std::string("unknown");
e.hasSlot = true;
e.slotNo = slotNo;
enqueueSQLiteMetric(network, std::move(e));
}
}
/* Logs a call error event. */
void TrafficNetwork::MetricsLogging::logCallErrorEvent(TrafficNetwork* network, uint32_t peerId, uint32_t streamId, uint32_t srcId,
uint32_t dstId, const std::string& message)
{
if (network == nullptr || !network->m_enableMetrics)
return;
uint64_t ts = nowNs();
if (network->m_enableInfluxDB) {
influxdb::QueryBuilder()
.meas("call_error_event")
.tag("peerId", std::to_string(peerId))
.tag("streamId", std::to_string(streamId))
.tag("srcId", std::to_string(srcId))
.tag("dstId", std::to_string(dstId))
.field("message", message)
.timestamp(ts)
.requestAsync(network->m_influxServer);
}
if (network->m_enableSQLite && network->m_sqliteDB != nullptr) {
MetricEvent e;
e.type = MetricEventType::CALL_ERROR_EVENT;
e.tsNs = ts;
e.peerId = peerId;
e.streamId = streamId;
e.srcId = srcId;
e.dstId = dstId;
e.message = message;
enqueueSQLiteMetric(network, std::move(e));
}
}
/* Logs a call error event with slot number. */
void TrafficNetwork::MetricsLogging::logCallErrorEvent(TrafficNetwork* network, uint32_t peerId, uint32_t streamId, uint32_t srcId,
uint32_t dstId, const std::string& message, uint8_t slotNo)
{
if (network == nullptr || !network->m_enableMetrics)
return;
uint64_t ts = nowNs();
if (network->m_enableInfluxDB) {
influxdb::QueryBuilder()
.meas("call_error_event")
.tag("peerId", std::to_string(peerId))
.tag("streamId", std::to_string(streamId))
.tag("srcId", std::to_string(srcId))
.tag("dstId", std::to_string(dstId))
.field("message", message)
.field("slot", slotNo)
.timestamp(ts)
.requestAsync(network->m_influxServer);
}
if (network->m_enableSQLite && network->m_sqliteDB != nullptr) {
MetricEvent e;
e.type = MetricEventType::CALL_ERROR_EVENT;
e.tsNs = ts;
e.peerId = peerId;
e.streamId = streamId;
e.srcId = srcId;
e.dstId = dstId;
e.message = message;
e.hasSlot = true;
e.slotNo = slotNo;
enqueueSQLiteMetric(network, std::move(e));
}
}
/* Logs a call collision event with slot number. */
void TrafficNetwork::MetricsLogging::logCallCollisionEvent(TrafficNetwork* network, uint32_t peerId, uint32_t streamId, uint32_t srcId,
uint32_t dstId, uint8_t slotNo, uint32_t rxPeerId, uint32_t rxStreamId, uint32_t rxSrcId, uint32_t rxDstId, uint8_t rxSlot)
{
if (network == nullptr || !network->m_enableMetrics)
return;
uint64_t ts = nowNs();
if (network->m_enableInfluxDB) {
influxdb::QueryBuilder()
.meas("call_collision_event")
.tag("peerId", std::to_string(peerId))
.tag("streamId", std::to_string(streamId))
.tag("srcId", std::to_string(srcId))
.tag("dstId", std::to_string(dstId))
.tag("rxPeerId", std::to_string(rxPeerId))
.tag("rxStreamId", std::to_string(rxStreamId))
.tag("rxSrcId", std::to_string(rxSrcId))
.tag("rxDstId", std::to_string(rxDstId))
.field("slot", slotNo)
.field("rxSlot", rxSlot)
.timestamp(ts)
.requestAsync(network->m_influxServer);
}
if (network->m_enableSQLite && network->m_sqliteDB != nullptr) {
MetricEvent e;
e.type = MetricEventType::CALL_COLLISION_EVENT;
e.tsNs = ts;
e.peerId = peerId;
e.streamId = streamId;
e.srcId = srcId;
e.dstId = dstId;
e.rxPeerId = rxPeerId;
e.rxStreamId = rxStreamId;
e.rxSrcId = rxSrcId;
e.rxDstId = rxDstId;
if (slotNo != 0U) {
e.hasSlot = true;
e.slotNo = slotNo;
}
if (rxSlot != 0U) {
e.hasRxSlot = true;
e.rxSlot = rxSlot;
}
enqueueSQLiteMetric(network, std::move(e));
}
}
/* Logs a P25 TSBK raw event. */
void TrafficNetwork::MetricsLogging::logTSBKEvent(TrafficNetwork* network, uint32_t peerId, const std::string& lco, const std::string& tsbk,
const std::string& raw)
{
if (network == nullptr || !network->m_enableMetrics)
return;
uint64_t ts = nowNs();
if (network->m_enableInfluxDB && network->m_metricsLogRawData) {
influxdb::QueryBuilder()
.meas("tsbk_event")
.tag("peerId", std::to_string(peerId))
.tag("lco", lco)
.tag("tsbk", tsbk)
.field("raw", raw)
.timestamp(ts)
.requestAsync(network->m_influxServer);
}
if (network->m_enableSQLite && network->m_sqliteDB != nullptr && network->m_metricsLogRawData) {
MetricEvent e;
e.type = MetricEventType::TSBK_EVENT;
e.tsNs = ts;
e.peerId = peerId;
e.lco = lco;
e.rawDescription = tsbk;
e.raw = raw;
enqueueSQLiteMetric(network, std::move(e));
}
}
/* Logs a DMR CSBK raw event. */
void TrafficNetwork::MetricsLogging::logCSBKEvent(TrafficNetwork* network, uint32_t peerId, const std::string& lco, const std::string& csbk,
const std::string& raw)
{
if (network == nullptr || !network->m_enableMetrics)
return;
uint64_t ts = nowNs();
if (network->m_enableInfluxDB && network->m_metricsLogRawData) {
influxdb::QueryBuilder()
.meas("csbk_event")
.tag("peerId", std::to_string(peerId))
.tag("lco", lco)
.tag("csbk", csbk)
.field("raw", raw)
.timestamp(ts)
.requestAsync(network->m_influxServer);
}
if (network->m_enableSQLite && network->m_sqliteDB != nullptr && network->m_metricsLogRawData) {
MetricEvent e;
e.type = MetricEventType::CSBK_EVENT;
e.tsNs = ts;
e.peerId = peerId;
e.lco = lco;
e.rawDescription = csbk;
e.raw = raw;
enqueueSQLiteMetric(network, std::move(e));
}
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Checks if the SQLite database for the specified TrafficNetwork instance is blank. */
bool TrafficNetwork::MetricsLogging::isSQLiteBlank(TrafficNetwork* network)
{
if (!network->m_enableMetrics)
return false;
if (!network->m_enableSQLite)
return false;
std::string sql = "SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';";
sqlite3_stmt* stmt = nullptr;
bool blank = true;
if (sqlite3_prepare_v2(network->m_sqliteDB, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int cnt = sqlite3_column_int(stmt, 0);
if (cnt > 0) {
blank = false;
}
}
}
sqlite3_finalize(stmt);
return blank;
}
/* Initializes the SQLite database for the specified TrafficNetwork instance. */
void TrafficNetwork::MetricsLogging::initializeSQLite(TrafficNetwork* network)
{
if (!network->m_enableMetrics)
return;
if (!network->m_enableSQLite)
return;
if (network->m_sqliteDB == nullptr)
return;
if (sqlite3_exec(network->m_sqliteDB, "PRAGMA journal_mode = WAL;", nullptr, nullptr, nullptr) != SQLITE_OK) {
LogError(LOG_MASTER, "Error setting SQLite journal_mode, error: %s", sqlite3_errmsg(network->m_sqliteDB));
sqlite3_close(network->m_sqliteDB);
network->m_sqliteDB = nullptr;
network->m_enableSQLite = false;
return;
}
if (sqlite3_exec(network->m_sqliteDB, "PRAGMA synchronous = NORMAL;", nullptr, nullptr, nullptr) != SQLITE_OK) {
LogError(LOG_MASTER, "Error setting SQLite synchronous mode, error: %s", sqlite3_errmsg(network->m_sqliteDB));
sqlite3_close(network->m_sqliteDB);
network->m_sqliteDB = nullptr;
network->m_enableSQLite = false;
return;
}
if (sqlite3_exec(network->m_sqliteDB, "PRAGMA foreign_keys = ON;", nullptr, nullptr, nullptr) != SQLITE_OK) {
LogError(LOG_MASTER, "Error enabling SQLite foreign_keys, error: %s", sqlite3_errmsg(network->m_sqliteDB));
sqlite3_close(network->m_sqliteDB);
network->m_sqliteDB = nullptr;
network->m_enableSQLite = false;
return;
}
if (!ensureSQLiteMetricTables(network)) {
sqlite3_close(network->m_sqliteDB);
network->m_sqliteDB = nullptr;
network->m_enableSQLite = false;
}
}
/* Ensures SQLite tables required for persisted metrics counters exist. */
bool TrafficNetwork::MetricsLogging::ensureSQLiteCounterTable(TrafficNetwork* network)
{
if (network == nullptr || network->m_sqliteDB == nullptr) {
return false;
}
std::string sql = "CREATE TABLE IF NOT EXISTS metrics_counters (key TEXT PRIMARY KEY, value INTEGER NOT NULL);";
if (sqlite3_exec(network->m_sqliteDB, sql.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) {
LogError(LOG_MASTER, "Error creating SQLite metrics_counters table, error: %s", sqlite3_errmsg(network->m_sqliteDB));
return false;
}
return true;
}
/* Ensures all SQLite metric tables and indexes exist. */
bool TrafficNetwork::MetricsLogging::ensureSQLiteMetricTables(TrafficNetwork* network)
{
if (network == nullptr || network->m_sqliteDB == nullptr) {
return false;
}
// SQLite schema statements for creating metric tables and indexes
const char* schemaStatements[] = {
// Activity table and indexes
R"(CREATE TABLE IF NOT EXISTS activity (
id INTEGER PRIMARY KEY,
ts_ns INTEGER NOT NULL,
ts_s REAL GENERATED ALWAYS AS (ts_ns / 1000000000.0) STORED,
peer_id INTEGER NOT NULL,
identity TEXT NOT NULL,
msg TEXT NOT NULL
);)",
"CREATE INDEX IF NOT EXISTS idx_activity_time ON activity(ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_activity_peer_time ON activity(peer_id, ts_ns);",
// Diagnostic table and indexes
R"(CREATE TABLE IF NOT EXISTS diag (
id INTEGER PRIMARY KEY,
ts_ns INTEGER NOT NULL,
ts_s REAL GENERATED ALWAYS AS (ts_ns / 1000000000.0) STORED,
peer_id INTEGER NOT NULL,
identity TEXT NOT NULL,
msg TEXT NOT NULL
);)",
"CREATE INDEX IF NOT EXISTS idx_diag_time ON diag(ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_diag_peer_time ON diag(peer_id, ts_ns);",
// Call event table and indexes
R"(CREATE TABLE IF NOT EXISTS call_event (
id INTEGER PRIMARY KEY,
ts_ns INTEGER NOT NULL,
ts_s REAL GENERATED ALWAYS AS (ts_ns / 1000000000.0) STORED,
peer_id INTEGER NOT NULL,
mode TEXT NOT NULL CHECK (mode IN ('Analog','DMR','NXDN','P25')),
stream_id INTEGER NOT NULL,
src_id INTEGER NOT NULL,
dst_id INTEGER NOT NULL,
duration_ms INTEGER NOT NULL,
slot INTEGER NULL
);)",
"CREATE INDEX IF NOT EXISTS idx_call_event_time ON call_event(ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_event_mode_time ON call_event(mode, ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_event_peer_time ON call_event(peer_id, ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_event_dst_time ON call_event(dst_id, ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_event_stream ON call_event(stream_id);",
// Call error event table and indexes
R"(CREATE TABLE IF NOT EXISTS call_error_event (
id INTEGER PRIMARY KEY,
ts_ns INTEGER NOT NULL,
ts_s REAL GENERATED ALWAYS AS (ts_ns / 1000000000.0) STORED,
peer_id INTEGER NOT NULL,
stream_id INTEGER NOT NULL,
src_id INTEGER NOT NULL,
dst_id INTEGER NOT NULL,
message TEXT NOT NULL,
slot INTEGER NULL
);)",
"CREATE INDEX IF NOT EXISTS idx_call_error_time ON call_error_event(ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_error_peer_time ON call_error_event(peer_id, ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_error_message_time ON call_error_event(message, ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_error_stream ON call_error_event(stream_id);",
// Call collision event table and indexes
R"(CREATE TABLE IF NOT EXISTS call_collision_event (
id INTEGER PRIMARY KEY,
ts_ns INTEGER NOT NULL,
ts_s REAL GENERATED ALWAYS AS (ts_ns / 1000000000.0) STORED,
peer_id INTEGER NOT NULL,
stream_id INTEGER NOT NULL,
src_id INTEGER NOT NULL,
dst_id INTEGER NOT NULL,
slot INTEGER NULL,
rx_peer_id INTEGER NOT NULL,
rx_stream_id INTEGER NOT NULL,
rx_src_id INTEGER NOT NULL,
rx_dst_id INTEGER NOT NULL,
rx_slot INTEGER NULL
);)",
"CREATE INDEX IF NOT EXISTS idx_call_collision_time ON call_collision_event(ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_collision_peer_time ON call_collision_event(peer_id, ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_collision_stream ON call_collision_event(stream_id);",
"CREATE INDEX IF NOT EXISTS idx_call_collision_rx_peer_time ON call_collision_event(rx_peer_id, ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_call_collision_rx_stream ON call_collision_event(rx_stream_id);",
// TSBK event table and indexes
R"(CREATE TABLE IF NOT EXISTS tsbk_event (
id INTEGER PRIMARY KEY,
ts_ns INTEGER NOT NULL,
ts_s REAL GENERATED ALWAYS AS (ts_ns / 1000000000.0) STORED,
peer_id INTEGER NOT NULL,
lco TEXT NOT NULL,
tsbk TEXT NOT NULL,
raw TEXT NOT NULL
);)",
"CREATE INDEX IF NOT EXISTS idx_tsbk_time ON tsbk_event(ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_tsbk_peer_time ON tsbk_event(peer_id, ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_tsbk_lco_time ON tsbk_event(lco, ts_ns);",
// CSBK event table and indexes
R"(CREATE TABLE IF NOT EXISTS csbk_event (
id INTEGER PRIMARY KEY,
ts_ns INTEGER NOT NULL,
ts_s REAL GENERATED ALWAYS AS (ts_ns / 1000000000.0) STORED,
peer_id INTEGER NOT NULL,
lco TEXT NOT NULL,
csbk TEXT NOT NULL,
raw TEXT NOT NULL
);)",
"CREATE INDEX IF NOT EXISTS idx_csbk_time ON csbk_event(ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_csbk_peer_time ON csbk_event(peer_id, ts_ns);",
"CREATE INDEX IF NOT EXISTS idx_csbk_lco_time ON csbk_event(lco, ts_ns);"
};
// execute each schema statement to ensure the SQLite database has the necessary tables and indexes
for (const char* statement : schemaStatements) {
if (sqlite3_exec(network->m_sqliteDB, statement, nullptr, nullptr, nullptr) != SQLITE_OK) {
LogError(LOG_MASTER, "Error ensuring SQLite metrics schema, error: %s", sqlite3_errmsg(network->m_sqliteDB));
return false;
}
}
return ensureSQLiteCounterTable(network);
}
/* Loads persisted metrics counters from SQLite. */
void TrafficNetwork::MetricsLogging::loadSQLiteCounters(TrafficNetwork* network)
{
if (network == nullptr || network->m_sqliteDB == nullptr) {
return;
}
sqlite3_stmt* stmt = nullptr;
std::string sql = "SELECT key, value FROM metrics_counters WHERE key IN (?, ?, ?);";
if (sqlite3_prepare_v2(network->m_sqliteDB, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
LogWarning(LOG_MASTER, "Unable to prepare SQLite counter load statement: %s", sqlite3_errmsg(network->m_sqliteDB));
return;
}
sqlite3_bind_text(stmt, 1, SQLITE_COUNTER_CALLS_PROCESSED, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, SQLITE_COUNTER_CALL_COLLISIONS, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, SQLITE_COUNTER_CALL_SWITCHES, -1, SQLITE_STATIC);
uint64_t loadedProcessed = 0U;
uint64_t loadedCollisions = 0U;
uint64_t loadedSwitches = 0U;
bool hasProcessed = false;
bool hasCollisions = false;
bool hasSwitches = false;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* key = (const char*)sqlite3_column_text(stmt, 0);
uint64_t value = (uint64_t)sqlite3_column_int64(stmt, 1);
if (key == nullptr) {
continue;
}
if (::strcmp(key, SQLITE_COUNTER_CALLS_PROCESSED) == 0) {
loadedProcessed = value;
hasProcessed = true;
} else if (::strcmp(key, SQLITE_COUNTER_CALL_COLLISIONS) == 0) {
loadedCollisions = value;
hasCollisions = true;
} else if (::strcmp(key, SQLITE_COUNTER_CALL_SWITCHES) == 0) {
loadedSwitches = value;
hasSwitches = true;
}
}
sqlite3_finalize(stmt);
if (hasProcessed) {
s_totalCallsProcessed.store(loadedProcessed, std::memory_order_relaxed);
}
if (hasCollisions) {
s_totalCallCollisions.store(loadedCollisions, std::memory_order_relaxed);
}
if (hasSwitches) {
s_totalCallSwitches.store(loadedSwitches, std::memory_order_relaxed);
}
}
/* Persists a single metrics counter to SQLite. */
void TrafficNetwork::MetricsLogging::persistSQLiteCounter(TrafficNetwork* network, const char* key, uint64_t value)
{
if (network == nullptr || key == nullptr) {
return;
}
if (!network->m_enableSQLite || network->m_sqliteDB == nullptr) {
return;
}
MetricEvent e;
e.type = MetricEventType::COUNTER_EVENT;
e.counterKey = key;
e.counterValue = value;
enqueueSQLiteMetric(network, std::move(e));
}

Powered by TurnKey Linux.