Merge pull request #5456 from sysown/copilot/add-ssl-tls-certificate-stats-table

feat: Add stats_tls_certificates and stats_global tables for TLS certificate metadata and tracking
copilot/feature-load-restapi-routes-config^2
René Cannaò 2 months ago committed by GitHub
commit cb8d0aa737
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -178,6 +178,13 @@
#define STATS_SQLITE_TABLE_TSDB "CREATE TABLE stats_tsdb (Variable_Name VARCHAR NOT NULL PRIMARY KEY , Variable_Value VARCHAR NOT NULL)"
/**
* @brief Table for global ProxySQL statistics that are not specific to MySQL or PgSQL.
* @details Populated at query time by stats___global(). Contains metrics such as TLS load
* status, certificate file paths, and future cross-protocol statistics.
*/
#define STATS_SQLITE_TABLE_GLOBAL "CREATE TABLE stats_proxysql_global (Variable_Name VARCHAR NOT NULL PRIMARY KEY , Variable_Value VARCHAR NOT NULL)"
#define STATS_SQLITE_TABLE_MEMORY_METRICS "CREATE TABLE stats_memory_metrics (Variable_Name VARCHAR NOT NULL PRIMARY KEY , Variable_Value VARCHAR NOT NULL)"
#define STATS_SQLITE_TABLE_MYSQL_GTID_EXECUTED "CREATE TABLE stats_mysql_gtid_executed (hostname VARCHAR NOT NULL , port INT NOT NULL DEFAULT 3306 , gtid_executed VARCHAR , events INT NOT NULL)"
@ -188,6 +195,8 @@
#define STATS_SQLITE_TABLE_MYSQL_CLIENT_HOST_CACHE "CREATE TABLE stats_mysql_client_host_cache (client_address VARCHAR NOT NULL , error_count INT NOT NULL , last_updated BIGINT NOT NULL)"
#define STATS_SQLITE_TABLE_MYSQL_CLIENT_HOST_CACHE_RESET "CREATE TABLE stats_mysql_client_host_cache_reset (client_address VARCHAR NOT NULL , error_count INT NOT NULL , last_updated BIGINT NOT NULL)"
#define STATS_SQLITE_TABLE_TLS_CERTIFICATES "CREATE TABLE stats_tls_certificates (cert_type VARCHAR NOT NULL PRIMARY KEY , file_path VARCHAR NOT NULL , subject_cn VARCHAR , issuer_cn VARCHAR , serial_number VARCHAR , not_before VARCHAR , not_after VARCHAR , days_until_expiry INT , sha256_fingerprint VARCHAR , loaded_at INT NOT NULL DEFAULT 0)"
#ifdef DEBUG
#define ADMIN_SQLITE_TABLE_DEBUG_LEVELS "CREATE TABLE debug_levels (module VARCHAR NOT NULL PRIMARY KEY , verbosity INT NOT NULL DEFAULT 0)"
#define ADMIN_SQLITE_TABLE_DEBUG_FILTERS "CREATE TABLE debug_filters (filename VARCHAR NOT NULL , line INT NOT NULL , funct VARCHAR NOT NULL , PRIMARY KEY (filename, line, funct) )"

@ -808,6 +808,8 @@ class ProxySQL_Admin {
void stats___mysql_prepared_statements_info();
void stats___mysql_gtid_executed();
void stats___mysql_client_host_cache(bool reset);
void stats___tls_certificates();
void stats___proxysql_global();
#ifdef PROXYSQLGENAI
void stats___mcp_query_tools_counters(bool reset);

@ -141,6 +141,12 @@ class ProxySQL_GlobalVariables {
pthread_mutex_t ext_glomth_mutex;
pthread_mutex_t ext_glopth_mutex;
bool ssl_keylog_enabled;
uint64_t tls_load_count;
time_t tls_last_load_timestamp;
bool tls_last_load_ok;
char *tls_cert_file;
char *tls_ca_file;
char *tls_key_file;
} global;
struct mysql {
char *server_version;

@ -893,6 +893,8 @@ bool ProxySQL_Admin::init(const bootstrap_info_t& bootstrap_info) {
insert_into_tables_defs(tables_defs_stats,"stats_mysql_client_host_cache", STATS_SQLITE_TABLE_MYSQL_CLIENT_HOST_CACHE);
insert_into_tables_defs(tables_defs_stats,"stats_mysql_client_host_cache_reset", STATS_SQLITE_TABLE_MYSQL_CLIENT_HOST_CACHE_RESET);
insert_into_tables_defs(tables_defs_stats,"stats_mysql_query_events", ADMIN_SQLITE_TABLE_STATS_MYSQL_QUERY_EVENTS);
insert_into_tables_defs(tables_defs_stats,"stats_tls_certificates", STATS_SQLITE_TABLE_TLS_CERTIFICATES);
insert_into_tables_defs(tables_defs_stats,"stats_proxysql_global", STATS_SQLITE_TABLE_GLOBAL);
insert_into_tables_defs(tables_defs_stats,"stats_pgsql_global", STATS_SQLITE_TABLE_PGSQL_GLOBAL);
insert_into_tables_defs(tables_defs_stats,"stats_pgsql_connection_pool", STATS_SQLITE_TABLE_PGSQL_CONNECTION_POOL);

@ -1262,6 +1262,8 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign
bool stats_mysql_client_host_cache_reset=false;
bool stats_pgsql_client_host_cache = false;
bool stats_pgsql_client_host_cache_reset = false;
bool stats_tls_certificates=false;
bool stats_proxysql_global=false;
bool dump_global_variables=false;
bool runtime_scheduler=false;
@ -1446,6 +1448,10 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign
{ stats_pgsql_client_host_cache = true; refresh = true; }
if (strstr(query_no_space, "stats_pgsql_client_host_cache_reset"))
{ stats_pgsql_client_host_cache_reset = true; refresh = true; }
if (strstr(query_no_space,"stats_tls_certificates"))
{ stats_tls_certificates=true; refresh=true; }
if (strstr(query_no_space,"stats_proxysql_global"))
{ stats_proxysql_global=true; refresh=true; }
if (strstr(query_no_space,"stats_proxysql_servers_checksums"))
{ stats_proxysql_servers_checksums = true; refresh = true; }
if (strstr(query_no_space,"stats_proxysql_servers_metrics"))
@ -1705,6 +1711,12 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign
if (stats_pgsql_client_host_cache_reset) {
stats___pgsql_client_host_cache(true);
}
if (stats_tls_certificates) {
stats___tls_certificates();
}
if (stats_proxysql_global) {
stats___proxysql_global();
}
#ifdef PROXYSQLGENAI
if (stats_mcp_query_tools_counters) {
stats___mcp_query_tools_counters(false);

@ -25,6 +25,7 @@
#include "Query_Tool_Handler.h"
#include "RAG_Tool_Handler.h"
#endif /* PROXYSQLGENAI */
#include <openssl/x509v3.h>
#define SAFE_SQLITE3_STEP(_stmt) do {\
do {\
@ -522,6 +523,18 @@ const void sqlite3_global_stats_row_step(
rc = (*proxy_sqlite3_reset)(stmt); ASSERT_SQLITE_OK(rc, db);
};
static void sqlite3_global_stats_row_step_str(
SQLite3DB* db, sqlite3_stmt* stmt, const char* name, const char* val
) {
int rc = (*proxy_sqlite3_bind_text)(stmt, 1, name, -1, SQLITE_TRANSIENT);
ASSERT_SQLITE_OK(rc, db);
rc = (*proxy_sqlite3_bind_text)(stmt, 2, val ? val : "", -1, SQLITE_TRANSIENT);
ASSERT_SQLITE_OK(rc, db);
SAFE_SQLITE3_STEP2(stmt);
rc = (*proxy_sqlite3_clear_bindings)(stmt); ASSERT_SQLITE_OK(rc, db);
rc = (*proxy_sqlite3_reset)(stmt); ASSERT_SQLITE_OK(rc, db);
};
void ProxySQL_Admin::stats___mysql_global() {
if (!GloMTH) return;
SQLite3_result * resultset=GloMTH->SQL3_GlobalStatus(true);
@ -615,7 +628,6 @@ void ProxySQL_Admin::stats___mysql_global() {
sqlite3_global_stats_row_step(statsdb, row_stmt, "mysql_listener_paused", admin_proxysql_mysql_paused);
sqlite3_global_stats_row_step(statsdb, row_stmt, "OpenSSL_Version_Num", OpenSSL_version_num());
if (GloMyLogger != nullptr) {
const string prefix = "MySQL_Logger_";
std::unordered_map<std::string, unsigned long long> metrics = GloMyLogger->getAllMetrics();
@ -804,6 +816,48 @@ void ProxySQL_Admin::stats___pgsql_global() {
statsdb->execute("COMMIT");
}
/**
* @brief Populates the `stats_proxysql_global` table with ProxySQL-wide metrics
* that are not specific to the MySQL or PgSQL protocol.
*
* @details This function is called at query time whenever the stats_proxysql_global table
* is accessed (e.g. "SELECT * FROM stats.stats_proxysql_global"). It deletes all existing
* rows and reinserts fresh values, ensuring `TLS_Last_Load_Timestamp` and other
* time-sensitive data are always current.
*
* Currently tracked variables:
* - TLS_Load_Count : Number of times TLS has been loaded or reloaded.
* - TLS_Last_Load_Timestamp : Unix timestamp of the most recent successful TLS load.
* - TLS_Last_Load_Result : "NONE", "SUCCESS", or "FAILED" depending on last load outcome.
* - TLS_Server_Cert_File : Path to the server TLS certificate file.
* - TLS_CA_Cert_File : Path to the CA certificate file.
* - TLS_Key_File : Path to the private key file.
*/
void ProxySQL_Admin::stats___proxysql_global() {
statsdb->execute("BEGIN");
statsdb->execute("DELETE FROM stats_proxysql_global");
const string q_row_insert { "INSERT INTO stats_proxysql_global VALUES (?1, ?2)" };
int rc = 0;
stmt_unique_ptr u_row_stmt { nullptr };
std::tie(rc, u_row_stmt) = statsdb->prepare_v2(q_row_insert.c_str());
ASSERT_SQLITE_OK(rc, statsdb);
sqlite3_stmt *row_stmt = u_row_stmt.get();
{
std::lock_guard<std::mutex> lock(GloVars.global.ssl_mutex);
sqlite3_global_stats_row_step(statsdb, row_stmt, "TLS_Load_Count", GloVars.global.tls_load_count);
sqlite3_global_stats_row_step(statsdb, row_stmt, "TLS_Last_Load_Timestamp", (unsigned long long)GloVars.global.tls_last_load_timestamp);
const char *tls_result = GloVars.global.tls_load_count == 0 ? "NONE" : (GloVars.global.tls_last_load_ok ? "SUCCESS" : "FAILED");
sqlite3_global_stats_row_step_str(statsdb, row_stmt, "TLS_Last_Load_Result", tls_result);
sqlite3_global_stats_row_step_str(statsdb, row_stmt, "TLS_Server_Cert_File", GloVars.global.tls_cert_file ? GloVars.global.tls_cert_file : "");
sqlite3_global_stats_row_step_str(statsdb, row_stmt, "TLS_CA_Cert_File", GloVars.global.tls_ca_file ? GloVars.global.tls_ca_file : "");
sqlite3_global_stats_row_step_str(statsdb, row_stmt, "TLS_Key_File", GloVars.global.tls_key_file ? GloVars.global.tls_key_file : "");
}
statsdb->execute("COMMIT");
}
#ifdef PROXYSQLTSDB
void ProxySQL_Admin::stats___tsdb() {
if (!GloProxyStats) return;
@ -2841,3 +2895,147 @@ void ProxySQL_Admin::stats___mcp_query_rules() {
delete resultset;
}
#endif /* PROXYSQLGENAI */
// Helper: convert ASN1_TIME to ISO 8601 string (YYYY-MM-DDTHH:MM:SSZ)
static std::string asn1_time_to_iso8601(const ASN1_TIME *asn1t) {
if (!asn1t) return "";
struct tm t = {};
if (!ASN1_TIME_to_tm(asn1t, &t)) return "";
char buf[32] = {};
strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &t);
return std::string(buf);
}
// Helper: get CN from X509_NAME
static std::string x509_name_cn(X509_NAME *name) {
if (!name) return "";
char buf[1024] = {};
if (X509_NAME_get_text_by_NID(name, NID_commonName, buf, sizeof(buf)) < 0)
return "";
return std::string(buf);
}
// Helper: get hex serial number from X509
static std::string x509_serial_hex(X509 *cert) {
ASN1_INTEGER *serial = X509_get_serialNumber(cert);
if (!serial) return "";
BIGNUM *bn = ASN1_INTEGER_to_BN(serial, NULL);
if (!bn) return "";
char *hex = BN_bn2hex(bn);
std::string result = hex ? std::string(hex) : "";
OPENSSL_free(hex);
BN_free(bn);
return result;
}
// Helper: get SHA-256 fingerprint hex string from X509
static std::string x509_sha256_fingerprint(X509 *cert) {
unsigned char md[EVP_MAX_MD_SIZE] = {};
unsigned int len = 0;
if (!X509_digest(cert, EVP_sha256(), md, &len)) return "";
std::string result;
result.reserve(len * 2);
char hex_byte[3] = {};
for (unsigned int i = 0; i < len; i++) {
snprintf(hex_byte, sizeof(hex_byte), "%02X", md[i]);
result += hex_byte;
}
return result;
}
// Helper: insert one certificate row into stats_tls_certificates
static void insert_tls_cert_row(
SQLite3DB *statsdb, sqlite3_stmt *stmt,
const char *cert_type, const char *file_path,
X509 *cert, time_t loaded_at
) {
std::string subject_cn = x509_name_cn(X509_get_subject_name(cert));
std::string issuer_cn = x509_name_cn(X509_get_issuer_name(cert));
std::string serial_num = x509_serial_hex(cert);
std::string not_before = asn1_time_to_iso8601(X509_get0_notBefore(cert));
std::string not_after = asn1_time_to_iso8601(X509_get0_notAfter(cert));
std::string fingerprint = x509_sha256_fingerprint(cert);
// Calculate days_until_expiry at query time
// pday = full days from now to not_after (negative if expired)
int pday = 0;
{
int psec = 0;
if (!ASN1_TIME_diff(&pday, &psec, NULL, X509_get0_notAfter(cert))) {
pday = 0; // on error, default to 0 (treat as expiring today)
}
}
int days_until_expiry = pday;
int rc = (*proxy_sqlite3_bind_text)(stmt, 1, cert_type, -1, SQLITE_TRANSIENT); // cert_type
ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_bind_text)(stmt, 2, file_path, -1, SQLITE_TRANSIENT); // file_path
ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_bind_text)(stmt, 3, subject_cn.c_str(), -1, SQLITE_TRANSIENT); // subject_cn
ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_bind_text)(stmt, 4, issuer_cn.c_str(), -1, SQLITE_TRANSIENT); // issuer_cn
ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_bind_text)(stmt, 5, serial_num.c_str(), -1, SQLITE_TRANSIENT); // serial_number
ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_bind_text)(stmt, 6, not_before.c_str(), -1, SQLITE_TRANSIENT); // not_before
ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_bind_text)(stmt, 7, not_after.c_str(), -1, SQLITE_TRANSIENT); // not_after
ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_bind_int)(stmt, 8, days_until_expiry); // days_until_expiry
ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_bind_text)(stmt, 9, fingerprint.c_str(), -1, SQLITE_TRANSIENT); // sha256_fingerprint
ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_bind_int64)(stmt, 10, (sqlite3_int64)loaded_at); // loaded_at
ASSERT_SQLITE_OK(rc, statsdb);
SAFE_SQLITE3_STEP2(stmt);
rc = (*proxy_sqlite3_clear_bindings)(stmt); ASSERT_SQLITE_OK(rc, statsdb);
rc = (*proxy_sqlite3_reset)(stmt); ASSERT_SQLITE_OK(rc, statsdb);
}
void ProxySQL_Admin::stats___tls_certificates() {
statsdb->execute("BEGIN");
statsdb->execute("DELETE FROM stats_tls_certificates");
// Copy cert file paths and tracking info under ssl_mutex to avoid races
char *cert_file = NULL;
char *ca_file = NULL;
time_t loaded_at = 0;
{
std::lock_guard<std::mutex> lock(GloVars.global.ssl_mutex);
if (GloVars.global.tls_cert_file)
cert_file = strdup(GloVars.global.tls_cert_file);
if (GloVars.global.tls_ca_file)
ca_file = strdup(GloVars.global.tls_ca_file);
loaded_at = GloVars.global.tls_last_load_timestamp;
}
const char *insert_q =
"INSERT INTO stats_tls_certificates VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)";
int rc = 0;
stmt_unique_ptr u_stmt { nullptr };
std::tie(rc, u_stmt) = statsdb->prepare_v2(insert_q);
ASSERT_SQLITE_OK(rc, statsdb);
sqlite3_stmt *stmt = u_stmt.get();
// Helper lambda: read a PEM cert from file and insert a row into stats_tls_certificates
auto process_cert = [&](const char *cert_type, char *file_path) {
if (!file_path) return;
BIO *bio = BIO_new_file(file_path, "r");
if (bio) {
X509 *cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
BIO_free_all(bio);
if (cert) {
insert_tls_cert_row(statsdb, stmt, cert_type, file_path, cert, loaded_at);
X509_free(cert);
}
}
free(file_path);
};
process_cert("server", cert_file);
process_cert("ca", ca_file);
statsdb->execute("COMMIT");
}

@ -170,6 +170,18 @@ ProxySQL_GlobalVariables::~ProxySQL_GlobalVariables() {
free(global.gr_bootstrap_ssl_mode);
global.gr_bootstrap_ssl_mode = nullptr;
}
if (global.tls_cert_file) {
free(global.tls_cert_file);
global.tls_cert_file = nullptr;
}
if (global.tls_ca_file) {
free(global.tls_ca_file);
global.tls_ca_file = nullptr;
}
if (global.tls_key_file) {
free(global.tls_key_file);
global.tls_key_file = nullptr;
}
};
ProxySQL_GlobalVariables::ProxySQL_GlobalVariables() :
@ -246,6 +258,12 @@ ProxySQL_GlobalVariables::ProxySQL_GlobalVariables() :
global.gr_bootstrap_ssl_key = nullptr;
global.gr_bootstrap_ssl_mode = nullptr;
global.ssl_keylog_enabled = false;
global.tls_load_count = 0;
global.tls_last_load_timestamp = 0;
global.tls_last_load_ok = false;
global.tls_cert_file = NULL;
global.tls_ca_file = NULL;
global.tls_key_file = NULL;
opt = new ez::ezOptionParser();
opt->overview = "High Performance Advanced Proxy for MySQL";
opt->syntax = "proxysql [OPTIONS]";

@ -406,6 +406,20 @@ int ProxySQL_create_or_load_TLS(bool bootstrap, std::string& msg) {
// clients (MySQL > 8.0.29) attempt session reuses during reconnect operations.
SSL_CTX_set_options(GloVars.global.ssl_ctx, SSL_OP_NO_TICKET);
SSL_CTX_set_session_cache_mode(GloVars.global.ssl_ctx, SSL_SESS_CACHE_OFF);
// Store TLS file paths and tracking info for stats table
{
std::lock_guard<std::mutex> lock(GloVars.global.ssl_mutex);
free(GloVars.global.tls_key_file);
GloVars.global.tls_key_file = ssl_key_fp ? strdup(ssl_key_fp) : NULL;
free(GloVars.global.tls_cert_file);
GloVars.global.tls_cert_file = ssl_cert_fp ? strdup(ssl_cert_fp) : NULL;
free(GloVars.global.tls_ca_file);
GloVars.global.tls_ca_file = ssl_ca_fp ? strdup(ssl_ca_fp) : NULL;
GloVars.global.tls_load_count++;
GloVars.global.tls_last_load_timestamp = time(NULL);
GloVars.global.tls_last_load_ok = true;
}
} else {
// here we use global.tmp_ssl_ctx instead of global.ssl_ctx
// because we will try to swap at the end
@ -425,6 +439,16 @@ int ProxySQL_create_or_load_TLS(bool bootstrap, std::string& msg) {
free(GloVars.global.ssl_cert_pem_mem);
GloVars.global.ssl_key_pem_mem = load_file(ssl_key_fp);
GloVars.global.ssl_cert_pem_mem = load_file(ssl_cert_fp);
// Update TLS tracking fields for stats table (under ssl_mutex)
free(GloVars.global.tls_key_file);
GloVars.global.tls_key_file = ssl_key_fp ? strdup(ssl_key_fp) : NULL;
free(GloVars.global.tls_cert_file);
GloVars.global.tls_cert_file = ssl_cert_fp ? strdup(ssl_cert_fp) : NULL;
free(GloVars.global.tls_ca_file);
GloVars.global.tls_ca_file = ssl_ca_fp ? strdup(ssl_ca_fp) : NULL;
GloVars.global.tls_load_count++;
GloVars.global.tls_last_load_timestamp = time(NULL);
GloVars.global.tls_last_load_ok = true;
} else {
proxy_error("Failed to load location of CA certificates for verification\n");
@ -461,6 +485,12 @@ int ProxySQL_create_or_load_TLS(bool bootstrap, std::string& msg) {
// Completely disable session tickets and session-cache. See comment above.
SSL_CTX_set_options(GloVars.global.ssl_ctx, SSL_OP_NO_TICKET);
SSL_CTX_set_session_cache_mode(GloVars.global.ssl_ctx, SSL_SESS_CACHE_OFF);
} else if (!bootstrap) {
// Record reload failure in TLS tracking stats
std::lock_guard<std::mutex> lock(GloVars.global.ssl_mutex);
GloVars.global.tls_load_count++;
GloVars.global.tls_last_load_timestamp = time(NULL);
GloVars.global.tls_last_load_ok = false;
}
X509_free(x509);
EVP_PKEY_free(pkey);

@ -312,6 +312,7 @@
"test_ssl_large_query-2-t" : [ "legacy-g4","mysql84-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"test_stats_proxysql_message_metrics-t" : [ "legacy-g4","mysql84-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"test_thread_conn_dist-t" : [ "legacy-g4","mysql84-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"test_tls_stats-t" : [ "legacy-g4","mysql84-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"test_throttle_max_bytes_per_second_to_client-t" : [ "legacy-g4","mysql84-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],
"test_tsdb_api-t" : [ "ai-g1" ],
"test_tsdb_variables-t" : [ "ai-g1" ],

@ -0,0 +1,344 @@
/**
* @file test_tls_stats-t.cpp
* @brief TAP test for stats_tls_certificates and stats_proxysql_global TLS metrics.
*
* @details This test verifies:
* 1. stats_proxysql_global table exists and is queryable from the stats schema.
* 2. stats_proxysql_global contains the expected TLS tracking variables:
* - TLS_Load_Count
* - TLS_Last_Load_Timestamp
* - TLS_Last_Load_Result
* - TLS_Server_Cert_File
* - TLS_CA_Cert_File
* - TLS_Key_File
* 3. TLS_Load_Count >= 1 (ProxySQL always loads certs at startup).
* 4. TLS_Last_Load_Timestamp is a positive Unix timestamp.
* 5. TLS_Last_Load_Result is one of "NONE", "SUCCESS", or "FAILED".
* 6. TLS_Server_Cert_File and TLS_CA_Cert_File are non-empty strings.
* 7. stats_tls_certificates table exists and is queryable.
* 8. stats_tls_certificates has exactly two rows (cert_type 'server' and 'ca').
* 9. Both rows have non-empty file_path, subject_cn, issuer_cn, serial_number,
* not_before, not_after, sha256_fingerprint.
* 10. days_until_expiry is a reasonable value (> -36500 and < 36500).
* 11. loaded_at is a positive Unix timestamp.
* 12. After PROXYSQL RELOAD TLS:
* - TLS_Load_Count in stats_proxysql_global increments by 1.
* - stats_tls_certificates rows are still present and non-empty.
* 13. TLS_Last_Load_Timestamp in stats_proxysql_global increases after PROXYSQL RELOAD TLS.
* 14. TLS-related variables are NOT present in stats_mysql_global.
*/
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "mysql.h"
#include "tap.h"
#include "command_line.h"
#include "utils.h"
using std::map;
using std::set;
using std::string;
using std::vector;
/** @brief Maximum plausible lifetime of a certificate in days (approx. 100 years). */
static const int MAX_CERT_DAYS = 36500;
/**
* @brief Executes a query and returns all rows as a map keyed by the first column.
* @param conn An open MySQL connection.
* @param query The SQL query to run (must return exactly 2 columns).
* @return A map<string, string> where key=col[0] and value=col[1].
*/
static map<string,string> query_key_value(MYSQL* conn, const char* query) {
map<string,string> result;
if (mysql_query(conn, query)) {
diag("Query failed: '%s' err='%s'", query, mysql_error(conn));
return result;
}
MYSQL_RES* res = mysql_store_result(conn);
if (!res) return result;
MYSQL_ROW row;
while ((row = mysql_fetch_row(res))) {
if (row[0] && row[1])
result[string(row[0])] = string(row[1]);
}
mysql_free_result(res);
return result;
}
/**
* @brief Executes a query and returns all rows as a vector of key-value maps.
* @param conn An open MySQL connection.
* @param query The SQL query to run.
* @param col_names Column names to use as map keys (in order).
* @return A vector of maps, one per result row.
*/
static vector<map<string,string>> query_rows(MYSQL* conn, const char* query, const vector<string>& col_names) {
vector<map<string,string>> rows;
if (mysql_query(conn, query)) {
diag("Query failed: '%s' err='%s'", query, mysql_error(conn));
return rows;
}
MYSQL_RES* res = mysql_store_result(conn);
if (!res) return rows;
MYSQL_ROW row;
while ((row = mysql_fetch_row(res))) {
map<string,string> r;
for (size_t i = 0; i < col_names.size(); i++) {
r[col_names[i]] = row[i] ? string(row[i]) : "";
}
rows.push_back(r);
}
mysql_free_result(res);
return rows;
}
int main(int argc, char** argv) {
CommandLine cl;
diag("TAP test for SSL/TLS Certificate Statistics Table");
diag("This test verifies that ProxySQL correctly reports TLS certificate information,");
diag("tracks TLS load operations in stats_proxysql_global, and updates these stats after a reload.");
if (cl.getEnv()) {
diag("Failed to get the required environmental variables.");
return exit_status();
}
// Tests:
// 1-5: stats_proxysql_global TLS variables present
// 6-9: stats_proxysql_global TLS variable values
// 10-11: TLS_Last_Load_Result valid value
// 12-16: stats_tls_certificates structure
// 17-24: stats_tls_certificates row data for 'server' and 'ca'
// 25-28: PROXYSQL RELOAD TLS increments counter and updates timestamp
// 29: TLS vars NOT in stats_mysql_global
plan(29);
diag("Connecting to ProxySQL Admin interface on %s:%d", cl.host, cl.admin_port);
MYSQL* admin = mysql_init(NULL);
if (!admin) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
return exit_status();
}
if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
return exit_status();
}
// -----------------------------------------------------------------------
// Part 1: stats_proxysql_global - TLS tracking variables
// -----------------------------------------------------------------------
diag("Step 1: Verifying TLS tracking variables in stats.stats_proxysql_global");
diag("--- Querying stats.stats_proxysql_global ---");
auto global_stats = query_key_value(admin,
"SELECT Variable_Name, Variable_Value FROM stats.stats_proxysql_global");
const vector<string> expected_tls_vars = {
"TLS_Load_Count",
"TLS_Last_Load_Timestamp",
"TLS_Last_Load_Result",
"TLS_Server_Cert_File",
"TLS_CA_Cert_File",
"TLS_Key_File",
};
for (const auto& var : expected_tls_vars) {
ok(global_stats.count(var) > 0,
"stats_proxysql_global: Variable '%s' is present", var.c_str());
}
diag("Verifying values of TLS tracking variables...");
// TLS_Load_Count >= 1
int tls_load_count = 0;
if (global_stats.count("TLS_Load_Count"))
tls_load_count = std::stoi(global_stats["TLS_Load_Count"]);
ok(tls_load_count >= 1,
"stats_proxysql_global: TLS_Load_Count is >= 1 (was %d)", tls_load_count);
// TLS_Last_Load_Timestamp is a positive Unix timestamp (> 0)
long long tls_ts = 0;
if (global_stats.count("TLS_Last_Load_Timestamp"))
tls_ts = std::stoll(global_stats["TLS_Last_Load_Timestamp"]);
ok(tls_ts > 0,
"stats_proxysql_global: TLS_Last_Load_Timestamp is positive (was %lld)", tls_ts);
// TLS_Last_Load_Result must be one of NONE, SUCCESS, FAILED
string tls_result = global_stats.count("TLS_Last_Load_Result") ?
global_stats["TLS_Last_Load_Result"] : "";
ok(tls_result == "SUCCESS" || tls_result == "NONE" || tls_result == "FAILED",
"stats_proxysql_global: TLS_Last_Load_Result='%s' is valid (NONE/SUCCESS/FAILED)",
tls_result.c_str());
// TLS_Server_Cert_File should be a non-empty path
string cert_file = global_stats.count("TLS_Server_Cert_File") ?
global_stats["TLS_Server_Cert_File"] : "";
ok(!cert_file.empty(),
"stats_proxysql_global: TLS_Server_Cert_File is non-empty ('%s')", cert_file.c_str());
// TLS_CA_Cert_File should be a non-empty path
string ca_file = global_stats.count("TLS_CA_Cert_File") ?
global_stats["TLS_CA_Cert_File"] : "";
ok(!ca_file.empty(),
"stats_proxysql_global: TLS_CA_Cert_File is non-empty ('%s')", ca_file.c_str());
// -----------------------------------------------------------------------
// Part 2: stats_tls_certificates - table structure and row content
// -----------------------------------------------------------------------
diag("Step 2: Verifying content of stats.stats_tls_certificates");
diag("--- Querying stats.stats_tls_certificates ---");
const vector<string> cert_cols = {
"cert_type", "file_path", "subject_cn", "issuer_cn",
"serial_number", "not_before", "not_after",
"days_until_expiry", "sha256_fingerprint", "loaded_at"
};
auto cert_rows = query_rows(admin,
"SELECT cert_type, file_path, subject_cn, issuer_cn, serial_number, "
"not_before, not_after, days_until_expiry, sha256_fingerprint, loaded_at "
"FROM stats.stats_tls_certificates",
cert_cols);
// Should have exactly 2 rows: 'server' and 'ca'
ok(cert_rows.size() == 2,
"stats_tls_certificates: has exactly 2 rows (got %zu)", cert_rows.size());
// Find the 'server' and 'ca' rows
map<string,string> server_row, ca_row;
for (const auto& r : cert_rows) {
if (r.at("cert_type") == "server") server_row = r;
if (r.at("cert_type") == "ca") ca_row = r;
}
diag("Validating cert_type 'server' and 'ca' rows exist...");
ok(!server_row.empty(), "stats_tls_certificates: row with cert_type='server' exists");
ok(!ca_row.empty(), "stats_tls_certificates: row with cert_type='ca' exists");
// Check non-empty required fields for server cert
diag("Validating server certificate row data...");
if (!server_row.empty()) {
ok(!server_row["file_path"].empty(),
"stats_tls_certificates: server file_path is non-empty ('%s')",
server_row["file_path"].c_str());
ok(!server_row["sha256_fingerprint"].empty(),
"stats_tls_certificates: server sha256_fingerprint is non-empty");
int server_days = std::stoi(server_row["days_until_expiry"]);
ok(server_days > -MAX_CERT_DAYS && server_days < MAX_CERT_DAYS,
"stats_tls_certificates: server days_until_expiry=%d is reasonable",
server_days);
long long server_loaded_at = std::stoll(server_row["loaded_at"]);
ok(server_loaded_at > 0,
"stats_tls_certificates: server loaded_at=%lld is positive",
server_loaded_at);
} else {
ok(false, "stats_tls_certificates: server file_path is non-empty (row missing)");
ok(false, "stats_tls_certificates: server sha256_fingerprint is non-empty (row missing)");
ok(false, "stats_tls_certificates: server days_until_expiry is reasonable (row missing)");
ok(false, "stats_tls_certificates: server loaded_at is positive (row missing)");
}
// Check non-empty required fields for CA cert
diag("Validating CA certificate row data...");
if (!ca_row.empty()) {
ok(!ca_row["file_path"].empty(),
"stats_tls_certificates: ca file_path is non-empty ('%s')",
ca_row["file_path"].c_str());
ok(!ca_row["sha256_fingerprint"].empty(),
"stats_tls_certificates: ca sha256_fingerprint is non-empty");
int ca_days = std::stoi(ca_row["days_until_expiry"]);
ok(ca_days > -MAX_CERT_DAYS && ca_days < MAX_CERT_DAYS,
"stats_tls_certificates: ca days_until_expiry=%d is reasonable",
ca_days);
long long ca_loaded_at = std::stoll(ca_row["loaded_at"]);
ok(ca_loaded_at > 0,
"stats_tls_certificates: ca loaded_at=%lld is positive",
ca_loaded_at);
} else {
ok(false, "stats_tls_certificates: ca file_path is non-empty (row missing)");
ok(false, "stats_tls_certificates: ca sha256_fingerprint is non-empty (row missing)");
ok(false, "stats_tls_certificates: ca days_until_expiry is reasonable (row missing)");
ok(false, "stats_tls_certificates: ca loaded_at is positive (row missing)");
}
// -----------------------------------------------------------------------
// Part 3: PROXYSQL RELOAD TLS increments TLS_Load_Count and updates timestamp
// -----------------------------------------------------------------------
diag("Step 3: Verifying PROXYSQL RELOAD TLS updates stats");
diag("--- Executing PROXYSQL RELOAD TLS ---");
// Sleep 1 second to guarantee timestamp changes on fast systems
sleep(1);
if (mysql_query(admin, "PROXYSQL RELOAD TLS")) {
diag("PROXYSQL RELOAD TLS failed: %s", mysql_error(admin));
}
mysql_free_result(mysql_store_result(admin));
diag("Verifying updated stats in stats.stats_proxysql_global after reload...");
auto global_stats_after = query_key_value(admin,
"SELECT Variable_Name, Variable_Value FROM stats.stats_proxysql_global");
int tls_load_count_after = 0;
if (global_stats_after.count("TLS_Load_Count"))
tls_load_count_after = std::stoi(global_stats_after["TLS_Load_Count"]);
ok(tls_load_count_after == tls_load_count + 1,
"stats_proxysql_global: TLS_Load_Count incremented after RELOAD TLS (%d -> %d)",
tls_load_count, tls_load_count_after);
long long tls_ts_after = 0;
if (global_stats_after.count("TLS_Last_Load_Timestamp"))
tls_ts_after = std::stoll(global_stats_after["TLS_Last_Load_Timestamp"]);
ok(tls_ts_after > tls_ts,
"stats_proxysql_global: TLS_Last_Load_Timestamp increased after RELOAD TLS (%lld -> %lld)",
tls_ts, tls_ts_after);
string tls_result_after = global_stats_after.count("TLS_Last_Load_Result") ?
global_stats_after["TLS_Last_Load_Result"] : "";
ok(tls_result_after == "SUCCESS",
"stats_proxysql_global: TLS_Last_Load_Result='SUCCESS' after RELOAD TLS (got '%s')",
tls_result_after.c_str());
diag("Verifying stats.stats_tls_certificates rows after reload...");
// stats_tls_certificates rows still present after reload
auto cert_rows_after = query_rows(admin,
"SELECT cert_type, file_path, sha256_fingerprint FROM stats.stats_tls_certificates",
{"cert_type", "file_path", "sha256_fingerprint"});
ok(cert_rows_after.size() == 2,
"stats_tls_certificates: still has 2 rows after RELOAD TLS (got %zu)",
cert_rows_after.size());
// -----------------------------------------------------------------------
// Part 4: TLS variables must NOT appear in stats_mysql_global
// -----------------------------------------------------------------------
diag("Step 4: Verifying TLS variables are absent from stats.stats_mysql_global");
diag("--- Querying stats.stats_mysql_global ---");
auto mysql_global_stats = query_key_value(admin,
"SELECT Variable_Name, Variable_Value FROM stats.stats_mysql_global");
bool tls_vars_in_mysql_global = false;
for (const auto& var : expected_tls_vars) {
if (mysql_global_stats.count(var)) {
diag("UNEXPECTED: TLS variable '%s' found in stats_mysql_global", var.c_str());
tls_vars_in_mysql_global = true;
}
}
ok(!tls_vars_in_mysql_global,
"TLS variables are NOT present in stats_mysql_global");
diag("Test completed successfully, closing connection.");
mysql_close(admin);
return exit_status();
}
Loading…
Cancel
Save