From d1e51a2cf83b0db795df7eb3e1d582ed3c5c3238 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:22:43 +0000 Subject: [PATCH 1/9] Initial plan From 4f88740b205b451c6d68b312fbfdd93c19c0d644 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:57:46 +0000 Subject: [PATCH 2/9] feat: add stats_tls_certificates table and TLS tracking metrics - Add STATS_SQLITE_TABLE_TLS_CERTIFICATES definition with cert_type, file_path, subject_cn, issuer_cn, serial_number, not_before, not_after, days_until_expiry, sha256_fingerprint, and loaded_at columns - Add TLS tracking fields to GloVars.global: tls_load_count, tls_last_load_timestamp, tls_last_load_ok, tls_cert_file, tls_ca_file, tls_key_file - Update ProxySQL_create_or_load_TLS() to populate tracking fields on success - Register stats_tls_certificates table in Admin_Bootstrap - Add stats___tls_certificates() function that reads certs from disk at query time, extracts metadata (CN, serial, validity dates, SHA-256 fingerprint, days_until_expiry) and populates the table - Add TLS variables to stats___mysql_global(): TLS_Load_Count, TLS_Last_Load_Timestamp, TLS_Last_Load_Result, TLS_Server_Cert_File, TLS_CA_Cert_File, TLS_Key_File - Wire up query detection and refresh in GenericRefreshStatistics() Co-authored-by: renecannao <3645227+renecannao@users.noreply.github.com> --- include/ProxySQL_Admin_Tables_Definitions.h | 2 + include/proxysql_admin.h | 1 + include/proxysql_glovars.hpp | 6 + lib/Admin_Bootstrap.cpp | 1 + lib/ProxySQL_Admin.cpp | 6 + lib/ProxySQL_Admin_Stats.cpp | 176 ++++++++++++++++++++ lib/ProxySQL_GloVars.cpp | 6 + src/proxy_tls.cpp | 12 ++ 8 files changed, 210 insertions(+) diff --git a/include/ProxySQL_Admin_Tables_Definitions.h b/include/ProxySQL_Admin_Tables_Definitions.h index 7214f3dcd..78f4bae0b 100644 --- a/include/ProxySQL_Admin_Tables_Definitions.h +++ b/include/ProxySQL_Admin_Tables_Definitions.h @@ -188,6 +188,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) )" diff --git a/include/proxysql_admin.h b/include/proxysql_admin.h index 42e371533..195c49571 100644 --- a/include/proxysql_admin.h +++ b/include/proxysql_admin.h @@ -808,6 +808,7 @@ 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(); #ifdef PROXYSQLGENAI void stats___mcp_query_tools_counters(bool reset); diff --git a/include/proxysql_glovars.hpp b/include/proxysql_glovars.hpp index 86d038af6..7624368a1 100644 --- a/include/proxysql_glovars.hpp +++ b/include/proxysql_glovars.hpp @@ -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; diff --git a/lib/Admin_Bootstrap.cpp b/lib/Admin_Bootstrap.cpp index cf3135dfe..0266c00cb 100644 --- a/lib/Admin_Bootstrap.cpp +++ b/lib/Admin_Bootstrap.cpp @@ -893,6 +893,7 @@ 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_pgsql_global", STATS_SQLITE_TABLE_PGSQL_GLOBAL); insert_into_tables_defs(tables_defs_stats,"stats_pgsql_connection_pool", STATS_SQLITE_TABLE_PGSQL_CONNECTION_POOL); diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index e89bb6175..08415d2ff 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -1262,6 +1262,7 @@ 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 dump_global_variables=false; bool runtime_scheduler=false; @@ -1446,6 +1447,8 @@ 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_servers_checksums")) { stats_proxysql_servers_checksums = true; refresh = true; } if (strstr(query_no_space,"stats_proxysql_servers_metrics")) @@ -1705,6 +1708,9 @@ 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(); + } #ifdef PROXYSQLGENAI if (stats_mcp_query_tools_counters) { stats___mcp_query_tools_counters(false); diff --git a/lib/ProxySQL_Admin_Stats.cpp b/lib/ProxySQL_Admin_Stats.cpp index 2f6a6e4a2..7a4a69800 100644 --- a/lib/ProxySQL_Admin_Stats.cpp +++ b/lib/ProxySQL_Admin_Stats.cpp @@ -25,6 +25,7 @@ #include "Query_Tool_Handler.h" #include "RAG_Tool_Handler.h" #endif /* PROXYSQLGENAI */ +#include #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,6 +628,17 @@ 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()); + { + std::lock_guard 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 : ""); + } + if (GloMyLogger != nullptr) { const string prefix = "MySQL_Logger_"; @@ -2841,3 +2865,155 @@ 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[256] = {}; + 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; + ASN1_TIME_diff(&pday, &psec, NULL, X509_get0_notAfter(cert)); + } + 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 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(); + + // Process server certificate + if (cert_file) { + BIO *bio = BIO_new_file(cert_file, "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, "server", cert_file, cert, loaded_at); + X509_free(cert); + } + } + free(cert_file); + } + + // Process CA certificate + if (ca_file) { + BIO *bio = BIO_new_file(ca_file, "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, "ca", ca_file, cert, loaded_at); + X509_free(cert); + } + } + free(ca_file); + } + + statsdb->execute("COMMIT"); +} diff --git a/lib/ProxySQL_GloVars.cpp b/lib/ProxySQL_GloVars.cpp index 8a5eb8b92..60b9ee919 100644 --- a/lib/ProxySQL_GloVars.cpp +++ b/lib/ProxySQL_GloVars.cpp @@ -246,6 +246,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]"; diff --git a/src/proxy_tls.cpp b/src/proxy_tls.cpp index 4f79eba07..e278b409a 100644 --- a/src/proxy_tls.cpp +++ b/src/proxy_tls.cpp @@ -406,6 +406,14 @@ 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 (no lock needed during bootstrap) + GloVars.global.tls_key_file = ssl_key_fp; + GloVars.global.tls_cert_file = ssl_cert_fp; + GloVars.global.tls_ca_file = ssl_ca_fp; + 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 +433,10 @@ 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) + 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"); From 0357862590a308e58751edeaf7ce29a71e74105c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:02:51 +0000 Subject: [PATCH 3/9] fix: address code review feedback - use strdup for file paths, check ASN1_TIME_diff return, refactor dup cert loading, larger CN buffer Co-authored-by: renecannao <3645227+renecannao@users.noreply.github.com> --- lib/ProxySQL_Admin_Stats.cpp | 34 +++++++++++++--------------------- src/proxy_tls.cpp | 9 ++++++--- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/ProxySQL_Admin_Stats.cpp b/lib/ProxySQL_Admin_Stats.cpp index 7a4a69800..f5ef86632 100644 --- a/lib/ProxySQL_Admin_Stats.cpp +++ b/lib/ProxySQL_Admin_Stats.cpp @@ -2879,7 +2879,7 @@ static std::string asn1_time_to_iso8601(const ASN1_TIME *asn1t) { // Helper: get CN from X509_NAME static std::string x509_name_cn(X509_NAME *name) { if (!name) return ""; - char buf[256] = {}; + char buf[1024] = {}; if (X509_NAME_get_text_by_NID(name, NID_commonName, buf, sizeof(buf)) < 0) return ""; return std::string(buf); @@ -2931,7 +2931,9 @@ static void insert_tls_cert_row( int pday = 0; { int psec = 0; - ASN1_TIME_diff(&pday, &psec, NULL, X509_get0_notAfter(cert)); + 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; @@ -2987,33 +2989,23 @@ void ProxySQL_Admin::stats___tls_certificates() { ASSERT_SQLITE_OK(rc, statsdb); sqlite3_stmt *stmt = u_stmt.get(); - // Process server certificate - if (cert_file) { - BIO *bio = BIO_new_file(cert_file, "r"); + // 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, "server", cert_file, cert, loaded_at); + insert_tls_cert_row(statsdb, stmt, cert_type, file_path, cert, loaded_at); X509_free(cert); } } - free(cert_file); - } + free(file_path); + }; - // Process CA certificate - if (ca_file) { - BIO *bio = BIO_new_file(ca_file, "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, "ca", ca_file, cert, loaded_at); - X509_free(cert); - } - } - free(ca_file); - } + process_cert("server", cert_file); + process_cert("ca", ca_file); statsdb->execute("COMMIT"); } diff --git a/src/proxy_tls.cpp b/src/proxy_tls.cpp index e278b409a..a1c9b1d74 100644 --- a/src/proxy_tls.cpp +++ b/src/proxy_tls.cpp @@ -408,9 +408,12 @@ int ProxySQL_create_or_load_TLS(bool bootstrap, std::string& msg) { SSL_CTX_set_session_cache_mode(GloVars.global.ssl_ctx, SSL_SESS_CACHE_OFF); // Store TLS file paths and tracking info for stats table (no lock needed during bootstrap) - GloVars.global.tls_key_file = ssl_key_fp; - GloVars.global.tls_cert_file = ssl_cert_fp; - GloVars.global.tls_ca_file = ssl_ca_fp; + 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; From e27486285743be6bb6a85e1eee5789e0e0f7b686 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:40:42 +0000 Subject: [PATCH 4/9] feat: introduce stats_global table and move TLS metrics out of stats_mysql_global; add TAP tests - Add STATS_SQLITE_TABLE_GLOBAL definition (stats_global table for non-MySQL/PgSQL metrics) - Register stats_global in Admin_Bootstrap.cpp - Add stats___global() implementation and declaration; document with Doxygen comment - Remove TLS_* variables from stats___mysql_global() - they were misplaced there - Move all TLS tracking metrics to stats___global() under ssl_mutex - Wire up stats_global query detection and refresh in GenericRefreshStatistics() - Add TAP test test_tls_stats-t.cpp: - Verifies stats_global contains all 6 TLS tracking variables - Checks value ranges and validity of each TLS variable - Verifies stats_tls_certificates has 2 rows (server + ca) with correct fields - Verifies TLS_Load_Count increments and TLS_Last_Load_Timestamp increases after PROXYSQL RELOAD TLS - Confirms TLS variables are absent from stats_mysql_global Co-authored-by: renecannao <3645227+renecannao@users.noreply.github.com> --- include/ProxySQL_Admin_Tables_Definitions.h | 7 + include/proxysql_admin.h | 1 + lib/Admin_Bootstrap.cpp | 1 + lib/ProxySQL_Admin.cpp | 6 + lib/ProxySQL_Admin_Stats.cpp | 54 +++- test/tap/tests/test_tls_stats-t.cpp | 328 ++++++++++++++++++++ 6 files changed, 385 insertions(+), 12 deletions(-) create mode 100644 test/tap/tests/test_tls_stats-t.cpp diff --git a/include/ProxySQL_Admin_Tables_Definitions.h b/include/ProxySQL_Admin_Tables_Definitions.h index 78f4bae0b..736eec556 100644 --- a/include/ProxySQL_Admin_Tables_Definitions.h +++ b/include/ProxySQL_Admin_Tables_Definitions.h @@ -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_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)" diff --git a/include/proxysql_admin.h b/include/proxysql_admin.h index 195c49571..aea399bdb 100644 --- a/include/proxysql_admin.h +++ b/include/proxysql_admin.h @@ -809,6 +809,7 @@ class ProxySQL_Admin { void stats___mysql_gtid_executed(); void stats___mysql_client_host_cache(bool reset); void stats___tls_certificates(); + void stats___global(); #ifdef PROXYSQLGENAI void stats___mcp_query_tools_counters(bool reset); diff --git a/lib/Admin_Bootstrap.cpp b/lib/Admin_Bootstrap.cpp index 0266c00cb..be98faf36 100644 --- a/lib/Admin_Bootstrap.cpp +++ b/lib/Admin_Bootstrap.cpp @@ -894,6 +894,7 @@ bool ProxySQL_Admin::init(const bootstrap_info_t& bootstrap_info) { 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_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); diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index 08415d2ff..3d5c702be 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -1263,6 +1263,7 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign bool stats_pgsql_client_host_cache = false; bool stats_pgsql_client_host_cache_reset = false; bool stats_tls_certificates=false; + bool stats_global=false; bool dump_global_variables=false; bool runtime_scheduler=false; @@ -1449,6 +1450,8 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign { 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_global")) + { stats_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")) @@ -1711,6 +1714,9 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign if (stats_tls_certificates) { stats___tls_certificates(); } + if (stats_global) { + stats___global(); + } #ifdef PROXYSQLGENAI if (stats_mcp_query_tools_counters) { stats___mcp_query_tools_counters(false); diff --git a/lib/ProxySQL_Admin_Stats.cpp b/lib/ProxySQL_Admin_Stats.cpp index f5ef86632..3f4c9f46b 100644 --- a/lib/ProxySQL_Admin_Stats.cpp +++ b/lib/ProxySQL_Admin_Stats.cpp @@ -628,18 +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()); - { - std::lock_guard 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 : ""); - } - - if (GloMyLogger != nullptr) { const string prefix = "MySQL_Logger_"; std::unordered_map metrics = GloMyLogger->getAllMetrics(); @@ -828,6 +816,48 @@ void ProxySQL_Admin::stats___pgsql_global() { statsdb->execute("COMMIT"); } +/** + * @brief Populates the `stats_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_global table + * is accessed (e.g. "SELECT * FROM stats.stats_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___global() { + statsdb->execute("BEGIN"); + statsdb->execute("DELETE FROM stats_global"); + + const string q_row_insert { "INSERT INTO stats_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 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; diff --git a/test/tap/tests/test_tls_stats-t.cpp b/test/tap/tests/test_tls_stats-t.cpp new file mode 100644 index 000000000..d53c7fe5d --- /dev/null +++ b/test/tap/tests/test_tls_stats-t.cpp @@ -0,0 +1,328 @@ +/** + * @file test_tls_stats-t.cpp + * @brief TAP test for stats_tls_certificates and stats_global TLS metrics. + * + * @details This test verifies: + * 1. stats_global table exists and is queryable from the stats schema. + * 2. stats_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_global increments by 1. + * - stats_tls_certificates rows are still present and non-empty. + * 13. TLS_Last_Load_Timestamp in stats_global increases after PROXYSQL RELOAD TLS. + * 14. TLS-related variables are NOT present in stats_mysql_global. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#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 where key=col[0] and value=col[1]. + */ +static map query_key_value(MYSQL* conn, const char* query) { + map 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> query_rows(MYSQL* conn, const char* query, const vector& col_names) { + vector> 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 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; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return exit_status(); + } + + // Tests: + // 1-5: stats_global TLS variables present + // 6-9: stats_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); + + 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_global - TLS tracking variables + // ----------------------------------------------------------------------- + diag("--- Testing stats.stats_global TLS tracking variables ---"); + + auto global_stats = query_key_value(admin, + "SELECT Variable_Name, Variable_Value FROM stats.stats_global"); + + const vector 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_global: Variable '%s' is present", var.c_str()); + } + + // 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_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_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_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_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_global: TLS_CA_Cert_File is non-empty ('%s')", ca_file.c_str()); + + // ----------------------------------------------------------------------- + // Part 2: stats_tls_certificates - table structure and row content + // ----------------------------------------------------------------------- + diag("--- Testing stats.stats_tls_certificates ---"); + + const vector 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 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; + } + + 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 + 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 + 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("--- Testing PROXYSQL RELOAD TLS updates stats ---"); + + // 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)); + + auto global_stats_after = query_key_value(admin, + "SELECT Variable_Name, Variable_Value FROM stats.stats_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_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_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_global: TLS_Last_Load_Result='SUCCESS' after RELOAD TLS (got '%s')", + tls_result_after.c_str()); + + // 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("--- Verifying TLS variables are absent from 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"); + + mysql_close(admin); + return exit_status(); +} From 2e552b8ae8c271bdc9ee7473f27dd2cbc215b207 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Fri, 20 Mar 2026 23:50:30 +0000 Subject: [PATCH 5/9] fix(tls): improve SSL/TLS certificate tracking and fix memory leaks - lib/ProxySQL_GloVars.cpp: Free tls_cert_file, tls_ca_file, and tls_key_file in ProxySQL_GlobalVariables destructor. - src/proxy_tls.cpp: Added std::lock_guard during initial bootstrap of TLS variables to prevent race conditions. - src/proxy_tls.cpp: Ensure TLS file paths are updated in GloVars during PROXYSQL RELOAD TLS to keep stats table consistent with runtime configuration. --- lib/ProxySQL_GloVars.cpp | 12 ++++++++++++ src/proxy_tls.cpp | 29 +++++++++++++++++++---------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/ProxySQL_GloVars.cpp b/lib/ProxySQL_GloVars.cpp index 60b9ee919..e6f271246 100644 --- a/lib/ProxySQL_GloVars.cpp +++ b/lib/ProxySQL_GloVars.cpp @@ -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() : diff --git a/src/proxy_tls.cpp b/src/proxy_tls.cpp index a1c9b1d74..56a3e9bd0 100644 --- a/src/proxy_tls.cpp +++ b/src/proxy_tls.cpp @@ -407,16 +407,19 @@ int ProxySQL_create_or_load_TLS(bool bootstrap, std::string& msg) { 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 (no lock needed during bootstrap) - 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; + // Store TLS file paths and tracking info for stats table + { + std::lock_guard 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 @@ -437,6 +440,12 @@ int ProxySQL_create_or_load_TLS(bool bootstrap, std::string& msg) { 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; From 730f77e64ef963b99d51493db3553e7cb527bac9 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 21 Mar 2026 00:59:17 +0000 Subject: [PATCH 6/9] test: add summary and step-by-step reporting to TLS stats TAP test --- test/tap/tests/test_tls_stats-t.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/test/tap/tests/test_tls_stats-t.cpp b/test/tap/tests/test_tls_stats-t.cpp index d53c7fe5d..5603b2c21 100644 --- a/test/tap/tests/test_tls_stats-t.cpp +++ b/test/tap/tests/test_tls_stats-t.cpp @@ -105,6 +105,10 @@ static vector> query_rows(MYSQL* conn, const char* query, con 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_global, and updates these stats after a reload."); + if (cl.getEnv()) { diag("Failed to get the required environmental variables."); return exit_status(); @@ -120,6 +124,7 @@ int main(int argc, char** argv) { // 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)); @@ -134,7 +139,8 @@ int main(int argc, char** argv) { // ----------------------------------------------------------------------- // Part 1: stats_global - TLS tracking variables // ----------------------------------------------------------------------- - diag("--- Testing stats.stats_global TLS tracking variables ---"); + diag("Step 1: Verifying TLS tracking variables in stats.stats_global"); + diag("--- Querying stats.stats_global ---"); auto global_stats = query_key_value(admin, "SELECT Variable_Name, Variable_Value FROM stats.stats_global"); @@ -153,6 +159,7 @@ int main(int argc, char** argv) { "stats_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")) @@ -189,7 +196,8 @@ int main(int argc, char** argv) { // ----------------------------------------------------------------------- // Part 2: stats_tls_certificates - table structure and row content // ----------------------------------------------------------------------- - diag("--- Testing stats.stats_tls_certificates ---"); + diag("Step 2: Verifying content of stats.stats_tls_certificates"); + diag("--- Querying stats.stats_tls_certificates ---"); const vector cert_cols = { "cert_type", "file_path", "subject_cn", "issuer_cn", @@ -214,10 +222,12 @@ int main(int argc, char** argv) { 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')", @@ -240,6 +250,7 @@ int main(int argc, char** argv) { } // 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')", @@ -264,7 +275,8 @@ int main(int argc, char** argv) { // ----------------------------------------------------------------------- // Part 3: PROXYSQL RELOAD TLS increments TLS_Load_Count and updates timestamp // ----------------------------------------------------------------------- - diag("--- Testing PROXYSQL RELOAD TLS updates stats ---"); + 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); @@ -274,6 +286,7 @@ int main(int argc, char** argv) { } mysql_free_result(mysql_store_result(admin)); + diag("Verifying updated stats in stats.stats_global after reload..."); auto global_stats_after = query_key_value(admin, "SELECT Variable_Name, Variable_Value FROM stats.stats_global"); @@ -297,6 +310,7 @@ int main(int argc, char** argv) { "stats_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", @@ -308,7 +322,8 @@ int main(int argc, char** argv) { // ----------------------------------------------------------------------- // Part 4: TLS variables must NOT appear in stats_mysql_global // ----------------------------------------------------------------------- - diag("--- Verifying TLS variables are absent from 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"); @@ -323,6 +338,7 @@ int main(int argc, char** argv) { 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(); } From 9850d9daebdb2cf2d06c4df0bb6d0eb271ccf3dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Canna=C3=B2?= Date: Sun, 22 Mar 2026 16:11:15 +0100 Subject: [PATCH 7/9] Fix stats_global substring collision and reload failure tracking 1. stats_global strstr match: add exclusion guards for stats_mysql_global and stats_pgsql_global to prevent spurious stats___global() calls and unnecessary ssl_mutex contention on every MySQL/PgSQL global stats query. 2. TLS reload failure tracking: set tls_last_load_ok=false and update tls_load_count/tls_last_load_timestamp on non-bootstrap reload failures. Previously, a failed PROXYSQL RELOAD TLS would leave stale SUCCESS status in stats_global. --- lib/ProxySQL_Admin.cpp | 2 +- src/proxy_tls.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index 3d5c702be..7f9ae9fa6 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -1450,7 +1450,7 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign { 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_global")) + if (strstr(query_no_space,"stats_global") && !strstr(query_no_space,"stats_mysql_global") && !strstr(query_no_space,"stats_pgsql_global")) { stats_global=true; refresh=true; } if (strstr(query_no_space,"stats_proxysql_servers_checksums")) { stats_proxysql_servers_checksums = true; refresh = true; } diff --git a/src/proxy_tls.cpp b/src/proxy_tls.cpp index 56a3e9bd0..38acc553f 100644 --- a/src/proxy_tls.cpp +++ b/src/proxy_tls.cpp @@ -485,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 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); From efedd93c923fe1dc9f80ed4dc4c3c51aa2539db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Canna=C3=B2?= Date: Sun, 22 Mar 2026 16:19:49 +0100 Subject: [PATCH 8/9] Rename stats_global to stats_proxysql_global to avoid strstr substring collision stats_global is a substring of stats_mysql_global and stats_pgsql_global, causing the strstr-based dispatch to match incorrectly. Exclusion guards don't work either because a query referencing both tables would suppress the stats_proxysql_global refresh. Renaming to stats_proxysql_global follows the existing naming convention (stats_mysql_global, stats_pgsql_global) and eliminates the substring collision entirely. --- include/ProxySQL_Admin_Tables_Definitions.h | 2 +- include/proxysql_admin.h | 2 +- lib/Admin_Bootstrap.cpp | 2 +- lib/ProxySQL_Admin.cpp | 10 ++--- lib/ProxySQL_Admin_Stats.cpp | 12 +++--- test/tap/tests/test_tls_stats-t.cpp | 46 ++++++++++----------- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/include/ProxySQL_Admin_Tables_Definitions.h b/include/ProxySQL_Admin_Tables_Definitions.h index 736eec556..30b2105cc 100644 --- a/include/ProxySQL_Admin_Tables_Definitions.h +++ b/include/ProxySQL_Admin_Tables_Definitions.h @@ -183,7 +183,7 @@ * @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_global (Variable_Name VARCHAR NOT NULL PRIMARY KEY , Variable_Value VARCHAR NOT NULL)" +#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)" diff --git a/include/proxysql_admin.h b/include/proxysql_admin.h index aea399bdb..eaf2b67b9 100644 --- a/include/proxysql_admin.h +++ b/include/proxysql_admin.h @@ -809,7 +809,7 @@ class ProxySQL_Admin { void stats___mysql_gtid_executed(); void stats___mysql_client_host_cache(bool reset); void stats___tls_certificates(); - void stats___global(); + void stats___proxysql_global(); #ifdef PROXYSQLGENAI void stats___mcp_query_tools_counters(bool reset); diff --git a/lib/Admin_Bootstrap.cpp b/lib/Admin_Bootstrap.cpp index be98faf36..c22e7075b 100644 --- a/lib/Admin_Bootstrap.cpp +++ b/lib/Admin_Bootstrap.cpp @@ -894,7 +894,7 @@ bool ProxySQL_Admin::init(const bootstrap_info_t& bootstrap_info) { 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_global", STATS_SQLITE_TABLE_GLOBAL); + 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); diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index 7f9ae9fa6..82bccb41b 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -1263,7 +1263,7 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign bool stats_pgsql_client_host_cache = false; bool stats_pgsql_client_host_cache_reset = false; bool stats_tls_certificates=false; - bool stats_global=false; + bool stats_proxysql_global=false; bool dump_global_variables=false; bool runtime_scheduler=false; @@ -1450,8 +1450,8 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign { 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_global") && !strstr(query_no_space,"stats_mysql_global") && !strstr(query_no_space,"stats_pgsql_global")) - { stats_global=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")) @@ -1714,8 +1714,8 @@ bool ProxySQL_Admin::GenericRefreshStatistics(const char *query_no_space, unsign if (stats_tls_certificates) { stats___tls_certificates(); } - if (stats_global) { - stats___global(); + if (stats_proxysql_global) { + stats___proxysql_global(); } #ifdef PROXYSQLGENAI if (stats_mcp_query_tools_counters) { diff --git a/lib/ProxySQL_Admin_Stats.cpp b/lib/ProxySQL_Admin_Stats.cpp index 3f4c9f46b..ee763ae19 100644 --- a/lib/ProxySQL_Admin_Stats.cpp +++ b/lib/ProxySQL_Admin_Stats.cpp @@ -817,11 +817,11 @@ void ProxySQL_Admin::stats___pgsql_global() { } /** - * @brief Populates the `stats_global` table with ProxySQL-wide metrics + * @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_global table - * is accessed (e.g. "SELECT * FROM stats.stats_global"). It deletes all existing + * @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. * @@ -833,11 +833,11 @@ void ProxySQL_Admin::stats___pgsql_global() { * - TLS_CA_Cert_File : Path to the CA certificate file. * - TLS_Key_File : Path to the private key file. */ -void ProxySQL_Admin::stats___global() { +void ProxySQL_Admin::stats___proxysql_global() { statsdb->execute("BEGIN"); - statsdb->execute("DELETE FROM stats_global"); + statsdb->execute("DELETE FROM stats_proxysql_global"); - const string q_row_insert { "INSERT INTO stats_global VALUES (?1, ?2)" }; + 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()); diff --git a/test/tap/tests/test_tls_stats-t.cpp b/test/tap/tests/test_tls_stats-t.cpp index 5603b2c21..68ebe6137 100644 --- a/test/tap/tests/test_tls_stats-t.cpp +++ b/test/tap/tests/test_tls_stats-t.cpp @@ -1,10 +1,10 @@ /** * @file test_tls_stats-t.cpp - * @brief TAP test for stats_tls_certificates and stats_global TLS metrics. + * @brief TAP test for stats_tls_certificates and stats_proxysql_global TLS metrics. * * @details This test verifies: - * 1. stats_global table exists and is queryable from the stats schema. - * 2. stats_global contains the expected TLS tracking variables: + * 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 @@ -22,9 +22,9 @@ * 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_global increments by 1. + * - 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_global increases after PROXYSQL RELOAD TLS. + * 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. */ @@ -107,7 +107,7 @@ int main(int argc, char** argv) { 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_global, and updates these stats after a reload."); + 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."); @@ -115,8 +115,8 @@ int main(int argc, char** argv) { } // Tests: - // 1-5: stats_global TLS variables present - // 6-9: stats_global TLS variable values + // 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' @@ -137,13 +137,13 @@ int main(int argc, char** argv) { } // ----------------------------------------------------------------------- - // Part 1: stats_global - TLS tracking variables + // Part 1: stats_proxysql_global - TLS tracking variables // ----------------------------------------------------------------------- - diag("Step 1: Verifying TLS tracking variables in stats.stats_global"); - diag("--- Querying stats.stats_global ---"); + 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_global"); + "SELECT Variable_Name, Variable_Value FROM stats.stats_proxysql_global"); const vector expected_tls_vars = { "TLS_Load_Count", @@ -156,7 +156,7 @@ int main(int argc, char** argv) { for (const auto& var : expected_tls_vars) { ok(global_stats.count(var) > 0, - "stats_global: Variable '%s' is present", var.c_str()); + "stats_proxysql_global: Variable '%s' is present", var.c_str()); } diag("Verifying values of TLS tracking variables..."); @@ -165,33 +165,33 @@ int main(int argc, char** argv) { if (global_stats.count("TLS_Load_Count")) tls_load_count = std::stoi(global_stats["TLS_Load_Count"]); ok(tls_load_count >= 1, - "stats_global: TLS_Load_Count is >= 1 (was %d)", tls_load_count); + "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_global: TLS_Last_Load_Timestamp is positive (was %lld)", tls_ts); + "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_global: TLS_Last_Load_Result='%s' is valid (NONE/SUCCESS/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_global: TLS_Server_Cert_File is non-empty ('%s')", cert_file.c_str()); + "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_global: TLS_CA_Cert_File is non-empty ('%s')", ca_file.c_str()); + "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 @@ -286,28 +286,28 @@ int main(int argc, char** argv) { } mysql_free_result(mysql_store_result(admin)); - diag("Verifying updated stats in stats.stats_global after reload..."); + 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_global"); + "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_global: TLS_Load_Count incremented after RELOAD TLS (%d -> %d)", + "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_global: TLS_Last_Load_Timestamp increased after RELOAD TLS (%lld -> %lld)", + "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_global: TLS_Last_Load_Result='SUCCESS' after RELOAD TLS (got '%s')", + "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..."); From a616b34b95e2106afb8a543338b9f948cbc87cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Canna=C3=B2?= Date: Sun, 22 Mar 2026 16:39:01 +0100 Subject: [PATCH 9/9] Register test_tls_stats-t in groups.json (g4 groups) --- test/tap/groups/groups.json | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index 3164b077f..069cb6092 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -210,6 +210,7 @@ "test_ssl_large_query-2-t" : [ "legacy-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","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","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","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], "test_unshun_algorithm-t" : [ "legacy-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_unsupported_queries-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ],