mirror of https://github.com/sysown/proxysql
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.
844 lines
31 KiB
844 lines
31 KiB
/**
|
|
* @file statistics_unit-t.cpp
|
|
* @brief Unit tests for ProxySQL_Statistics.
|
|
*
|
|
* Tests cover:
|
|
* - Constructor / init / table creation
|
|
* - Timer functions (MySQL_Threads_Handler_timetoget, system_cpu_timetoget,
|
|
* MySQL_Query_Cache_timetoget, eventslog timer functions)
|
|
* - TSDB variable management (set_variable, get_variable, has_variable,
|
|
* get_variables_list) — guarded by PROXYSQLTSDB
|
|
* - TSDB timer functions (tsdb_sampler_timetoget, tsdb_downsample_timetoget,
|
|
* tsdb_monitor_timetoget, tsdb_retention_timetoget) — guarded by PROXYSQLTSDB
|
|
* - TSDB metric insertion and query (insert_tsdb_metric, query_tsdb_metrics)
|
|
* which indirectly exercises the anonymous-namespace helpers
|
|
* escape_sql_string_literal and valid_label_key
|
|
* - TSDB backend health insertion and query
|
|
* - TSDB downsampling and retention cleanup
|
|
* - TSDB status retrieval
|
|
*
|
|
* All tests use in-memory or /tmp SQLite3 databases — no external deps.
|
|
*/
|
|
|
|
#include "tap.h"
|
|
#include "test_globals.h"
|
|
#include "test_init.h"
|
|
#include "proxysql.h"
|
|
#include "cpp.h"
|
|
#include "ProxySQL_Statistics.hpp"
|
|
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <map>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
// Forward-declare the extern that the constructor reads.
|
|
// GloVars is defined in test_globals.cpp via the PROXYSQL_EXTERN mechanism.
|
|
|
|
static ProxySQL_Statistics *stats = nullptr;
|
|
static std::string tmpdb_path;
|
|
|
|
// ============================================================
|
|
// Setup / teardown
|
|
// ============================================================
|
|
|
|
static void setup_stats() {
|
|
// ProxySQL_Statistics constructor opens GloVars.statsdb_disk as a file path.
|
|
// Use /tmp directly to avoid datadir issues in CI environments.
|
|
const char *tmpdir = getenv("TMPDIR");
|
|
if (tmpdir == nullptr) tmpdir = "/tmp";
|
|
tmpdb_path = std::string(tmpdir) + "/proxysql_test_statsdb_" + std::to_string(getpid()) + ".db";
|
|
|
|
// Ensure datadir exists (needed by other parts of ProxySQL_Statistics)
|
|
if (GloVars.datadir != nullptr)
|
|
mkdir(GloVars.datadir, 0755);
|
|
|
|
// Set the global so the constructor can open it
|
|
GloVars.statsdb_disk = strdup(tmpdb_path.c_str());
|
|
|
|
stats = new ProxySQL_Statistics();
|
|
if (stats->statsdb_disk == nullptr) {
|
|
diag("statsdb_disk is NULL after construction — possible struct layout mismatch");
|
|
diag("(rebuild with: make clean && make debug in test/tap/tests/unit/)");
|
|
diag("GloVars.statsdb_disk=%s", GloVars.statsdb_disk ? GloVars.statsdb_disk : "NULL");
|
|
}
|
|
if (stats->statsdb_disk != nullptr)
|
|
stats->init();
|
|
}
|
|
|
|
static void teardown_stats() {
|
|
delete stats;
|
|
stats = nullptr;
|
|
|
|
// Clean up temp database files
|
|
unlink(tmpdb_path.c_str());
|
|
std::string wal = tmpdb_path + "-wal";
|
|
std::string shm = tmpdb_path + "-shm";
|
|
unlink(wal.c_str());
|
|
unlink(shm.c_str());
|
|
|
|
if (GloVars.statsdb_disk != nullptr) {
|
|
free(GloVars.statsdb_disk);
|
|
GloVars.statsdb_disk = nullptr;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Constructor and init
|
|
// ============================================================
|
|
|
|
static bool statsdb_ok = false;
|
|
|
|
static void test_constructor_creates_databases() {
|
|
ok(stats != nullptr, "ProxySQL_Statistics object created successfully");
|
|
statsdb_ok = (stats != nullptr && stats->statsdb_disk != nullptr);
|
|
ok(statsdb_ok, "statsdb_disk is not null after construction");
|
|
}
|
|
|
|
static void test_init_creates_tables() {
|
|
if (!statsdb_ok) {
|
|
skip(3, "statsdb_disk is NULL — struct layout mismatch (clean rebuild needed)");
|
|
return;
|
|
}
|
|
// After init(), standard tables should exist. Verify by querying them.
|
|
SQLite3_result *resultset = nullptr;
|
|
char *error = nullptr;
|
|
int cols = 0, affected_rows = 0;
|
|
|
|
stats->statsdb_disk->execute_statement(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='mysql_connections'",
|
|
&error, &cols, &affected_rows, &resultset);
|
|
if (error) { free(error); error = nullptr; }
|
|
ok(resultset != nullptr && resultset->rows_count > 0 &&
|
|
strcmp(resultset->rows[0]->fields[0], "1") == 0,
|
|
"init: mysql_connections table exists on disk");
|
|
delete resultset;
|
|
|
|
resultset = nullptr;
|
|
stats->statsdb_disk->execute_statement(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='system_cpu'",
|
|
&error, &cols, &affected_rows, &resultset);
|
|
if (error) { free(error); error = nullptr; }
|
|
ok(resultset != nullptr && resultset->rows_count > 0 &&
|
|
strcmp(resultset->rows[0]->fields[0], "1") == 0,
|
|
"init: system_cpu table exists on disk");
|
|
delete resultset;
|
|
|
|
resultset = nullptr;
|
|
stats->statsdb_disk->execute_statement(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='history_mysql_query_digest'",
|
|
&error, &cols, &affected_rows, &resultset);
|
|
if (error) { free(error); error = nullptr; }
|
|
ok(resultset != nullptr && resultset->rows_count > 0 &&
|
|
strcmp(resultset->rows[0]->fields[0], "1") == 0,
|
|
"init: history_mysql_query_digest table exists on disk");
|
|
delete resultset;
|
|
}
|
|
|
|
#ifdef PROXYSQLTSDB
|
|
static void test_init_creates_tsdb_tables() {
|
|
SQLite3_result *resultset = nullptr;
|
|
char *error = nullptr;
|
|
int cols = 0, affected_rows = 0;
|
|
|
|
stats->statsdb_disk->execute_statement(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tsdb_metrics'",
|
|
&error, &cols, &affected_rows, &resultset);
|
|
if (error) { free(error); error = nullptr; }
|
|
ok(resultset != nullptr && resultset->rows_count > 0 &&
|
|
strcmp(resultset->rows[0]->fields[0], "1") == 0,
|
|
"init: tsdb_metrics table exists on disk");
|
|
delete resultset;
|
|
|
|
resultset = nullptr;
|
|
stats->statsdb_disk->execute_statement(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tsdb_metrics_hour'",
|
|
&error, &cols, &affected_rows, &resultset);
|
|
if (error) { free(error); error = nullptr; }
|
|
ok(resultset != nullptr && resultset->rows_count > 0 &&
|
|
strcmp(resultset->rows[0]->fields[0], "1") == 0,
|
|
"init: tsdb_metrics_hour table exists on disk");
|
|
delete resultset;
|
|
|
|
resultset = nullptr;
|
|
stats->statsdb_disk->execute_statement(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tsdb_backend_health'",
|
|
&error, &cols, &affected_rows, &resultset);
|
|
if (error) { free(error); error = nullptr; }
|
|
ok(resultset != nullptr && resultset->rows_count > 0 &&
|
|
strcmp(resultset->rows[0]->fields[0], "1") == 0,
|
|
"init: tsdb_backend_health table exists on disk");
|
|
delete resultset;
|
|
}
|
|
#endif
|
|
|
|
// ============================================================
|
|
// Timer functions — MySQL_Threads_Handler_timetoget
|
|
// ============================================================
|
|
|
|
static void test_timer_returns_false_when_disabled() {
|
|
// When stats_mysql_connections is 0, timetoget should return false
|
|
stats->variables.stats_mysql_connections = 0;
|
|
bool result = stats->MySQL_Threads_Handler_timetoget(1000000ULL);
|
|
ok(result == false, "MySQL_Threads_Handler_timetoget: returns false when interval is 0");
|
|
}
|
|
|
|
static void test_timer_returns_true_on_first_call() {
|
|
// With interval > 0 and curtime > next_timer (initially 0), should return true
|
|
stats->variables.stats_mysql_connections = 5;
|
|
bool result = stats->MySQL_Threads_Handler_timetoget(5000000ULL);
|
|
ok(result == true, "MySQL_Threads_Handler_timetoget: returns true on first call (timer was 0)");
|
|
}
|
|
|
|
static void test_timer_returns_false_before_interval() {
|
|
// After a successful trigger, calling again immediately should return false
|
|
stats->variables.stats_mysql_connections = 60; // 60 second interval
|
|
// First call triggers and sets next_timer
|
|
stats->MySQL_Threads_Handler_timetoget(1000000000ULL); // 1000 seconds in usec
|
|
// Call again at almost the same time — should not trigger
|
|
bool result = stats->MySQL_Threads_Handler_timetoget(1000000001ULL);
|
|
ok(result == false, "MySQL_Threads_Handler_timetoget: returns false before interval elapses");
|
|
}
|
|
|
|
static void test_timer_returns_true_after_interval() {
|
|
stats->variables.stats_mysql_connections = 5; // 5 second interval
|
|
// First call at t=1000s triggers
|
|
stats->MySQL_Threads_Handler_timetoget(1000000000ULL);
|
|
// Call again at t=1006s — should trigger (5s+ elapsed)
|
|
bool result = stats->MySQL_Threads_Handler_timetoget(1006000000ULL);
|
|
ok(result == true, "MySQL_Threads_Handler_timetoget: returns true after interval elapses");
|
|
}
|
|
|
|
// ============================================================
|
|
// Timer functions — MySQL_Query_Cache_timetoget
|
|
// ============================================================
|
|
|
|
static void test_query_cache_timer_disabled() {
|
|
stats->variables.stats_mysql_query_cache = 0;
|
|
ok(stats->MySQL_Query_Cache_timetoget(5000000ULL) == false,
|
|
"MySQL_Query_Cache_timetoget: returns false when interval is 0");
|
|
}
|
|
|
|
static void test_query_cache_timer_triggers() {
|
|
stats->variables.stats_mysql_query_cache = 10;
|
|
ok(stats->MySQL_Query_Cache_timetoget(2000000000ULL) == true,
|
|
"MySQL_Query_Cache_timetoget: returns true on first call");
|
|
}
|
|
|
|
// ============================================================
|
|
// Timer functions — system_cpu_timetoget
|
|
// ============================================================
|
|
|
|
static void test_system_cpu_timer_disabled() {
|
|
stats->variables.stats_system_cpu = 0;
|
|
ok(stats->system_cpu_timetoget(5000000ULL) == false,
|
|
"system_cpu_timetoget: returns false when interval is 0");
|
|
}
|
|
|
|
static void test_system_cpu_timer_triggers() {
|
|
stats->variables.stats_system_cpu = 10;
|
|
ok(stats->system_cpu_timetoget(3000000000ULL) == true,
|
|
"system_cpu_timetoget: returns true on first call");
|
|
}
|
|
|
|
// ============================================================
|
|
// Timer functions — eventslog timers
|
|
// ============================================================
|
|
|
|
static void test_mysql_eventslog_timer_disabled() {
|
|
stats->variables.stats_mysql_eventslog_sync_buffer_to_disk = 0;
|
|
ok(stats->MySQL_Logger_dump_eventslog_timetoget(5000000ULL) == false,
|
|
"MySQL_Logger_dump_eventslog_timetoget: returns false when interval is 0");
|
|
}
|
|
|
|
static void test_mysql_eventslog_timer_triggers() {
|
|
stats->variables.stats_mysql_eventslog_sync_buffer_to_disk = 5;
|
|
ok(stats->MySQL_Logger_dump_eventslog_timetoget(6000000ULL) == true,
|
|
"MySQL_Logger_dump_eventslog_timetoget: returns true on first call");
|
|
}
|
|
|
|
static void test_mysql_eventslog_timer_no_retrigger() {
|
|
stats->variables.stats_mysql_eventslog_sync_buffer_to_disk = 10;
|
|
stats->MySQL_Logger_dump_eventslog_timetoget(10000000ULL); // triggers, sets last_timer
|
|
ok(stats->MySQL_Logger_dump_eventslog_timetoget(15000000ULL) == false,
|
|
"MySQL_Logger_dump_eventslog_timetoget: returns false before 10s elapses");
|
|
}
|
|
|
|
static void test_pgsql_eventslog_timer_disabled() {
|
|
stats->variables.stats_pgsql_eventslog_sync_buffer_to_disk = 0;
|
|
ok(stats->PgSQL_Logger_dump_eventslog_timetoget(5000000ULL) == false,
|
|
"PgSQL_Logger_dump_eventslog_timetoget: returns false when interval is 0");
|
|
}
|
|
|
|
static void test_pgsql_eventslog_timer_triggers() {
|
|
stats->variables.stats_pgsql_eventslog_sync_buffer_to_disk = 5;
|
|
ok(stats->PgSQL_Logger_dump_eventslog_timetoget(6000000ULL) == true,
|
|
"PgSQL_Logger_dump_eventslog_timetoget: returns true on first call");
|
|
}
|
|
|
|
// ============================================================
|
|
// Timer functions — mysql_query_digest_to_disk_timetoget
|
|
// ============================================================
|
|
|
|
static void test_digest_to_disk_timer_disabled() {
|
|
stats->variables.stats_mysql_query_digest_to_disk = 0;
|
|
ok(stats->mysql_query_digest_to_disk_timetoget(5000000ULL) == false,
|
|
"mysql_query_digest_to_disk_timetoget: returns false when interval is 0");
|
|
}
|
|
|
|
static void test_digest_to_disk_timer_triggers() {
|
|
stats->variables.stats_mysql_query_digest_to_disk = 10;
|
|
ok(stats->mysql_query_digest_to_disk_timetoget(4000000000ULL) == true,
|
|
"mysql_query_digest_to_disk_timetoget: returns true on first call");
|
|
}
|
|
|
|
// ============================================================
|
|
// TSDB variable management (PROXYSQLTSDB only)
|
|
// ============================================================
|
|
|
|
#ifdef PROXYSQLTSDB
|
|
|
|
static void test_set_variable_enabled() {
|
|
ok(stats->set_variable("enabled", "1") == true,
|
|
"set_variable: 'enabled' accepts value '1'");
|
|
ok(stats->set_variable("enabled", "0") == true,
|
|
"set_variable: 'enabled' accepts value '0'");
|
|
}
|
|
|
|
static void test_set_variable_enabled_out_of_range() {
|
|
ok(stats->set_variable("enabled", "2") == false,
|
|
"set_variable: 'enabled' rejects value '2' (max=1)");
|
|
ok(stats->set_variable("enabled", "-1") == false,
|
|
"set_variable: 'enabled' rejects value '-1' (min=0)");
|
|
}
|
|
|
|
static void test_set_variable_sample_interval() {
|
|
ok(stats->set_variable("sample_interval", "1") == true,
|
|
"set_variable: 'sample_interval' accepts min value '1'");
|
|
ok(stats->set_variable("sample_interval", "3600") == true,
|
|
"set_variable: 'sample_interval' accepts max value '3600'");
|
|
ok(stats->set_variable("sample_interval", "0") == false,
|
|
"set_variable: 'sample_interval' rejects value '0' (below min)");
|
|
ok(stats->set_variable("sample_interval", "3601") == false,
|
|
"set_variable: 'sample_interval' rejects value '3601' (above max)");
|
|
}
|
|
|
|
static void test_set_variable_retention_days() {
|
|
ok(stats->set_variable("retention_days", "1") == true,
|
|
"set_variable: 'retention_days' accepts value '1'");
|
|
ok(stats->set_variable("retention_days", "3650") == true,
|
|
"set_variable: 'retention_days' accepts max value '3650'");
|
|
ok(stats->set_variable("retention_days", "0") == false,
|
|
"set_variable: 'retention_days' rejects '0'");
|
|
ok(stats->set_variable("retention_days", "3651") == false,
|
|
"set_variable: 'retention_days' rejects '3651'");
|
|
}
|
|
|
|
static void test_set_variable_monitor_enabled() {
|
|
ok(stats->set_variable("monitor_enabled", "0") == true,
|
|
"set_variable: 'monitor_enabled' accepts '0'");
|
|
ok(stats->set_variable("monitor_enabled", "1") == true,
|
|
"set_variable: 'monitor_enabled' accepts '1'");
|
|
ok(stats->set_variable("monitor_enabled", "3") == false,
|
|
"set_variable: 'monitor_enabled' rejects '3'");
|
|
}
|
|
|
|
static void test_set_variable_monitor_interval() {
|
|
ok(stats->set_variable("monitor_interval", "10") == true,
|
|
"set_variable: 'monitor_interval' accepts '10'");
|
|
ok(stats->set_variable("monitor_interval", "0") == false,
|
|
"set_variable: 'monitor_interval' rejects '0'");
|
|
}
|
|
|
|
static void test_set_variable_invalid_name() {
|
|
ok(stats->set_variable("nonexistent_var", "1") == false,
|
|
"set_variable: unknown variable name returns false");
|
|
}
|
|
|
|
static void test_set_variable_null_inputs() {
|
|
ok(stats->set_variable(nullptr, "1") == false,
|
|
"set_variable: null name returns false");
|
|
ok(stats->set_variable("enabled", nullptr) == false,
|
|
"set_variable: null value returns false");
|
|
ok(stats->set_variable("enabled", "") == false,
|
|
"set_variable: empty value returns false");
|
|
}
|
|
|
|
static void test_set_variable_non_integer_value() {
|
|
ok(stats->set_variable("enabled", "abc") == false,
|
|
"set_variable: non-integer value returns false");
|
|
ok(stats->set_variable("enabled", "1.5") == false,
|
|
"set_variable: float value returns false");
|
|
ok(stats->set_variable("enabled", "1abc") == false,
|
|
"set_variable: trailing garbage returns false");
|
|
}
|
|
|
|
static void test_set_variable_case_insensitive() {
|
|
ok(stats->set_variable("ENABLED", "1") == true,
|
|
"set_variable: variable name is case-insensitive (uppercase)");
|
|
ok(stats->set_variable("Enabled", "0") == true,
|
|
"set_variable: variable name is case-insensitive (mixed case)");
|
|
}
|
|
|
|
static void test_get_variable() {
|
|
stats->set_variable("enabled", "1");
|
|
char *val = stats->get_variable("enabled");
|
|
ok(val != nullptr && strcmp(val, "1") == 0,
|
|
"get_variable: returns '1' after set_variable('enabled', '1')");
|
|
free(val);
|
|
|
|
stats->set_variable("sample_interval", "42");
|
|
val = stats->get_variable("sample_interval");
|
|
ok(val != nullptr && strcmp(val, "42") == 0,
|
|
"get_variable: returns '42' after set_variable('sample_interval', '42')");
|
|
free(val);
|
|
|
|
stats->set_variable("retention_days", "365");
|
|
val = stats->get_variable("retention_days");
|
|
ok(val != nullptr && strcmp(val, "365") == 0,
|
|
"get_variable: returns '365' after set_variable('retention_days', '365')");
|
|
free(val);
|
|
|
|
stats->set_variable("monitor_enabled", "1");
|
|
val = stats->get_variable("monitor_enabled");
|
|
ok(val != nullptr && strcmp(val, "1") == 0,
|
|
"get_variable: returns '1' for monitor_enabled");
|
|
free(val);
|
|
|
|
stats->set_variable("monitor_interval", "30");
|
|
val = stats->get_variable("monitor_interval");
|
|
ok(val != nullptr && strcmp(val, "30") == 0,
|
|
"get_variable: returns '30' for monitor_interval");
|
|
free(val);
|
|
}
|
|
|
|
static void test_get_variable_null_and_unknown() {
|
|
ok(stats->get_variable(nullptr) == nullptr,
|
|
"get_variable: null name returns null");
|
|
ok(stats->get_variable("nonexistent") == nullptr,
|
|
"get_variable: unknown name returns null");
|
|
}
|
|
|
|
static void test_has_variable() {
|
|
ok(stats->has_variable("enabled") == true,
|
|
"has_variable: 'enabled' exists");
|
|
ok(stats->has_variable("sample_interval") == true,
|
|
"has_variable: 'sample_interval' exists");
|
|
ok(stats->has_variable("retention_days") == true,
|
|
"has_variable: 'retention_days' exists");
|
|
ok(stats->has_variable("monitor_enabled") == true,
|
|
"has_variable: 'monitor_enabled' exists");
|
|
ok(stats->has_variable("monitor_interval") == true,
|
|
"has_variable: 'monitor_interval' exists");
|
|
ok(stats->has_variable("nonexistent") == false,
|
|
"has_variable: 'nonexistent' does not exist");
|
|
ok(stats->has_variable(nullptr) == false,
|
|
"has_variable: null returns false");
|
|
}
|
|
|
|
static void test_get_variables_list() {
|
|
char **list = stats->get_variables_list();
|
|
ok(list != nullptr, "get_variables_list: returns non-null");
|
|
|
|
int count = 0;
|
|
if (list) {
|
|
while (list[count] != nullptr) count++;
|
|
}
|
|
ok(count == 5, "get_variables_list: returns 5 variables (got %d)", count);
|
|
|
|
// Free the list
|
|
if (list) {
|
|
for (int i = 0; list[i]; i++) free(list[i]);
|
|
free(list);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TSDB timer functions
|
|
// ============================================================
|
|
|
|
static void test_tsdb_sampler_timer_disabled() {
|
|
stats->set_variable("enabled", "0");
|
|
ok(stats->tsdb_sampler_timetoget(5000000ULL) == false,
|
|
"tsdb_sampler_timetoget: returns false when TSDB disabled");
|
|
}
|
|
|
|
static void test_tsdb_sampler_timer_triggers() {
|
|
stats->set_variable("enabled", "1");
|
|
stats->set_variable("sample_interval", "5");
|
|
// next_timer starts at 0, curtime > 0 should trigger
|
|
ok(stats->tsdb_sampler_timetoget(1000000ULL) == true,
|
|
"tsdb_sampler_timetoget: returns true on first call when enabled");
|
|
}
|
|
|
|
static void test_tsdb_sampler_timer_no_retrigger() {
|
|
stats->set_variable("enabled", "1");
|
|
stats->set_variable("sample_interval", "10");
|
|
stats->tsdb_sampler_timetoget(1000000ULL); // triggers, sets next = 1 + 10s = 11000000
|
|
ok(stats->tsdb_sampler_timetoget(5000000ULL) == false,
|
|
"tsdb_sampler_timetoget: returns false before interval elapses");
|
|
}
|
|
|
|
static void test_tsdb_downsample_timer() {
|
|
stats->set_variable("enabled", "0");
|
|
ok(stats->tsdb_downsample_timetoget(5000000ULL) == false,
|
|
"tsdb_downsample_timetoget: returns false when disabled");
|
|
|
|
stats->set_variable("enabled", "1");
|
|
ok(stats->tsdb_downsample_timetoget(1000000ULL) == true,
|
|
"tsdb_downsample_timetoget: returns true on first call when enabled");
|
|
}
|
|
|
|
static void test_tsdb_monitor_timer() {
|
|
stats->set_variable("enabled", "1");
|
|
stats->set_variable("monitor_enabled", "0");
|
|
ok(stats->tsdb_monitor_timetoget(5000000ULL) == false,
|
|
"tsdb_monitor_timetoget: returns false when monitor disabled");
|
|
|
|
stats->set_variable("monitor_enabled", "1");
|
|
stats->set_variable("monitor_interval", "10");
|
|
ok(stats->tsdb_monitor_timetoget(1000000ULL) == true,
|
|
"tsdb_monitor_timetoget: returns true on first call when both enabled");
|
|
}
|
|
|
|
static void test_tsdb_retention_timer() {
|
|
stats->set_variable("enabled", "0");
|
|
ok(stats->tsdb_retention_timetoget(5000000ULL) == false,
|
|
"tsdb_retention_timetoget: returns false when disabled");
|
|
|
|
stats->set_variable("enabled", "1");
|
|
ok(stats->tsdb_retention_timetoget(1000000ULL) == true,
|
|
"tsdb_retention_timetoget: returns true on first call when enabled");
|
|
}
|
|
|
|
// ============================================================
|
|
// TSDB metric insertion and query
|
|
// ============================================================
|
|
|
|
static void test_insert_and_query_tsdb_metric() {
|
|
stats->set_variable("enabled", "1");
|
|
|
|
std::map<std::string, std::string> labels;
|
|
labels["instance"] = "localhost:6032";
|
|
|
|
time_t now = time(nullptr);
|
|
stats->insert_tsdb_metric("test_metric", labels, 42.5, now);
|
|
stats->insert_tsdb_metric("test_metric", labels, 43.0, now + 1);
|
|
|
|
// Query back
|
|
std::map<std::string, std::string> no_filter;
|
|
SQLite3_result *result = stats->query_tsdb_metrics("test_metric", no_filter, now - 10, now + 10);
|
|
ok(result != nullptr, "query_tsdb_metrics: returns non-null result for inserted metric");
|
|
if (result) {
|
|
ok(result->rows_count == 2, "query_tsdb_metrics: returns 2 rows (got %lu)",
|
|
(unsigned long)result->rows_count);
|
|
delete result;
|
|
} else {
|
|
ok(false, "query_tsdb_metrics: returns 2 rows (null result)");
|
|
}
|
|
}
|
|
|
|
static void test_query_tsdb_metric_with_label_filter() {
|
|
time_t now = time(nullptr) + 100; // offset to not collide with previous test
|
|
|
|
std::map<std::string, std::string> labels_a;
|
|
labels_a["host"] = "server_a";
|
|
stats->insert_tsdb_metric("cpu_usage", labels_a, 75.0, now);
|
|
|
|
std::map<std::string, std::string> labels_b;
|
|
labels_b["host"] = "server_b";
|
|
stats->insert_tsdb_metric("cpu_usage", labels_b, 90.0, now);
|
|
|
|
// Filter by label
|
|
std::map<std::string, std::string> filter;
|
|
filter["host"] = "server_a";
|
|
SQLite3_result *result = stats->query_tsdb_metrics("cpu_usage", filter, now - 10, now + 10);
|
|
ok(result != nullptr, "query_tsdb_metrics: label filter returns non-null");
|
|
if (result) {
|
|
ok(result->rows_count == 1, "query_tsdb_metrics: label filter returns 1 row (got %lu)",
|
|
(unsigned long)result->rows_count);
|
|
delete result;
|
|
} else {
|
|
ok(false, "query_tsdb_metrics: label filter returns 1 row (null result)");
|
|
}
|
|
}
|
|
|
|
static void test_query_tsdb_metric_with_single_quote_in_name() {
|
|
// This exercises escape_sql_string_literal indirectly
|
|
time_t now = time(nullptr) + 200;
|
|
|
|
std::map<std::string, std::string> labels;
|
|
stats->insert_tsdb_metric("metric_with'quote", labels, 10.0, now);
|
|
|
|
std::map<std::string, std::string> no_filter;
|
|
SQLite3_result *result = stats->query_tsdb_metrics("metric_with'quote", no_filter, now - 10, now + 10);
|
|
ok(result != nullptr, "query_tsdb_metrics: metric name with single quote works (escape_sql_string_literal)");
|
|
if (result) {
|
|
ok(result->rows_count == 1, "query_tsdb_metrics: single-quote metric returns 1 row (got %lu)",
|
|
(unsigned long)result->rows_count);
|
|
delete result;
|
|
} else {
|
|
ok(false, "query_tsdb_metrics: single-quote metric returns 1 row (null result)");
|
|
}
|
|
}
|
|
|
|
static void test_query_tsdb_metric_swapped_time_range() {
|
|
// query_tsdb_metrics should swap from/to if to < from
|
|
time_t now = time(nullptr) + 300;
|
|
std::map<std::string, std::string> labels;
|
|
stats->insert_tsdb_metric("swap_test", labels, 1.0, now);
|
|
|
|
std::map<std::string, std::string> no_filter;
|
|
SQLite3_result *result = stats->query_tsdb_metrics("swap_test", no_filter, now + 10, now - 10);
|
|
ok(result != nullptr, "query_tsdb_metrics: swapped time range still returns result");
|
|
if (result) {
|
|
ok(result->rows_count == 1, "query_tsdb_metrics: swapped range returns correct row count");
|
|
delete result;
|
|
} else {
|
|
ok(false, "query_tsdb_metrics: swapped range returns correct row count (null)");
|
|
}
|
|
}
|
|
|
|
static void test_query_tsdb_metric_invalid_label_key() {
|
|
// valid_label_key rejects keys with special chars — query should return NULL
|
|
time_t now = time(nullptr) + 400;
|
|
std::map<std::string, std::string> bad_filter;
|
|
bad_filter["bad key!"] = "value"; // space and ! are invalid
|
|
|
|
SQLite3_result *result = stats->query_tsdb_metrics("some_metric", bad_filter, now - 10, now + 10);
|
|
ok(result == nullptr, "query_tsdb_metrics: invalid label key returns null (valid_label_key rejects it)");
|
|
}
|
|
|
|
// ============================================================
|
|
// TSDB backend health
|
|
// ============================================================
|
|
|
|
static void test_insert_and_query_backend_health() {
|
|
time_t now = time(nullptr) + 500;
|
|
|
|
stats->insert_backend_health(1, "db1.example.com", 3306, true, 5, now);
|
|
stats->insert_backend_health(1, "db2.example.com", 3306, false, -1, now);
|
|
stats->insert_backend_health(2, "db3.example.com", 5432, true, 2, now);
|
|
|
|
SQLite3_result *result = stats->get_backend_health_metrics(now - 10, now + 10);
|
|
ok(result != nullptr, "get_backend_health_metrics: returns non-null for inserted data");
|
|
if (result) {
|
|
ok(result->rows_count == 3, "get_backend_health_metrics: returns 3 rows (got %lu)",
|
|
(unsigned long)result->rows_count);
|
|
delete result;
|
|
} else {
|
|
ok(false, "get_backend_health_metrics: returns 3 rows (null result)");
|
|
}
|
|
|
|
// Filter by hostgroup
|
|
result = stats->get_backend_health_metrics(now - 10, now + 10, 1);
|
|
ok(result != nullptr, "get_backend_health_metrics: hostgroup filter returns non-null");
|
|
if (result) {
|
|
ok(result->rows_count == 2, "get_backend_health_metrics: hostgroup=1 returns 2 rows (got %lu)",
|
|
(unsigned long)result->rows_count);
|
|
delete result;
|
|
} else {
|
|
ok(false, "get_backend_health_metrics: hostgroup=1 returns 2 rows (null result)");
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TSDB status
|
|
// ============================================================
|
|
|
|
static void test_get_tsdb_status() {
|
|
// We have already inserted some metrics above
|
|
ProxySQL_Statistics::tsdb_status_t st = stats->get_tsdb_status();
|
|
ok(st.total_datapoints > 0, "get_tsdb_status: total_datapoints > 0 (got %zu)", st.total_datapoints);
|
|
ok(st.total_series > 0, "get_tsdb_status: total_series > 0 (got %zu)", st.total_series);
|
|
ok(st.newest_datapoint > 0, "get_tsdb_status: newest_datapoint is set");
|
|
}
|
|
|
|
// ============================================================
|
|
// TSDB downsampling
|
|
// ============================================================
|
|
|
|
static void test_tsdb_downsample_disabled() {
|
|
stats->set_variable("enabled", "0");
|
|
// Should just return without error
|
|
stats->tsdb_downsample_metrics();
|
|
ok(true, "tsdb_downsample_metrics: does not crash when TSDB disabled");
|
|
}
|
|
|
|
static void test_tsdb_retention_cleanup_disabled() {
|
|
stats->set_variable("enabled", "0");
|
|
stats->tsdb_retention_cleanup();
|
|
ok(true, "tsdb_retention_cleanup: does not crash when TSDB disabled");
|
|
}
|
|
|
|
static void test_tsdb_downsample_runs() {
|
|
stats->set_variable("enabled", "1");
|
|
// Insert some metrics at specific bucket-aligned times
|
|
time_t hour_start = (time(nullptr) / 3600) * 3600 - 7200; // 2 hours ago
|
|
std::map<std::string, std::string> labels;
|
|
for (int i = 0; i < 10; i++) {
|
|
stats->insert_tsdb_metric("downsample_test", labels, (double)i, hour_start + i * 60);
|
|
}
|
|
|
|
stats->tsdb_downsample_metrics();
|
|
ok(true, "tsdb_downsample_metrics: runs without error on data");
|
|
|
|
// Check that hourly data was created
|
|
SQLite3_result *resultset = nullptr;
|
|
char *error = nullptr;
|
|
int cols = 0, affected_rows = 0;
|
|
stats->statsdb_disk->execute_statement(
|
|
"SELECT COUNT(*) FROM tsdb_metrics_hour WHERE metric_name='downsample_test'",
|
|
&error, &cols, &affected_rows, &resultset);
|
|
if (error) { free(error); error = nullptr; }
|
|
bool has_hourly = false;
|
|
if (resultset && resultset->rows_count > 0 && resultset->rows[0]->fields[0]) {
|
|
has_hourly = atoi(resultset->rows[0]->fields[0]) > 0;
|
|
}
|
|
ok(has_hourly, "tsdb_downsample_metrics: creates hourly aggregated data");
|
|
delete resultset;
|
|
}
|
|
|
|
static void test_tsdb_retention_cleanup_runs() {
|
|
stats->set_variable("enabled", "1");
|
|
stats->set_variable("retention_days", "1");
|
|
|
|
// Insert old data
|
|
time_t old_time = time(nullptr) - 86400 * 5; // 5 days ago
|
|
std::map<std::string, std::string> labels;
|
|
stats->insert_tsdb_metric("old_metric", labels, 1.0, old_time);
|
|
|
|
stats->tsdb_retention_cleanup();
|
|
|
|
// Verify old data was removed
|
|
SQLite3_result *resultset = nullptr;
|
|
char *error = nullptr;
|
|
int cols = 0, affected_rows = 0;
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf),
|
|
"SELECT COUNT(*) FROM tsdb_metrics WHERE metric_name='old_metric' AND timestamp < %ld",
|
|
(long)(time(nullptr) - 86400));
|
|
stats->statsdb_disk->execute_statement(buf, &error, &cols, &affected_rows, &resultset);
|
|
if (error) { free(error); error = nullptr; }
|
|
bool old_removed = false;
|
|
if (resultset && resultset->rows_count > 0 && resultset->rows[0]->fields[0]) {
|
|
old_removed = atoi(resultset->rows[0]->fields[0]) == 0;
|
|
}
|
|
ok(old_removed, "tsdb_retention_cleanup: removes data older than retention_days");
|
|
delete resultset;
|
|
}
|
|
|
|
#endif // PROXYSQLTSDB
|
|
|
|
// ============================================================
|
|
// Main
|
|
// ============================================================
|
|
|
|
int main() {
|
|
// Count tests
|
|
int num_tests = 0;
|
|
|
|
// Constructor + init tests: 5
|
|
num_tests += 5;
|
|
// Timer tests: 15
|
|
num_tests += 15;
|
|
|
|
#ifdef PROXYSQLTSDB
|
|
// TSDB table init: 3
|
|
num_tests += 3;
|
|
// Variable management: 42
|
|
num_tests += 42;
|
|
// TSDB timers: 9
|
|
num_tests += 9;
|
|
// TSDB metric insert/query: 9
|
|
num_tests += 9;
|
|
// TSDB backend health: 4
|
|
num_tests += 4;
|
|
// TSDB status: 3
|
|
num_tests += 3;
|
|
// TSDB downsampling/retention: 5
|
|
num_tests += 5;
|
|
#endif
|
|
|
|
plan(num_tests);
|
|
|
|
test_init_minimal();
|
|
setup_stats();
|
|
|
|
// Constructor + init
|
|
test_constructor_creates_databases();
|
|
test_init_creates_tables();
|
|
|
|
// Timer tests
|
|
test_timer_returns_false_when_disabled();
|
|
test_timer_returns_true_on_first_call();
|
|
test_timer_returns_false_before_interval();
|
|
test_timer_returns_true_after_interval();
|
|
test_query_cache_timer_disabled();
|
|
test_query_cache_timer_triggers();
|
|
test_system_cpu_timer_disabled();
|
|
test_system_cpu_timer_triggers();
|
|
test_mysql_eventslog_timer_disabled();
|
|
test_mysql_eventslog_timer_triggers();
|
|
test_mysql_eventslog_timer_no_retrigger();
|
|
test_pgsql_eventslog_timer_disabled();
|
|
test_pgsql_eventslog_timer_triggers();
|
|
test_digest_to_disk_timer_disabled();
|
|
test_digest_to_disk_timer_triggers();
|
|
|
|
#ifdef PROXYSQLTSDB
|
|
// TSDB table init
|
|
test_init_creates_tsdb_tables();
|
|
|
|
// Variable management
|
|
test_set_variable_enabled();
|
|
test_set_variable_enabled_out_of_range();
|
|
test_set_variable_sample_interval();
|
|
test_set_variable_retention_days();
|
|
test_set_variable_monitor_enabled();
|
|
test_set_variable_monitor_interval();
|
|
test_set_variable_invalid_name();
|
|
test_set_variable_null_inputs();
|
|
test_set_variable_non_integer_value();
|
|
test_set_variable_case_insensitive();
|
|
test_get_variable();
|
|
test_get_variable_null_and_unknown();
|
|
test_has_variable();
|
|
test_get_variables_list();
|
|
|
|
// TSDB timers
|
|
test_tsdb_sampler_timer_disabled();
|
|
test_tsdb_sampler_timer_triggers();
|
|
test_tsdb_sampler_timer_no_retrigger();
|
|
test_tsdb_downsample_timer();
|
|
test_tsdb_monitor_timer();
|
|
test_tsdb_retention_timer();
|
|
|
|
// TSDB metric insert/query
|
|
test_insert_and_query_tsdb_metric();
|
|
test_query_tsdb_metric_with_label_filter();
|
|
test_query_tsdb_metric_with_single_quote_in_name();
|
|
test_query_tsdb_metric_swapped_time_range();
|
|
test_query_tsdb_metric_invalid_label_key();
|
|
|
|
// TSDB backend health
|
|
test_insert_and_query_backend_health();
|
|
|
|
// TSDB status
|
|
test_get_tsdb_status();
|
|
|
|
// TSDB downsampling/retention
|
|
test_tsdb_downsample_disabled();
|
|
test_tsdb_retention_cleanup_disabled();
|
|
test_tsdb_downsample_runs();
|
|
test_tsdb_retention_cleanup_runs();
|
|
#endif
|
|
|
|
teardown_stats();
|
|
test_cleanup_minimal();
|
|
|
|
return exit_status();
|
|
}
|