Skip to content

feat(pgsql): advanced query logging parity with mysql eventslog#5391

Merged
renecannao merged 15 commits intov3.0from
v3.0_pgsql_advanced_logging
Feb 18, 2026
Merged

feat(pgsql): advanced query logging parity with mysql eventslog#5391
renecannao merged 15 commits intov3.0from
v3.0_pgsql_advanced_logging

Conversation

@renecannao
Copy link
Contributor

@renecannao renecannao commented Feb 18, 2026

Summary

This PR introduces PostgreSQL advanced query logging with parity to MySQL eventslog behavior, including manual dump, scheduler auto-dump, docs, and validation coverage.

Key Changes

  • Add PostgreSQL advanced query event logging pipeline and persistence.
  • Add DUMP PGSQL EVENTSLOG ... command support in the shared Admin module.
  • Ensure command behavior is consistent from both admin endpoints:
    • MySQL protocol on 6032
    • PostgreSQL protocol on 6132
  • Use database field naming for PostgreSQL query events.
  • Use PostgreSQL-native error fields: sqlstate and error.
  • Add scheduler-driven auto-dump behavior and align/fix MySQL scheduler wiring for symmetry.
  • Add TAP end-to-end coverage for:
    • Manual dump flow
    • Automatic dump flow
  • Expand Doxygen coverage for newly introduced interfaces.
  • Update internal PostgreSQL advanced logging architecture documentation.

Validation

  • TAP tests for PostgreSQL query logging manual dump and scheduler auto-dump.
  • Documentation and inline API docs updated with implemented behavior.

Closes #5390

Summary by CodeRabbit

  • New Features

    • PostgreSQL query event logging: configurable in-memory circular buffer, metrics exposure, history tables, auto-dump to memory/disk, admin variable for sync interval, and a DUMP PGSQL EVENTSLOG admin command.
  • Documentation

    • Added comprehensive PostgreSQL advanced query logging architecture document.
  • Tests

    • Added TAP tests covering autodump and memory/disk logging scenarios.
  • Chores

    • Updated ignore list and build clean to omit generated parser artifacts.

Context:
- Bison/Flex-generated parser files in `lib/` can be created locally during parser work.
- These files are generated artifacts and should not be tracked.

Changes:
- Add generated parser artifacts to `.gitignore`:
  - `lib/MySQL_Lexer.yy.c`
  - `lib/MySQL_Parser.output`
  - `lib/MySQL_Parser.tab.c`
  - `lib/MySQL_Parser.tab.h`
- Extend `lib/Makefile` `clean` target to remove the same generated files.

Why:
- Prevent accidental inclusion of regenerated parser outputs in feature commits.
- Keep worktrees clean and make `make clean` deterministic for parser-related changes.
…k sync

Context:
- PostgreSQL advanced query logging already had end-to-end coverage for manual dump flows.
- Scheduler-driven automatic dump path also needs explicit TAP coverage.

Test added:
- `test/tap/tests/pgsql_query_logging_autodump-t.cpp`
  - Connects to PGSQL admin and frontend interfaces.
  - Enables query logging and admin scheduler variable:
    `admin-stats_pgsql_eventslog_sync_buffer_to_disk=1`
  - Generates frontend workload (`SELECT 1` loop).
  - Polls `history_pgsql_query_events` to assert rows are flushed to disk by scheduler.
  - Validates successful rows are present (`sqlstate IS NULL`).
  - Restores scheduler variable to `0` during cleanup.

Harness wiring:
- Register `pgsql_query_logging_autodump-t` in `test/tap/groups/groups.json` so it runs with default PGSQL test group settings.

Why:
- Close e2e coverage gap between manual dump and auto-dump behavior for PGSQL events logging.
- Catch regressions in scheduler-based buffer synchronization path.
…anced logging components

Context:
- The PostgreSQL advanced query logging implementation added new event, buffer, and metrics APIs.
- Several newly introduced functions lacked Doxygen coverage.

Changes:
- Add Doxygen documentation blocks for newly introduced logging types and APIs in `include/PgSQL_Logger.hpp`:
  - metric enums and metric-map indexes
  - `PGSQL_LOG_EVENT_TYPE`
  - `PgSQL_Event` class and its new/updated lifecycle + serialization methods
  - `PgSQL_Logger_CircularBuffer` constructor/destructor and buffer operations
  - `PgSQL_Logger` constructor/destructor

Why:
- Improve maintainability and code navigation for the new PostgreSQL advanced logging feature set.
- Align implementation with project expectation that introduced features include inline Doxygen documentation.
… section with auto-dump E2E coverage

Context:
- The architecture document was rewritten as an as-built reference, but latest test coverage additions were not fully reflected.

Changes:
- Update validation mapping in `doc/internal/pgsql_advanced_query_logging_architecture.md` to include:
  - `pgsql_query_logging_autodump-t` TAP test
  - scheduler-driven auto-dump acceptance coverage
  - both relevant TAP group registrations

Why:
- Keep internal architecture/validation documentation aligned with implemented and wired test coverage.
- Avoid stale validation references when reviewing branch readiness.
…ross admin ports

Context:
- Admin commands are exposed by one shared Admin module over two protocols:
  - MySQL admin endpoint (`6032`)
  - PostgreSQL admin endpoint (`6132`)
- `DUMP PGSQL EVENTSLOG ...` must behave identically regardless of which admin protocol executes the command.

Issue fixed:
- PGSQL events dump path used thread-local PGSQL sizing variables when draining to memory (`stats_pgsql_query_events`).
- When command execution happened from a non-PGSQL protocol admin worker, TLS values could be out of sync for PGSQL sizing decisions.

Changes:
- In `lib/PgSQL_Logger.cpp`, add runtime-size helper functions that read canonical values from `GloPTH->variables` with TLS fallback:
  - `eventslog_buffer_max_query_length`
  - `eventslog_table_memory_size`
- Use these helpers in:
  - `PgSQL_Event` copy constructor (buffered query truncation)
  - `PgSQL_Logger::processEvents()` (in-memory retention for `stats_pgsql_query_events`)
- Update internal architecture doc to explicitly state command availability from both admin protocol endpoints.

Why:
- Ensure `DUMP PGSQL EVENTSLOG ...` semantics remain consistent across `6032` and `6132`, matching the shared-admin-module design.
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Implements PostgreSQL advanced query logging: in-memory circular buffer, batched persistence to memory/disk SQLite tables, Prometheus-style metrics, scheduler-driven auto-dumps, admin command DUMP PGSQL EVENTSLOG, runtime/thread variables, TAP tests, docs, and build/ignore updates.

Changes

Cohort / File(s) Summary
Logger core & API
include/PgSQL_Logger.hpp, lib/PgSQL_Logger.cpp, lib/PgSQL_Logger.cpp
Adds PgSQL_Event fields/methods, PgSQL_Logger_CircularBuffer, circular-buffered emit path, bulk DB insert helpers, processEvents(), metrics collection, Prometheus metric arrays, and runtime helpers.
Thread/runtime variables
include/PgSQL_Thread.h, include/proxysql_structs.h, lib/PgSQL_Thread.cpp
Introduces three new runtime/thread-local variables for eventslog: buffer history size, table memory size, max query length; defaults, ranges, refresh wiring, and propagation to runtime structures.
Admin & scheduling plumbing
include/proxysql_admin.h, lib/ProxySQL_Admin.cpp, lib/Admin_Handler.cpp, lib/Admin_Bootstrap.cpp, lib/ProxySQL_Admin_Stats.cpp
Adds stats_pgsql_eventslog_sync_buffer_to_disk admin variable, DUMP PGSQL EVENTSLOG command handling (FROM BUFFER TO MEMORY/TO DISK/TO BOTH), auto-dump scheduler wiring, metrics emission, and stats table registration.
Statistics & table defs
include/ProxySQL_Statistics.hpp, include/ProxySQL_Admin_Tables_Definitions.h, lib/ProxySQL_Statistics.cpp
Declares stats_pgsql_query_events and history_pgsql_query_events table macros/indexes, adds last-timer variable and PgSQL dump-timetoget helper to drive periodic dumps.
Admin runtime loop & bootstrap
lib/ProxySQL_Admin.cpp, lib/Admin_Bootstrap.cpp
Hooks new variables and metrics into admin main loop, vacuum list, automatic dumping, and module metrics updates.
Tests & test groups
test/tap/groups/groups.json, test/tap/tests/pgsql_query_logging_autodump-t.cpp, test/tap/tests/pgsql_query_logging_memory-t.cpp
Adds two TAP tests and group entries validating auto-dump and memory/history behaviors, schema and sqlstate/error capture, and runtime interactions.
Docs, build, ignore rules
doc/internal/pgsql_advanced_query_logging_architecture.md, .gitignore, lib/Makefile
New architecture doc; .gitignore updated to ignore generated MySQL parser artifacts; lib/Makefile clean target updated to remove generated parser files.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/Connection
    participant PgLogger as PgSQL_Logger
    participant CircBuf as PgSQL_Logger_CircularBuffer
    participant MemDB as Stats Memory DB
    participant DiskDB as Stats Disk DB
    participant Scheduler as Scheduler/Timer

    Client->>PgLogger: log_request(event)
    activate PgLogger
    PgLogger->>CircBuf: insert(PgSQL_Event)
    activate CircBuf
    CircBuf-->>CircBuf: bounded enqueue / drop oldest
    CircBuf-->>PgLogger: buffered
    deactivate CircBuf
    PgLogger-->>Client: return

    Scheduler->>PgLogger: processEvents(statsdb, statsdb_disk)
    activate PgLogger
    PgLogger->>CircBuf: get_all_events()
    activate CircBuf
    CircBuf-->>PgLogger: vector<PgSQL_Event*>
    deactivate CircBuf
    PgLogger->>MemDB: insertPgSQLEventsIntoDb(stats_pgsql_query_events)
    PgLogger->>DiskDB: insertPgSQLEventsIntoDb(history_pgsql_query_events)
    PgLogger->>PgLogger: p_update_metrics()
    PgLogger-->>Scheduler: done
    deactivate PgLogger
Loading
sequenceDiagram
    participant Admin as Admin Client
    participant Handler as Admin_Handler
    participant PgLogger as PgSQL_Logger
    participant MemDB as Stats Memory DB
    participant DiskDB as Stats Disk DB

    Admin->>Handler: DUMP PGSQL EVENTSLOG FROM BUFFER TO BOTH
    activate Handler
    Handler->>PgLogger: processEvents(statsdb, statsdb_disk)
    activate PgLogger
    PgLogger->>PgLogger: get_all_events()
    PgLogger->>MemDB: INSERT stats_pgsql_query_events
    PgLogger->>DiskDB: INSERT history_pgsql_query_events
    PgLogger->>PgLogger: update metrics
    PgLogger-->>Handler: row_count
    Handler-->>Admin: OK row_count
    deactivate PgLogger
    deactivate Handler
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
I hopped through buffers, light and spry,
Counted queries as they scurried by;
From RAM to disk I nudged the flow,
With metrics bright and tidy row —
Hooray, the logs now bloom and grow!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(pgsql): advanced query logging parity with mysql eventslog' clearly and specifically summarizes the main objective of the changeset—implementing PostgreSQL advanced query logging to match MySQL behavior.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #5390: PostgreSQL query-event logging pipeline with persistence tables, DUMP PGSQL EVENTSLOG admin command, scheduler auto-dump support, database/sqlstate/error fields, and TAP test coverage for manual and automatic dump flows.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the PR objectives from #5390. File modifications add PostgreSQL eventslog infrastructure (headers, tables, metrics, handlers), logging implementation (PgSQL_Logger.cpp), admin integration, documentation, and TAP tests without unrelated or extraneous changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v3.0_pgsql_advanced_logging

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @renecannao, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances PostgreSQL query observability by introducing an advanced logging system that achieves feature parity with the existing MySQL eventslog. It provides mechanisms for capturing, buffering, and persisting PostgreSQL query events, including detailed error information, to both in-memory and on-disk SQLite tables. The changes enable both manual and scheduled automatic dumping of these events, along with new configuration options and Prometheus metrics for monitoring. This upgrade allows users to gain deeper insights into PostgreSQL query performance and failures within the ProxySQL environment.

Highlights

  • Advanced PostgreSQL Query Logging: Introduced a comprehensive advanced query logging pipeline for PostgreSQL, mirroring the functionality of MySQL's eventslog. This includes capturing query events, in-memory circular buffering, and persistence to SQLite tables.
  • Manual and Automatic Event Dumping: Added new DUMP PGSQL EVENTSLOG commands to manually flush buffered PostgreSQL events to memory (stats_pgsql_query_events) and/or disk (history_pgsql_query_events). An automatic scheduler-driven dump mechanism was also implemented for periodic persistence to disk.
  • Enhanced Error Reporting: PostgreSQL query events now capture native error fields, including sqlstate and a textual error message, providing more detailed insights into failed queries.
  • New Configuration Variables: Added new configuration variables such as pgsql-eventslog_buffer_history_size, pgsql-eventslog_table_memory_size, pgsql-eventslog_buffer_max_query_length, and admin-stats_pgsql_eventslog_sync_buffer_to_disk to control the behavior of the new logging system.
  • Prometheus Metrics Integration: Integrated Prometheus metrics for the PostgreSQL logger, exposing various counters and gauges related to event processing, buffer usage, and copy operations for enhanced observability.
  • Comprehensive Test Coverage: Included new TAP end-to-end tests to validate both manual and automatic dump flows, ensuring the reliability and correctness of the new PostgreSQL advanced query logging feature.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .gitignore
    • Added generated MySQL parser artifacts to the ignore list.
  • doc/internal/pgsql_advanced_query_logging_architecture.md
    • Added a new internal documentation file detailing the architecture, data model, admin interface, scheduler integration, and metrics of the PostgreSQL advanced query logging feature.
  • include/PgSQL_Logger.hpp
    • Added new metric structs (p_pl_counter, p_pl_gauge, pl_metrics_map_idx) for Prometheus integration.
    • Extended PgSQL_Event class with sqlstate, errmsg fields, a deep-copy constructor, and a destructor for proper memory management.
    • Introduced PgSQL_Logger_CircularBuffer class for thread-safe event buffering.
    • Added new methods to PgSQL_Logger for inserting events into SQLite, processing events, retrieving metrics, and updating Prometheus metrics.
  • include/PgSQL_Thread.h
    • Added new thread variables (eventslog_buffer_history_size, eventslog_table_memory_size, eventslog_buffer_max_query_length) to control PostgreSQL advanced logging behavior.
  • include/ProxySQL_Admin_Tables_Definitions.h
    • Defined the stats_pgsql_query_events table for in-memory storage of PostgreSQL query events.
  • include/ProxySQL_Statistics.hpp
    • Defined the history_pgsql_query_events table for on-disk storage of PostgreSQL query events.
    • Added last_timer_pgsql_dump_eventslog_to_disk and stats_pgsql_eventslog_sync_buffer_to_disk variables for scheduler management.
    • Introduced PgSQL_Logger_dump_eventslog_timetoget method to check for scheduled PostgreSQL event dumps.
  • include/proxysql_admin.h
    • Added stats_pgsql_eventslog_sync_buffer_to_disk variable to control PostgreSQL eventslog auto-dump scheduling.
  • include/proxysql_structs.h
    • Declared new __thread variables for PostgreSQL eventslog buffer settings.
  • lib/Admin_Bootstrap.cpp
    • Registered the new stats_pgsql_query_events table definition.
  • lib/Admin_Handler.cpp
    • Implemented handling for new DUMP PGSQL EVENTSLOG FROM BUFFER TO MEMORY, TO DISK, and TO BOTH commands.
  • lib/Makefile
    • Updated the clean target to remove generated MySQL parser artifacts.
  • lib/PgSQL_Logger.cpp
    • Implemented the PgSQL_Event deep-copy constructor and destructor.
    • Added helper functions (get_pgsql_eventslog_buffer_max_query_length_runtime, get_pgsql_eventslog_table_memory_size_runtime) for runtime variable access.
    • Modified PgSQL_Event::set_error to capture SQLSTATE and error messages.
    • Updated PgSQL_Event::write_query_format_2_json to include sqlstate and error fields.
    • Initialized PgSQL_Logger with a new PgSQL_Logger_CircularBuffer and Prometheus metrics.
    • Modified PgSQL_Logger::log_request to conditionally write to file and insert into the circular buffer, capturing error details.
    • Implemented PgSQL_Logger_CircularBuffer methods for insertion, draining, and size management.
    • Implemented PgSQL_Logger::insertPgSQLEventsIntoDb for bulk SQLite inserts.
    • Implemented PgSQL_Logger::processEvents to drain the buffer and persist events to memory/disk tables, managing table size and event deletion.
    • Implemented PgSQL_Logger::getAllMetrics to expose internal logger metrics.
    • Implemented PgSQL_Logger::p_update_metrics for Prometheus metric updates.
  • lib/PgSQL_Thread.cpp
    • Added initialization for new PostgreSQL eventslog buffer variables.
    • Updated get_variables_list to include new eventslog buffer variables.
    • Modified refresh_variables to update the PgSQL_Logger_CircularBuffer size based on configuration changes.
  • lib/ProxySQL_Admin.cpp
    • Added stats_pgsql_eventslog_sync_buffer_to_disk to admin variables.
    • Updated vacuum_stats to include stats_mysql_query_events and stats_pgsql_query_events tables.
    • Modified admin_main_loop to include scheduled auto-dumping for both MySQL and PostgreSQL eventslogs.
    • Updated update_modules_metrics to include Prometheus metric updates for the PostgreSQL logger.
  • lib/ProxySQL_Admin_Stats.cpp
    • Included PgSQL_Logger.hpp.
    • Updated stats___pgsql_global to expose PostgreSQL logger metrics to the stats_pgsql_global table.
  • lib/ProxySQL_Statistics.cpp
    • Registered the history_pgsql_query_events table definition.
    • Created indexes for history_pgsql_query_events on start_time and query_digest.
    • Implemented PgSQL_Logger_dump_eventslog_timetoget for scheduling PostgreSQL eventslog dumps.
  • test/tap/groups/groups.json
    • Added new test groups for pgsql_query_logging_autodump-t and pgsql_query_logging_memory-t.
  • test/tap/tests/pgsql_query_logging_autodump-t.cpp
    • Added a new TAP test file to validate the automatic buffer-to-disk synchronization of PostgreSQL eventslog.
  • test/tap/tests/pgsql_query_logging_memory-t.cpp
    • Added a new TAP test file to validate PostgreSQL advanced query logging in memory and history tables, including error capture.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces advanced query logging for PostgreSQL, bringing it to parity with the existing MySQL eventslog functionality. Key changes include capturing query events, in-memory circular buffering, manual and scheduled auto-dumping to SQLite tables, and Prometheus metrics exposure. The changes also include necessary updates to the .gitignore file, internal documentation, and TAP tests to validate the new features. The implementation appears robust and well-documented, aligning PostgreSQL logging capabilities with those already present for MySQL.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
lib/ProxySQL_Admin_Stats.cpp (1)

787-798: Prefer prepared statement for PgSQL logger metrics inserts.

Avoid per-metric malloc/snprintf and keep the stats insert path consistent with the prepared-statement helpers used elsewhere.

♻️ Suggested refactor
 if (GloPgSQL_Logger != nullptr) {
 	const string prefix = "PgSQL_Logger_";
-	std::unordered_map<std::string, unsigned long long> metrics = GloPgSQL_Logger->getAllMetrics();
-	for (std::unordered_map<std::string, unsigned long long>::iterator it = metrics.begin(); it != metrics.end(); it++) {
-		string var_name = prefix + it->first;
-		query = (char*)malloc(strlen(a) + var_name.length() + 32 + 16);
-		snprintf(query, strlen(a) + var_name.length() + 32 + 16, a, var_name.c_str(), std::to_string(it->second).c_str());
-		statsdb->execute(query);
-		free(query);
-	}
+	int rc = 0;
+	stmt_unique_ptr u_stmt { nullptr };
+	std::tie(rc, u_stmt) = statsdb->prepare_v2("INSERT INTO stats_pgsql_global VALUES (?1, ?2)");
+	ASSERT_SQLITE_OK(rc, statsdb);
+	sqlite3_stmt* const stmt { u_stmt.get() };
+
+	std::unordered_map<std::string, unsigned long long> metrics = GloPgSQL_Logger->getAllMetrics();
+	for (std::unordered_map<std::string, unsigned long long>::iterator it = metrics.begin(); it != metrics.end(); it++) {
+		const string var_name = prefix + it->first;
+		const string val = std::to_string(it->second);
+		rc = (*proxy_sqlite3_bind_text)(stmt, 1, var_name.c_str(), -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, statsdb);
+		rc = (*proxy_sqlite3_bind_text)(stmt, 2, val.c_str(), -1, SQLITE_TRANSIENT); 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);
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ProxySQL_Admin_Stats.cpp` around lines 787 - 798, The loop that iterates
GloPgSQL_Logger->getAllMetrics() currently builds SQL per-metric using
malloc/snprintf and calls statsdb->execute(query); replace this with a single
prepared-statement path: prepare the INSERT/UPDATE statement once (reuse the
same parameterized statement used by other metrics helpers), then for each
metric call the prepared-statement execution API with bound parameters (use the
metric name = prefix + it->first and metric value = it->second converted to
string/number) instead of allocating and formatting strings; update code around
GloPgSQL_Logger, getAllMetrics, and statsdb->execute usage to use the prepared
statement helpers and release any resources accordingly.
test/tap/tests/pgsql_query_logging_autodump-t.cpp (1)

169-175: Consider adding a diagnostic for the success-row count assertion.

If success_rows < num_queries, the ok() at line 175 fails with no context about the actual row count, which can make CI failures harder to triage — especially if the auto-dump fired but logged fewer rows than expected.

💡 Suggested improvement
  ok(success_rows_ok && success_rows >= num_queries, "History table includes expected successful rows");
+ if (success_rows_ok && success_rows < num_queries) {
+     diag("Expected >= %d successful rows, got %lld", num_queries, success_rows);
+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/tap/tests/pgsql_query_logging_autodump-t.cpp` around lines 169 - 175,
The test currently calls ok(success_rows_ok && success_rows >= num_queries, ...)
without emitting the actual values on failure; update the assertion to emit a
diagnostic when the count check fails by checking success_rows_ok and comparing
success_rows to num_queries and calling the test framework's diagnostic (e.g.,
diag or a message) with success_rows, num_queries and success_rows_ok
before/when calling ok; locate the variables success_rows, success_rows_ok and
the call to query_one_int and modify the logic so a clear diagnostic containing
success_rows and num_queries is printed when the condition is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/Admin_Handler.cpp`:
- Around line 3193-3218: The current branch handling "DUMP PGSQL EVENTSLOG"
treats a null GloPgSQL_Logger as success returning 0 rows; change that to report
an explicit error: in the block where you check if (it != commandMap.end()) add
a guard for GloPgSQL_Logger == nullptr and call
SPA->send_error_msg_to_client(sess, ...) (with a clear message like "PgSQL
logger not initialized") and avoid sending SPA->send_ok_msg_to_client;
otherwise, when GloPgSQL_Logger is non-null call
GloPgSQL_Logger->processEvents(it->second.first, it->second.second) and then
send_ok_msg_to_client as before so operators get a real error when the logger is
unavailable (refer to GloPgSQL_Logger, processEvents,
SPA->send_error_msg_to_client, SPA->send_ok_msg_to_client, commandMap,
query_no_space).

In `@lib/PgSQL_Logger.cpp`:
- Around line 1455-1472: The current logic deletes the table when events.size()
>= maxInMemorySize but then calls insertPgSQLEventsIntoDb(statsdb,
"stats_pgsql_query_events", numEventsToInsert, events.begin()), which reinserts
the oldest events from the batch; change the insert to use the tail of the
vector so the newest numEventsToInsert events are inserted (use events.end() -
numEventsToInsert as the iterator start) while keeping the same deletion logic
and using the existing helpers get_pgsql_eventslog_table_memory_size_runtime(),
statsdb, insertPgSQLEventsIntoDb, numEventsToInsert and events.
- Around line 319-322: PgSQL_Event::set_error currently stores raw pointers
(sqlstate, errmsg) to c_str() data which can dangle; instead copy the strings
into owned storage: modify set_error to take or construct std::string copies
(e.g., assign to member std::string fields or allocate std::string members)
rather than const char*; update PgSQL_Event to have std::string members (or
ensure assignments like sqlstate_str = _sqlstate; errmsg_str = _errmsg and set
sqlstate = sqlstate_str.c_str(); errmsg = errmsg_str.c_str()) so the error text
lifetime is owned by the object.

In `@test/tap/tests/pgsql_query_logging_autodump-t.cpp`:
- Around line 177-180: The cleanup currently only disables auto-dump scheduler
but leaves pgsql-eventslog_default_log and pgsql-eventslog_buffer_history_size
changed; update the cleanup sequence in the test (around the cleanup_ok /
exec_ok calls using admin_conn and exec_ok) to restore both settings (set
pgsql-eventslog_default_log back to 0 and pgsql-eventslog_buffer_history_size
back to its original value) before calling "LOAD ADMIN VARIABLES TO RUNTIME",
and include those exec_ok results in the cleanup_ok boolean so failures are
detected.

---

Nitpick comments:
In `@lib/ProxySQL_Admin_Stats.cpp`:
- Around line 787-798: The loop that iterates GloPgSQL_Logger->getAllMetrics()
currently builds SQL per-metric using malloc/snprintf and calls
statsdb->execute(query); replace this with a single prepared-statement path:
prepare the INSERT/UPDATE statement once (reuse the same parameterized statement
used by other metrics helpers), then for each metric call the prepared-statement
execution API with bound parameters (use the metric name = prefix + it->first
and metric value = it->second converted to string/number) instead of allocating
and formatting strings; update code around GloPgSQL_Logger, getAllMetrics, and
statsdb->execute usage to use the prepared statement helpers and release any
resources accordingly.

In `@test/tap/tests/pgsql_query_logging_autodump-t.cpp`:
- Around line 169-175: The test currently calls ok(success_rows_ok &&
success_rows >= num_queries, ...) without emitting the actual values on failure;
update the assertion to emit a diagnostic when the count check fails by checking
success_rows_ok and comparing success_rows to num_queries and calling the test
framework's diagnostic (e.g., diag or a message) with success_rows, num_queries
and success_rows_ok before/when calling ok; locate the variables success_rows,
success_rows_ok and the call to query_one_int and modify the logic so a clear
diagnostic containing success_rows and num_queries is printed when the condition
is false.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements advanced query logging for PostgreSQL with operational parity to the existing MySQL eventslog infrastructure. It introduces a complete event capture, buffering, persistence, and management pipeline for PostgreSQL queries, including in-memory circular buffering, manual dump commands, scheduler-driven auto-dump, comprehensive metrics, and end-to-end test coverage.

Changes:

  • Adds PostgreSQL query event logging pipeline with PgSQL_Event deep-copy semantics, PgSQL_Logger_CircularBuffer for thread-safe event buffering, and dual-sink persistence to in-memory (stats_pgsql_query_events) and on-disk (history_pgsql_query_events) SQLite tables
  • Introduces DUMP PGSQL EVENTSLOG FROM BUFFER TO {MEMORY|DISK|BOTH} admin commands accessible from both MySQL (port 6032) and PostgreSQL (port 6132) admin interfaces, with symmetric scheduler auto-dump controlled by admin-stats_pgsql_eventslog_sync_buffer_to_disk variable
  • Adds TAP test coverage for manual dump flow and scheduler-driven auto-dump with validation of table schemas, SQLSTATE capture, error message persistence, and row accounting

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
lib/PgSQL_Logger.cpp Core implementation of event capture, circular buffer, copy constructor, persistence to SQLite, and Prometheus metrics
include/PgSQL_Logger.hpp Class definitions for PgSQL_Event, PgSQL_Logger_CircularBuffer, metric enums, and API documentation
lib/PgSQL_Thread.cpp Integration of eventslog variables and runtime buffer size updates
lib/ProxySQL_Admin.cpp Admin variable registration, scheduler loop integration for auto-dump, and metrics update hooks
lib/Admin_Handler.cpp Command parser and dispatcher for DUMP PGSQL EVENTSLOG commands
lib/ProxySQL_Admin_Stats.cpp Stats table population with PgSQL_Logger metrics
lib/ProxySQL_Statistics.cpp Timer logic for periodic PgSQL eventslog dumps and table/index definitions
lib/Admin_Bootstrap.cpp In-memory stats table registration for stats_pgsql_query_events
include/ProxySQL_Admin_Tables_Definitions.h Schema definition for in-memory PostgreSQL query events table
include/ProxySQL_Statistics.hpp Schema definition for on-disk history table and timer state
include/proxysql_admin.h Admin variable declaration for stats_pgsql_eventslog_sync_buffer_to_disk
include/PgSQL_Thread.h Thread variable declarations for buffer configuration
include/proxysql_structs.h Thread-local variable extern declarations for eventslog config
test/tap/tests/pgsql_query_logging_memory-t.cpp TAP test validating manual dump, column schema, SQLSTATE capture, and error message persistence
test/tap/tests/pgsql_query_logging_autodump-t.cpp TAP test validating scheduler-driven auto-dump behavior
test/tap/groups/groups.json Test group registration for new PostgreSQL logging tests
doc/internal/pgsql_advanced_query_logging_architecture.md Comprehensive as-built architecture documentation
lib/Makefile Enhanced clean target to remove generated parser artifacts
.gitignore Added MySQL parser artifacts to ignore list

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +320 to +321
sqlstate = const_cast<char*>(_sqlstate);
errmsg = const_cast<char*>(_errmsg);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The set_error method uses const_cast to store pointers without copying the data. This creates a lifetime/ownership bug. The stored pointers (sqlstate and errmsg) point to memory owned by the caller (myds->myconn), but when the event is deep-copied via the copy constructor (lines 242-247), strdup is used to duplicate these pointers, setting free_on_delete=true. This means:

  1. The original event holds borrowed pointers (not owned)
  2. The copied event holds owned pointers (duplicated via strdup)
  3. When both are destroyed, the copy correctly frees its memory but the original event has free_on_delete=false so it won't free

However, if the original connection object is destroyed before the event copy, the original event's pointers become dangling.

MySQL's equivalent method set_errmsg (lib/MySQL_Logger.cpp:485-489) correctly uses strdup immediately to take ownership. This PgSQL implementation should match that pattern and strdup the strings in set_error rather than storing borrowed pointers with const_cast.

Suggested change
sqlstate = const_cast<char*>(_sqlstate);
errmsg = const_cast<char*>(_errmsg);
// If we previously owned these strings, free them before overwriting.
if (free_on_delete) {
if (sqlstate != nullptr) {
free(sqlstate);
}
if (errmsg != nullptr) {
free(errmsg);
}
}
// Take ownership of new copies of the provided strings.
sqlstate = (_sqlstate != nullptr) ? strdup(_sqlstate) : nullptr;
errmsg = (_errmsg != nullptr) ? strdup(_errmsg) : nullptr;
free_on_delete = true;

Copilot uses AI. Check for mistakes.
}

PgSQL_Event::PgSQL_Event(const PgSQL_Event& other) {
memcpy(this, &other, sizeof(PgSQL_Event));
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copy constructor uses memcpy(this, &other, sizeof(PgSQL_Event)) before selectively deep-copying pointer fields. This shallow copy includes copying all pointer values, which are then overwritten. While this works, it's fragile and error-prone:

  1. The buf array is copied but immediately zeroed in the original constructor (line 206), creating inconsistency
  2. If new pointer fields are added to PgSQL_Event but not handled in the copy constructor, they'll be shallow-copied leading to double-free bugs
  3. The pattern mixes shallow and deep copying in a non-obvious way

A safer approach would be to explicitly copy each field, or use a member-wise copy followed by selective deep-copies with clear documentation that all pointer fields must be handled explicitly.

Copilot uses AI. Check for mistakes.
}
wrunlock();
}
if (PgLogCB->buffer_size != 0) {
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buffer_size field is declared as std::atomic<size_t> (line 187 in include/PgSQL_Logger.hpp) but is accessed and modified while holding the mutex lock in setBufferSize. This creates a race condition:

  1. Line 1039 reads PgLogCB->buffer_size != 0 without holding the mutex
  2. Line 1353 writes buffer_size = newSize while holding the mutex
  3. Lines 1322 and 1327 read buffer_size while holding the mutex

This mixing of atomic and mutex-protected access is inconsistent. Either:

  • Make buffer_size a plain size_t and always access it under the mutex lock, OR
  • Use only atomic operations (no mutex) for buffer_size access, OR
  • Document that unlocked atomic reads are intentional for performance but ensure the logic handles potential races correctly

The current implementation at line 1039 could read a stale/intermediate buffer_size value during a concurrent setBufferSize call, though this particular race may be benign.

Copilot uses AI. Check for mistakes.

free_on_delete = true;
}

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing assignment operator (operator=) for PgSQL_Event class. The class has:

  • A custom copy constructor that performs deep copying
  • A custom destructor that conditionally frees memory
  • Pointer members that need special handling

Without a custom assignment operator, the compiler-generated one will perform shallow copying of pointers, leading to double-free bugs or memory leaks. This violates the Rule of Three/Five in C++.

Either explicitly delete the assignment operator if copying should only happen via copy constructor, or implement a proper deep-copy assignment operator matching the copy constructor semantics.

Suggested change
PgSQL_Event& PgSQL_Event::operator=(const PgSQL_Event& other) {
if (this == &other) {
return *this;
}
if (free_on_delete == true) {
if (username != nullptr) {
free(username); username = nullptr;
}
if (schemaname != nullptr) {
free(schemaname); schemaname = nullptr;
}
if (query_ptr != nullptr) {
free(query_ptr); query_ptr = nullptr;
}
if (server != nullptr) {
free(server); server = nullptr;
}
if (client != nullptr) {
free(client); client = nullptr;
}
if (extra_info != nullptr) {
free(extra_info); extra_info = nullptr;
}
if (client_stmt_name != nullptr) {
free(client_stmt_name); client_stmt_name = nullptr;
}
if (sqlstate != nullptr) {
free(sqlstate); sqlstate = nullptr;
}
if (errmsg != nullptr) {
free(errmsg); errmsg = nullptr;
}
}
memcpy(this, &other, sizeof(PgSQL_Event));
if (other.username != nullptr) {
username = strdup(other.username);
}
if (other.schemaname != nullptr) {
schemaname = strdup(other.schemaname);
}
if (other.query_ptr != nullptr) {
size_t maxQueryLen = get_pgsql_eventslog_buffer_max_query_length_runtime();
size_t lenToCopy = std::min(other.query_len, maxQueryLen);
query_ptr = (char*)malloc(lenToCopy + 1);
memcpy(query_ptr, other.query_ptr, lenToCopy);
query_ptr[lenToCopy] = '\0';
query_len = lenToCopy;
}
if (other.server != nullptr) {
server = (char*)malloc(server_len + 1);
memcpy(server, other.server, server_len);
server[server_len] = '\0';
}
if (other.client != nullptr) {
client = (char*)malloc(client_len + 1);
memcpy(client, other.client, client_len);
client[client_len] = '\0';
}
if (other.extra_info != nullptr) {
extra_info = strdup(other.extra_info);
}
if (other.client_stmt_name != nullptr) {
client_stmt_name = strdup(other.client_stmt_name);
}
if (other.sqlstate != nullptr) {
sqlstate = strdup(other.sqlstate);
}
if (other.errmsg != nullptr) {
errmsg = strdup(other.errmsg);
}
free_on_delete = true;
return *this;
}

Copilot uses AI. Check for mistakes.
Comment on lines +1485 to +1502
std::unordered_map<std::string, unsigned long long> PgSQL_Logger::getAllMetrics() const {
std::unordered_map<std::string, unsigned long long> allMetrics;

allMetrics["memoryCopyCount"] = metrics.memoryCopyCount;
allMetrics["diskCopyCount"] = metrics.diskCopyCount;
allMetrics["getAllEventsCallsCount"] = metrics.getAllEventsCallsCount;
allMetrics["getAllEventsEventsCount"] = metrics.getAllEventsEventsCount;
allMetrics["totalMemoryCopyTimeMicros"] = metrics.totalMemoryCopyTimeMicros;
allMetrics["totalDiskCopyTimeMicros"] = metrics.totalDiskCopyTimeMicros;
allMetrics["totalGetAllEventsTimeMicros"] = metrics.totalGetAllEventsTimeMicros;
allMetrics["totalEventsCopiedToMemory"] = metrics.totalEventsCopiedToMemory;
allMetrics["totalEventsCopiedToDisk"] = metrics.totalEventsCopiedToDisk;
allMetrics["circularBufferEventsAddedCount"] = PgLogCB->getEventsAddedCount();
allMetrics["circularBufferEventsDroppedCount"] = PgLogCB->getEventsDroppedCount();
allMetrics["circularBufferEventsSize"] = PgLogCB->size();

return allMetrics;
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getAllMetrics() method is marked const but calls PgLogCB->size(), PgLogCB->getEventsAddedCount(), and PgLogCB->getEventsDroppedCount(). The size() method acquires a mutex lock (line 1343), which is a non-const operation. This violates const-correctness - a const method should not modify object state (including acquiring locks on member objects).

While this may compile because PgLogCB is a pointer (not const-qualified), it's semantically incorrect and could lead to undefined behavior with const objects. Either:

  • Remove const from getAllMetrics(), or
  • Make the circular buffer methods const and use mutable mutex, or
  • Return cached/snapshot values instead of calling into the circular buffer

Copilot uses AI. Check for mistakes.
Comment on lines +1433 to +1483
int PgSQL_Logger::processEvents(SQLite3DB* statsdb, SQLite3DB* statsdb_disk) {
unsigned long long startTimeMicros = monotonic_time();
std::vector<PgSQL_Event*> events = {};
PgLogCB->get_all_events(events);
metrics.getAllEventsCallsCount++;
if (events.empty()) return 0;

unsigned long long afterGetAllEventsTimeMicros = monotonic_time();
metrics.getAllEventsEventsCount += events.size();
metrics.totalGetAllEventsTimeMicros += (afterGetAllEventsTimeMicros - startTimeMicros);

if (statsdb_disk != nullptr) {
unsigned long long diskStartTimeMicros = monotonic_time();
insertPgSQLEventsIntoDb(statsdb_disk, "history_pgsql_query_events", events.size(), events.begin());
unsigned long long diskEndTimeMicros = monotonic_time();
metrics.diskCopyCount++;
metrics.totalDiskCopyTimeMicros += (diskEndTimeMicros - diskStartTimeMicros);
metrics.totalEventsCopiedToDisk += events.size();
}

if (statsdb != nullptr) {
unsigned long long memoryStartTimeMicros = monotonic_time();
size_t maxInMemorySize = get_pgsql_eventslog_table_memory_size_runtime();
size_t numEventsToInsert = std::min(events.size(), maxInMemorySize);

if (events.size() >= maxInMemorySize) {
statsdb->execute("DELETE FROM stats_pgsql_query_events");
} else {
int current_rows = statsdb->return_one_int((char*)"SELECT COUNT(*) FROM stats_pgsql_query_events");
int rows_to_keep = maxInMemorySize - events.size();
if (current_rows > rows_to_keep) {
int rows_to_delete = (current_rows - rows_to_keep);
std::string query = "SELECT MAX(id) FROM (SELECT id FROM stats_pgsql_query_events ORDER BY id LIMIT " + std::to_string(rows_to_delete) + ")";
int maxIdToDelete = statsdb->return_one_int(query.c_str());
std::string delete_stmt = "DELETE FROM stats_pgsql_query_events WHERE id <= " + std::to_string(maxIdToDelete);
statsdb->execute(delete_stmt.c_str());
}
}
insertPgSQLEventsIntoDb(statsdb, "stats_pgsql_query_events", numEventsToInsert, events.begin());
unsigned long long memoryEndTimeMicros = monotonic_time();
metrics.memoryCopyCount++;
metrics.totalMemoryCopyTimeMicros += (memoryEndTimeMicros - memoryStartTimeMicros);
metrics.totalEventsCopiedToMemory += numEventsToInsert;
}

for (PgSQL_Event* event : events) {
delete event;
}

return events.size();
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The processEvents method lacks exception safety. If insertPgSQLEventsIntoDb throws an exception (e.g., due to SQLite errors at lines 1446 or 1471), the events vector will not be cleaned up and all PgSQL_Event pointers will leak memory. The cleanup loop at lines 1478-1480 will never execute.

Consider wrapping the events vector in a smart pointer container or using RAII (e.g., unique_ptr with custom deleter) to ensure cleanup happens even during exception unwinding. Alternatively, wrap the insertion calls in try-catch blocks with cleanup in the catch handler.

Copilot uses AI. Check for mistakes.
Comment on lines +1461 to +1464
int current_rows = statsdb->return_one_int((char*)"SELECT COUNT(*) FROM stats_pgsql_query_events");
int rows_to_keep = maxInMemorySize - events.size();
if (current_rows > rows_to_keep) {
int rows_to_delete = (current_rows - rows_to_keep);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential signed/unsigned mismatch and overflow risk. The variable rows_to_keep is calculated as maxInMemorySize - events.size() where both operands are size_t (unsigned), but the result is stored in int (signed). If events.size() > maxInMemorySize (which shouldn't happen based on line 1456's std::min, but defensive programming matters), this would cause unsigned wraparound before conversion to signed int, resulting in a very large positive or negative value.

Similarly, current_rows and rows_to_delete are int but compared/used with size_t values. Consider using size_t or adding explicit bounds checking to prevent potential overflow or comparison issues.

Suggested change
int current_rows = statsdb->return_one_int((char*)"SELECT COUNT(*) FROM stats_pgsql_query_events");
int rows_to_keep = maxInMemorySize - events.size();
if (current_rows > rows_to_keep) {
int rows_to_delete = (current_rows - rows_to_keep);
int current_rows_int = statsdb->return_one_int((char*)"SELECT COUNT(*) FROM stats_pgsql_query_events");
if (current_rows_int < 0) {
current_rows_int = 0;
}
size_t current_rows = static_cast<size_t>(current_rows_int);
size_t rows_to_keep = 0;
if (maxInMemorySize > events.size()) {
rows_to_keep = maxInMemorySize - events.size();
}
if (current_rows > rows_to_keep) {
size_t rows_to_delete = current_rows - rows_to_keep;

Copilot uses AI. Check for mistakes.
…memory table

Problem 1:
- `PgSQL_Event::set_error()` stored borrowed pointers from connection-owned strings.
- This could create lifetime hazards before/around event serialization and buffering.

Fix:
- Add explicit ownership tracking for error fields via `free_error_on_delete`.
- Make `set_error()` duplicate incoming SQLSTATE/error strings.
- Free owned error strings in destructor when the event does not own all fields.
- Keep copy-constructor behavior for deep-copied buffered events and disable copy-assignment to avoid accidental shallow assignment.

Problem 2:
- `processEvents()` could repopulate `stats_pgsql_query_events` using the beginning of the drained batch after retention pruning.
- This keeps older rows instead of the most recent rows when truncation is required.

Fix:
- Insert the tail of the drained batch (`events.end() - numEventsToInsert`) so memory table retention always preserves latest events.
- Tighten row retention arithmetic using `size_t` to avoid signed/unsigned mismatch in keep/delete calculations.
…insert path

Admin command behavior:
- `DUMP PGSQL EVENTSLOG ...` returned `OK 0` when `GloPgSQL_Logger` was null.
- This masked unavailable logger state from operators.

Change:
- Return an explicit admin error (`PgSQL logger not initialized`) when logger is unavailable.
- Preserve normal `OK` response path when logger is present.

Stats path behavior:
- PgSQL logger metrics insertion into `stats_pgsql_global` used per-row string formatting and `execute()` calls.

Change:
- Replace per-row dynamic SQL construction with a prepared-statement row insert path.
- Reuse `sqlite3_global_stats_row_step()` for consistent binding/reset semantics.
…time cleanup

Test diagnostics:
- Add explicit `diag()` output for successful-row assertion failures.
- Include expected row count, observed row count, and query status for faster CI triage.

Cleanup behavior:
- Extend cleanup to restore:
  - `pgsql-eventslog_default_log=0`
  - `pgsql-eventslog_buffer_history_size=0`
- Apply both runtime loads during cleanup:
  - `LOAD ADMIN VARIABLES TO RUNTIME`
  - `LOAD PGSQL VARIABLES TO RUNTIME`

This avoids residual PGSQL eventslog settings leaking into subsequent TAP tests.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
test/tap/tests/pgsql_query_logging_autodump-t.cpp (1)

125-191: Ensure cleanup runs on early exits to avoid cross-test config leakage.
Several return exit_status() paths occur after runtime variables are changed. A small cleanup helper (invoked before early returns and at the end) will keep TAP runs isolated.

♻️ Suggested refactor (centralize cleanup)
@@
-	bool setup_ok = true;
+	auto run_cleanup = [&]() {
+		if (!admin_conn) return;
+		bool cleanup_ok = true;
+		cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "SET admin-stats_pgsql_eventslog_sync_buffer_to_disk=0");
+		cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "SET pgsql-eventslog_default_log=0");
+		cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "SET pgsql-eventslog_buffer_history_size=0");
+		cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "LOAD ADMIN VARIABLES TO RUNTIME");
+		cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "LOAD PGSQL VARIABLES TO RUNTIME");
+		ok(cleanup_ok, "Cleanup completed and auto-dump scheduler disabled");
+	};
+
+	bool setup_ok = true;
@@
-	if (!setup_ok) {
-		return exit_status();
-	}
+	if (!setup_ok) {
+		run_cleanup();
+		return exit_status();
+	}
@@
-	if (!baseline_ok) {
-		return exit_status();
-	}
+	if (!baseline_ok) {
+		run_cleanup();
+		return exit_status();
+	}
@@
-	if (!run_queries_ok) {
-		return exit_status();
-	}
+	if (!run_queries_ok) {
+		run_cleanup();
+		return exit_status();
+	}
@@
-	bool cleanup_ok = true;
-	cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "SET admin-stats_pgsql_eventslog_sync_buffer_to_disk=0");
-	cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "SET pgsql-eventslog_default_log=0");
-	cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "SET pgsql-eventslog_buffer_history_size=0");
-	cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "LOAD ADMIN VARIABLES TO RUNTIME");
-	cleanup_ok = cleanup_ok && exec_ok(admin_conn.get(), "LOAD PGSQL VARIABLES TO RUNTIME");
-	ok(cleanup_ok, "Cleanup completed and auto-dump scheduler disabled");
+	run_cleanup();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/tap/tests/pgsql_query_logging_autodump-t.cpp` around lines 125 - 191,
The test currently returns early from multiple places (after setup and workload
generation) without restoring runtime vars, causing cross-test leakage; add a
small helper function (e.g., cleanup_test_env or
restore_pgsql_eventslog_settings) that runs the existing cleanup sequence (the
same calls using exec_ok with "SET
admin-stats_pgsql_eventslog_sync_buffer_to_disk=0", "SET
pgsql-eventslog_default_log=0", "SET pgsql-eventslog_buffer_history_size=0",
"LOAD ADMIN VARIABLES TO RUNTIME", "LOAD PGSQL VARIABLES TO RUNTIME") and call
this helper before every early return (the return exit_status() after setup_ok
and baseline_ok and run_queries_ok failures) as well as at the end in place of
the inline cleanup_ok block to centralize cleanup and prevent config leakage.
lib/PgSQL_Logger.cpp (1)

222-235: Unchecked malloc returns in copy constructor (Cppcheck).

Lines 222, 228, and 233 each call malloc and immediately pass the result to memcpy without a null check. An OOM return from malloc would produce a null-pointer dereference. This follows ProxySQL's established OOM-as-fatal convention, so it may be intentional, but the pattern warrants awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Logger.cpp` around lines 222 - 235, The copy constructor in
PgSQL_Logger (allocations for query_ptr, server, client) calls malloc and then
memcpy without checking for null; add checks after each malloc (for query_ptr,
server, client) and handle OOM consistently with the project's fatal-on-OOM
policy (e.g., log an error via the logger or process logger and abort/exit or
call the project-wide OOM helper), ensuring you do not call memcpy if malloc
returned nullptr and that allocated lengths (query_len, server_len, client_len)
remain consistent on error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/PgSQL_Logger.cpp`:
- Around line 1059-1062: Change PgLogCB->buffer_size to be std::atomic<size_t>
to avoid the data race: update the declaration of buffer_size in class
PgSQL_Logger_CircularBuffer to std::atomic<size_t>, replace plain assignments in
setBufferSize() with atomic stores (while keeping the existing mutex-protected
drain logic), and leave reads in the hot path (e.g., the check in the block
using PgLogCB->buffer_size and any getBufferSize()) as atomic loads; this
preserves semantics for insert()/new PgSQL_Event(me) while eliminating the UB
race.
- Around line 328-342: The set_error method can overwrite strdup'd
sqlstate/errmsg on copy-constructed objects when free_on_delete == true; modify
PgSQL_Event::set_error to free any existing sqlstate and errmsg if
free_error_on_delete is true (regardless of free_on_delete) before assigning new
strdup'd values, ensuring you check each pointer for nullptr before free and
then strdup the incoming _sqlstate/_errmsg and set free_error_on_delete = true.

---

Duplicate comments:
In `@lib/PgSQL_Logger.cpp`:
- Around line 1492-1498: The fix to preserve the newest events is already
correctly implemented: when events.size() > numEventsToInsert the code sets
begin_it = events.end() - numEventsToInsert and passes that iterator into
insertPgSQLEventsIntoDb, so no code change is required for
insertPgSQLEventsIntoDb, begin_it, events or numEventsToInsert; mark this review
comment as duplicate/resolved and remove or close the redundant remark.
- Around line 328-342: The original dangling-pointer issue in
PgSQL_Event::set_error (sqlstate/errmsg receiving raw const char* from
temporaries) is resolved by duplicating inputs with strdup and setting
free_error_on_delete = true; verify that PgSQL_Event::set_error consistently
frees any previously owned sqlstate/errmsg before replacing them (the existing
conditional frees the previous buffers) and keep free_error_on_delete set to
true so the destructor/cleanup logic knows these buffers are owned and will be
freed.

---

Nitpick comments:
In `@lib/PgSQL_Logger.cpp`:
- Around line 222-235: The copy constructor in PgSQL_Logger (allocations for
query_ptr, server, client) calls malloc and then memcpy without checking for
null; add checks after each malloc (for query_ptr, server, client) and handle
OOM consistently with the project's fatal-on-OOM policy (e.g., log an error via
the logger or process logger and abort/exit or call the project-wide OOM
helper), ensuring you do not call memcpy if malloc returned nullptr and that
allocated lengths (query_len, server_len, client_len) remain consistent on
error.

In `@test/tap/tests/pgsql_query_logging_autodump-t.cpp`:
- Around line 125-191: The test currently returns early from multiple places
(after setup and workload generation) without restoring runtime vars, causing
cross-test leakage; add a small helper function (e.g., cleanup_test_env or
restore_pgsql_eventslog_settings) that runs the existing cleanup sequence (the
same calls using exec_ok with "SET
admin-stats_pgsql_eventslog_sync_buffer_to_disk=0", "SET
pgsql-eventslog_default_log=0", "SET pgsql-eventslog_buffer_history_size=0",
"LOAD ADMIN VARIABLES TO RUNTIME", "LOAD PGSQL VARIABLES TO RUNTIME") and call
this helper before every early return (the return exit_status() after setup_ok
and baseline_ok and run_queries_ok failures) as well as at the end in place of
the inline cleanup_ok block to centralize cleanup and prevent config leakage.

… internals

Address two minor correctness/style follow-ups from final review:

1) `PgSQL_Event::set_error()` overwrite safety
- Free previous `sqlstate` / `errmsg` buffers when either ownership mode is active
  (`free_on_delete` or `free_error_on_delete`) before replacing pointers.
- This avoids latent leaks if `set_error()` is called on deep-copied events.

2) Explicit atomic access for circular-buffer capacity
- Keep `buffer_size` atomic and use explicit `load/store` operations with
  `std::memory_order_relaxed` in hot-path and helper methods.
- Update log-path check, insert path, getter, and setter to use explicit atomic
  operations for clearer thread-safety semantics.
Add the new autodump E2E test to the default-g4 test group configuration.
@sonarqubecloud
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/PgSQL_Logger.cpp`:
- Around line 1474-1492: The code currently treats maxInMemorySize == 0 as a
valid size, causing the unconditional DELETE; change the logic in the block
using get_pgsql_eventslog_table_memory_size_runtime() so that when
maxInMemorySize == 0 (the "disabled" sentinel) you skip the
trimming/delete/insert logic entirely (e.g., early continue/return or bypass the
stats_pgsql_query_events maintenance) — update the code around statsdb,
maxInMemorySize, and events handling (the block using statsdb->execute and
return_one_int and the variable numEventsToInsert) to explicitly guard on
maxInMemorySize > 0 before performing any DELETE or row-pruning operations.
- Around line 219-236: In the copy constructor where query_ptr, server, and
client are allocated (using malloc with
get_pgsql_eventslog_buffer_max_query_length_runtime(), and variables query_len,
server_len, client_len), add null-checks for each malloc result: if malloc
returns nullptr, avoid calling memcpy/setting terminator; instead handle OOM by
either throwing std::bad_alloc (or returning/setting the member pointer to
nullptr and corresponding length to 0) and clean up any already-allocated
members to avoid leaks; ensure each allocation path validates the pointer before
memcpy and sets a deterministic state (pointer and length) on failure.

Comment on lines +219 to +236
if (other.query_ptr != nullptr) {
size_t maxQueryLen = get_pgsql_eventslog_buffer_max_query_length_runtime();
size_t lenToCopy = std::min(other.query_len, maxQueryLen);
query_ptr = (char*)malloc(lenToCopy + 1);
memcpy(query_ptr, other.query_ptr, lenToCopy);
query_ptr[lenToCopy] = '\0';
query_len = lenToCopy;
}
if (other.server != nullptr) {
server = (char*)malloc(server_len + 1);
memcpy(server, other.server, server_len);
server[server_len] = '\0';
}
if (other.client != nullptr) {
client = (char*)malloc(client_len + 1);
memcpy(client, other.client, client_len);
client[client_len] = '\0';
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing null-check after malloc in copy constructor.

Lines 223, 229, and 234: if malloc returns nullptr (OOM), the subsequent memcpy dereferences a null pointer (UB). All three strdup-based copies in the same constructor carry the same latent risk (cppcheck also flags lines 31, 86 in the broader file).

🛡️ Proposed defensive fix
 if (other.query_ptr != nullptr) {
     size_t maxQueryLen = get_pgsql_eventslog_buffer_max_query_length_runtime();
     size_t lenToCopy = std::min(other.query_len, maxQueryLen);
     query_ptr = (char*)malloc(lenToCopy + 1);
+    if (query_ptr == nullptr) { proxy_error("malloc failed for query_ptr\n"); return; }
     memcpy(query_ptr, other.query_ptr, lenToCopy);
     query_ptr[lenToCopy] = '\0';
     query_len = lenToCopy;
 }
 if (other.server != nullptr) {
     server = (char*)malloc(server_len + 1);
+    if (server == nullptr) { proxy_error("malloc failed for server\n"); return; }
     memcpy(server, other.server, server_len);
     server[server_len] = '\0';
 }
 if (other.client != nullptr) {
     client = (char*)malloc(client_len + 1);
+    if (client == nullptr) { proxy_error("malloc failed for client\n"); return; }
     memcpy(client, other.client, client_len);
     client[client_len] = '\0';
 }
🧰 Tools
🪛 Cppcheck (2.19.0)

[warning] 223-223: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 224-224: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 229-229: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 230-230: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 234-234: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 235-235: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Logger.cpp` around lines 219 - 236, In the copy constructor where
query_ptr, server, and client are allocated (using malloc with
get_pgsql_eventslog_buffer_max_query_length_runtime(), and variables query_len,
server_len, client_len), add null-checks for each malloc result: if malloc
returns nullptr, avoid calling memcpy/setting terminator; instead handle OOM by
either throwing std::bad_alloc (or returning/setting the member pointer to
nullptr and corresponding length to 0) and clean up any already-allocated
members to avoid leaks; ensure each allocation path validates the pointer before
memcpy and sets a deterministic state (pointer and length) on failure.

Comment on lines +1474 to +1492
if (statsdb != nullptr) {
unsigned long long memoryStartTimeMicros = monotonic_time();
size_t maxInMemorySize = get_pgsql_eventslog_table_memory_size_runtime();
size_t numEventsToInsert = std::min(events.size(), maxInMemorySize);

if (events.size() >= maxInMemorySize) {
statsdb->execute("DELETE FROM stats_pgsql_query_events");
} else {
int current_rows_i = statsdb->return_one_int((char*)"SELECT COUNT(*) FROM stats_pgsql_query_events");
size_t current_rows = (current_rows_i > 0) ? static_cast<size_t>(current_rows_i) : 0;
size_t rows_to_keep = maxInMemorySize - events.size();
if (current_rows > rows_to_keep) {
size_t rows_to_delete = (current_rows - rows_to_keep);
std::string query = "SELECT MAX(id) FROM (SELECT id FROM stats_pgsql_query_events ORDER BY id LIMIT " + std::to_string(rows_to_delete) + ")";
int maxIdToDelete = statsdb->return_one_int(query.c_str());
std::string delete_stmt = "DELETE FROM stats_pgsql_query_events WHERE id <= " + std::to_string(maxIdToDelete);
statsdb->execute(delete_stmt.c_str());
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

maxInMemorySize == 0 silently clears stats_pgsql_query_events on every call.

When get_pgsql_eventslog_table_memory_size_runtime() returns 0 and statsdb != nullptr, the condition events.size() >= maxInMemorySize is size_t >= 0 which is always true (unsigned), so DELETE FROM stats_pgsql_query_events runs every invocation and no rows are ever inserted. If 0 is meant as a "disabled" sentinel, guard it explicitly before reaching the delete/insert logic.

🛡️ Proposed fix
 if (statsdb != nullptr) {
+    size_t maxInMemorySize = get_pgsql_eventslog_table_memory_size_runtime();
+    if (maxInMemorySize == 0) {
+        // Memory table disabled; skip entirely.
+    } else {
     unsigned long long memoryStartTimeMicros = monotonic_time();
-    size_t maxInMemorySize = get_pgsql_eventslog_table_memory_size_runtime();
     ...
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Logger.cpp` around lines 1474 - 1492, The code currently treats
maxInMemorySize == 0 as a valid size, causing the unconditional DELETE; change
the logic in the block using get_pgsql_eventslog_table_memory_size_runtime() so
that when maxInMemorySize == 0 (the "disabled" sentinel) you skip the
trimming/delete/insert logic entirely (e.g., early continue/return or bypass the
stats_pgsql_query_events maintenance) — update the code around statsdb,
maxInMemorySize, and events handling (the block using statsdb->execute and
return_one_int and the variable numEventsToInsert) to explicitly guard on
maxInMemorySize > 0 before performing any DELETE or row-pruning operations.

@renecannao renecannao merged commit bad7ce4 into v3.0 Feb 18, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: PostgreSQL advanced query logging parity with MySQL eventslog

2 participants