pax_global_header00006660000000000000000000000064151651541630014521gustar00rootroot0000000000000052 comment=bb61cbab7b06d58b4d704c9852a284952ca1d39e pgauditlogtofile-1.8.4/000077500000000000000000000000001516515416300150755ustar00rootroot00000000000000pgauditlogtofile-1.8.4/.gitignore000066400000000000000000000000631516515416300170640ustar00rootroot00000000000000/*.o /*.so /*.bc /.vagrant /test/.vagrant /.vscode pgauditlogtofile-1.8.4/COPYING000066400000000000000000000016761516515416300161420ustar00rootroot00000000000000pglog - PostgreSQL extension Copyright (c) 2014, 2ndQuadrant Ltd. Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL 2NDQUADRANT BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 2NDQUADRANT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 2NDQUADRANT SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND 2NDQUADRANT HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. pgauditlogtofile-1.8.4/LICENSE000066400000000000000000000023771516515416300161130ustar00rootroot00000000000000Portions Copyright (c) 2021-2026, Francisco Miguel Biete Banon Portions Copyright (c) 1996-2021, The PostgreSQL Global Development Group Portions Copyright (c) 1994, The Regents of the University of California This code is released under the PostgreSQL licence, as given at http://www.postgresql.org/about/licence/ Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN “AS IS” BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. pgauditlogtofile-1.8.4/Makefile000066400000000000000000000030531516515416300165360ustar00rootroot00000000000000# pgauditlogtofile/Makefile EXTENSION = pgauditlogtofile MODULE_big = pgauditlogtofile PGFILEDESC = "pgAuditLogToFile - An addon for pgAudit logging extension for PostgreSQL" OBJS = pgauditlogtofile.o logtofile.o logtofile_bgw.o logtofile_connect.o logtofile_guc.o logtofile_log.o logtofile_shmem.o logtofile_autoclose.o logtofile_vars.o logtofile_filename.o logtofile_json.o logtofile_csv.o logtofile_string_format.o logtofile_execution_memory.o logtofile_execution_time.o logtofile_execution_hook.o logtofile_urgentclose.o logtofile_signal_handler.o logtofile_errordata.o DATA = pgauditlogtofile--1.0.sql pgauditlogtofile--1.0--1.2.sql pgauditlogtofile--1.2--1.3.sql pgauditlogtofile--1.3--1.4.sql pgauditlogtofile--1.4--1.5.sql pgauditlogtofile--1.5--1.6.sql pgauditlogtofile--1.6--1.7.sql pgauditlogtofile--1.7--1.8.sql REGRESS_OPTS = --inputdir=test --outputdir=test --load-extension=pgaudit --load-extension=pgauditlogtofile --user=postgres REGRESS = extension_exists guc_defaults audit_file_exists audit_file_content audit_file_mode #REGRESS = extension_exists guc_defaults audit_file_exists audit_file_content rotation connections execution_data file_mode error_conditions disconnection_rotation_1_setup disconnection_rotation_2_check GCC_VERSION := $(shell gcc -dumpversion | cut -f1 -d.) ifeq ($(shell [ $(GCC_VERSION) -ge 10 ] && echo true),true) PG_CFLAGS += -fanalyzer -Wall -Wdiscarded-qualifiers -lz -llz4 -lzstd else PG_CFLAGS += -Wall -Wdiscarded-qualifiers -lz -llz4 -lzstd endif PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) pgauditlogtofile-1.8.4/README.md000066400000000000000000000154611516515416300163630ustar00rootroot00000000000000# pgAudit Log to File [pgAudit Log to File](https://github.com/fmbiete/pgauditlogtofile) is an addon to [pgAudit](https://www.pgaudit.org/) than will redirect audit log lines to an independent file, instead of using PostgreSQL server logger. This will allow us to have an audit file that we can easily rotate without polluting server logs with those messages. Audit logs in heavily used systems can grow very fast. This extension allows to automatically rotate the files based in a number of minutes. ## Build ``` make install USE_PGXS=1 ``` ## Installation - Build the extension - Install pgaudit extension - Add pgaudit to _shared_preload_libraries_ in postgresql.conf - Add pgauditlogtofile to _shared_preload_libraries_ in postgresql.conf, after pgaudit - Restart PostgreSQL to reload new shared library - Create extension in postgres database (like pgaudit we don't need to create it in all the databases) ## Test **pg_regres** ``` make installcheck ``` **Vagrant** ``` cd test vagrant plugin install vagrant-vbguest vagrant up ``` ## Signals **pgauditlogtofile** listen to multiple signals: - SIGHUP / pg_reload_conf() : reloads the configuration and triggers a complete rotation. - SIGUSR1 (against pgauditlogtofile background worker) : closes the audit log file handler in all backends. **HINT**: Use SIGUSR1 if you find inactive sessions holding file handles and you don't want to enable the auto-close feature. **ATTENTION**: pg_rotate_logfile() will not rotate or force a close/open for the audit file, because the audit file handles are hold by the backends. ## Configuration ### pgaudit.log_format Format used to write the audit records. **Scope**: System **Default**: 'csv' **Options**: csv / json **CSV Notes**: - All fields are quoted and escaped when required. - Statement and Parameters are treated as one unique value. - Empty values are printed as empty without quotes. **JSON Notes**: - Keys and values are quoted. - Values are escaped when required. ### pgaudit.log_directory Name of the directory where the audit file will be created. **Scope**: System **Default**: 'log' Empty or NULL will disable the extension and the audit logging will be done to PostgreSQL server logger. ### pgaudit.log_filename Name of the file where the audit will be written. Writing to an existing file will append the new entries. This variable can contain time patterns up to minute to allow automatic rotation. **Scope**: System **Default**: 'audit-%Y%m%d_%H%M.log' Empty or NULL will disable the extension and the audit logging will be done to PostgreSQL server logger. ### pgaudit.log_file_mode File permissions of the audit log files created. **Scope**: System **Default**: '0600' Permission changes are only applied after file rotation. Files cannot be marked as executable. ### pgaudit.log_rotation_age Number of minutes after which the audit file will be rotated. **Scope**: System **Default**: 1440 minutes (1 day) **Performance Notes**: - If _log_rotation_age < 5_ the rotation background worker will wake up every 10 seconds. - If _log_rotation_age >= 5_ the rotation background worker will wake up every 1 minute. ### pgaudit.log_connections Intercepts server log messages emited when log_connections is on **Scope**: System **Default**: off **Requires**: log_connections = on ### pgaudit.log_disconnections Intercepts server log messages emited when log_disconnections is on **Scope**: System **Default**: off **Requires**: log_disconnections = on ### pgaudit.log_autoclose_minutes **EXPERIMENTAL**: automatically closes the audit log file handler kept by a backend after N minutes of inactivity. _This features creates a background thread that will sleep in the background and close the file handler._ **Scope**: System **Default**: 0 ### pgaudit.log_execution_time Measures the execution time of each statement audited in seconds with nanoseconds precision. **Scope**: System [requires a restart] **Default**: off ### pgaudit.log_execution_memory Measures the execution memory footprint of each statement audited. _This features produces a start, end, delta and peak value._ **Scope**: System [requires a restart] **Default**: off ### pgaudit.log_compression Compress the audit log file as independent streams, the resulting file will be always bigger than writing without compression and compressing manually after rotation with an external script. **Scope**: System **Default**: off **Options**: off / gzip / lz4 / zstd **Performance**: **lz4** is recommended for high load as it provides the best performance (even faster than uncompressed). **zstd** offers a good balance between speed and compression ratio. #### Performance Benchmark (Transaction per second): Measured with pgbench and log_compression_level = 0 (default library behavior): ``` pgbench --client=10 --jobs=2 --time=60 --select-only ``` _Don't consider the tps, your system/configuration will provide different results. The impact ratio is what you should be interested in._ | log_compression value | impact ratio (% degradation) | tps (without initial connection time) | :-----------------------| ---------------------------: | ------------------------------------: | off | 0% | 113700.958346 | gzip | 18.77% | 92387.953737 | lz4 | -0.96% | 114795.996736 | zstd | 4.40% | 108699.232205 ### pgaudit.log_compression_level Specifies the compression level for the selected compression algorithm. **Scope**: System **Default**: 0 (Default library behavior) **Range**: 0 to 22 ## pgAudit Log To File - Record format ``` CREATE FOREIGN TABLE pgauditlogtofile_extern ( ----fields from postgresql session---- log_time timestamptz(9) NULL, user_name text NULL, database_name text NULL, process_id int4 NULL, remote_client text NULL, remote_port text NULL, session_id text NULL, session_line_num int8 NULL, command_tag text NULL, virtual_transaction_id text NULL, transaction_id int8 NULL, sql_state_code text NULL, -----fields from pgaudit record------- audit_type text NULL, statement_id text NULL, substatement_id text NULL, "class" text NULL, command text NULL, object_type text NULL, object_name text NULL, statement_with_parameters text NULL, ----additional fields-------- detail text NULL, hint text NULL, internal_query text NULL, internal_query_pos int4 NULL, context text NULL, debug_query text NULL, cursor_pos int4 NULL, function_name text NULL, filename_linenum text NULL, application_name text NULL, execution_time_start timestamptz(9) NULL, execution_time_end timestamptz(9) NULL, execution_time double NULL, execution_memory_start double NULL, execution_memory_end double NULL, execution_memory_peak double NULL, execution_memory_delta double NULL ) SERVER your_server OPTIONS (filename 'audit_log.csv', format 'csv'); ``` pgauditlogtofile-1.8.4/logtofile.c000066400000000000000000000166651516515416300172430ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile.c * Main entry point for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile.h" #include "logtofile_bgw.h" #include "logtofile_connect.h" #include "logtofile_execution_hook.h" #include "logtofile_guc.h" #include "logtofile_log.h" #include "logtofile_shmem.h" #include "logtofile_vars.h" #include /* these are always necessary for a bgworker */ #include #include #include #include #include #include #include #include #include #include #include #include static const struct config_enum_entry format_options[] = { {"csv", PGAUDIT_LTF_FORMAT_CSV, false}, {"json", PGAUDIT_LTF_FORMAT_JSON, false}, {NULL, 0, false}}; static const struct config_enum_entry compression_options[] = { {"off", PGAUDIT_LTF_COMPRESSION_OFF, false}, {"gzip", PGAUDIT_LTF_COMPRESSION_GZIP, false}, {"lz4", PGAUDIT_LTF_COMPRESSION_LZ4, false}, {"zstd", PGAUDIT_LTF_COMPRESSION_ZSTD, false}, {NULL, 0, false}}; /** * @brief Main entry point for the extension * @param void * @return void */ void _PG_init(void) { BackgroundWorker worker; if (!process_shared_preload_libraries_in_progress) { ereport(ERROR, (errmsg("pgauditlogtofile can only be loaded via shared_preload_libraries"), errhint("Add pgauditlogtofile to the shared_preload_libraries configuration variable in postgresql.conf."))); } PG_TRY(); { pgaudit_ltf_memory_context = AllocSetContextCreate(TopMemoryContext, "pgauditlogtofile context", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); } PG_CATCH(); { FlushErrorState(); ereport(FATAL, (errmsg("could not create pgauditlogtofile memory context"))); } PG_END_TRY(); /* guc variables */ DefineCustomStringVariable( "pgaudit.log_directory", "Directory where to spool log data", NULL, &guc_pgaudit_ltf_log_directory, "log", PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, PgAuditLogToFile_guc_check_directory, NULL, NULL); DefineCustomStringVariable( "pgaudit.log_filename", "Filename with time patterns (up to minutes) where to spool audit data", NULL, &guc_pgaudit_ltf_log_filename, "audit-%Y%m%d_%H%M.log", PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, PgAuditLogToFile_guc_check_filename, NULL, NULL); DefineCustomIntVariable( "pgaudit.log_file_mode", "Sets the file permissions for log files", NULL, &guc_pgaudit_ltf_log_file_mode, 0600, 0000, 0666, PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, NULL, PgAuditLogToFile_guc_show_file_mode); DefineCustomIntVariable( "pgaudit.log_rotation_age", "Automatic spool file rotation will occur after N minutes", NULL, &guc_pgaudit_ltf_log_rotation_age, HOURS_PER_DAY * MINS_PER_HOUR, 0, INT_MAX / SECS_PER_MINUTE, PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_UNIT_MIN | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomBoolVariable( "pgaudit.log_connections", "Intercepts log_connections messages", NULL, &guc_pgaudit_ltf_log_connections, false, PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomBoolVariable( "pgaudit.log_disconnections", "Intercepts log_disconnections messages", NULL, &guc_pgaudit_ltf_log_disconnections, false, PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomIntVariable( "pgaudit.log_autoclose_minutes", "Automatic spool file closure by backend after N minutes of inactivity", NULL, &guc_pgaudit_ltf_auto_close_minutes, 0, 0, INT_MAX / MINS_PER_HOUR, PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_UNIT_MIN | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomEnumVariable( "pgaudit.log_format", "Format of the audit data (csv or json)", NULL, &guc_pgaudit_ltf_log_format, PGAUDIT_LTF_FORMAT_CSV, format_options, PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomBoolVariable( "pgaudit.log_execution_time", "Logs the execution time of each statement.", NULL, &guc_pgaudit_ltf_log_execution_time, false, PGC_POSTMASTER, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomBoolVariable( "pgaudit.log_execution_memory", "Logs the memory usage of each statement.", NULL, &guc_pgaudit_ltf_log_execution_memory, false, PGC_POSTMASTER, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomEnumVariable( "pgaudit.log_compression", "Compress the audit log file (off, gzip, lz4, zstd).", NULL, &guc_pgaudit_ltf_log_compression, PGAUDIT_LTF_COMPRESSION_OFF, compression_options, PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomIntVariable( "pgaudit.log_compression_level", "Compression level (0=default, gzip: 1-9, lz4: 1-12, zstd: 1-22).", NULL, &guc_pgaudit_ltf_log_compression_level, 0, 0, 22, PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); EmitWarningsOnPlaceholders("pgauditlogtofile"); /* background worker */ MemSet(&worker, 0, sizeof(BackgroundWorker)); worker.bgw_flags = BGWORKER_SHMEM_ACCESS; worker.bgw_start_time = BgWorkerStart_ConsistentState; worker.bgw_restart_time = 1; worker.bgw_main_arg = Int32GetDatum(0); worker.bgw_notify_pid = 0; sprintf(worker.bgw_library_name, "pgauditlogtofile"); sprintf(worker.bgw_function_name, "PgAuditLogToFileMain"); snprintf(worker.bgw_name, BGW_MAXLEN, "pgauditlogtofile launcher"); RegisterBackgroundWorker(&worker); /* Executor hooks */ pgaudit_ltf_prev_ExecutorStart = ExecutorStart_hook; ExecutorStart_hook = PgAuditLogToFile_ExecutorStart_Hook; pgaudit_ltf_prev_ExecutorEnd = ExecutorEnd_hook; ExecutorEnd_hook = PgAuditLogToFile_ExecutorEnd_Hook; pgaudit_ltf_prev_ExecutorRun = ExecutorRun_hook; ExecutorRun_hook = PgAuditLogToFile_ExecutorRun_Hook; /* backend hooks */ #if (PG_VERSION_NUM >= 150000) pgaudit_ltf_prev_shmem_request_hook = shmem_request_hook; shmem_request_hook = PgAuditLogToFile_shmem_request; #else PgAuditLogToFile_shmem_request(); #endif pgaudit_ltf_prev_shmem_startup_hook = shmem_startup_hook; shmem_startup_hook = PgAuditLogToFile_shmem_startup; pgaudit_ltf_prev_emit_log_hook = emit_log_hook; emit_log_hook = PgAuditLogToFile_emit_log; } /** * @brief Extension finalization * @param void * @return void */ void _PG_fini(void) { emit_log_hook = pgaudit_ltf_prev_emit_log_hook; shmem_startup_hook = pgaudit_ltf_prev_shmem_startup_hook; ExecutorStart_hook = pgaudit_ltf_prev_ExecutorStart; ExecutorEnd_hook = pgaudit_ltf_prev_ExecutorEnd; if (pgaudit_ltf_memory_context != NULL) { MemoryContextDelete(pgaudit_ltf_memory_context); pgaudit_ltf_memory_context = NULL; } } pgauditlogtofile-1.8.4/logtofile.h000066400000000000000000000007431516515416300172360ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile.h * Main entry point for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_H_ #define _LOGTOFILE_H_ void _PG_init(void); void _PG_fini(void); #endif pgauditlogtofile-1.8.4/logtofile_autoclose.c000066400000000000000000000032161516515416300213050ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_autoclose.c * Autoclose thread for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_autoclose.h" #include "logtofile_vars.h" #include #include #include /** * @brief Main thread function to close the audit log file after a certain time * @param arg: autoclose_thread_status_debug - used to debug the thread status * @return void */ void *PgAuditLogToFile_autoclose_run(void *arg) { time_t ts_now; time_t last_active; double diff_mins; int *autoclose_thread_status_debug; /* don't use ereport here, use this flag to identify the position */ autoclose_thread_status_debug = (int *)arg; while (1) { sleep(1 * SECS_PER_MINUTE); ts_now = time(NULL); last_active = (time_t)pgaudit_ltf_autoclose_active_ts; diff_mins = difftime(ts_now, last_active) / 60.0; if (diff_mins >= (double)guc_pgaudit_ltf_auto_close_minutes) { int fd = pgaudit_ltf_file_handler; if (fd != -1) { pgaudit_ltf_file_handler = -1; close(fd); } *autoclose_thread_status_debug = 3; // file closed break; } else { *autoclose_thread_status_debug = 2; // file recently used } } // clear the flag to allow another thread creation pg_atomic_clear_flag(&pgaudit_ltf_autoclose_flag_thread); pthread_exit(NULL); } pgauditlogtofile-1.8.4/logtofile_autoclose.h000066400000000000000000000010461516515416300213110ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_autoclose.h * Autoclose thread for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_AUTOCLOSE_H_ #define _LOGTOFILE_AUTOCLOSE_H_ #include extern void *PgAuditLogToFile_autoclose_run(void *arg); #endif pgauditlogtofile-1.8.4/logtofile_bgw.c000066400000000000000000000146511516515416300200730ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_bgw.c * Background worker for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_bgw.h" /* these are always necessary for a bgworker */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "logtofile_filename.h" #include "logtofile_shmem.h" #include "logtofile_vars.h" /* * Wait events for pg_stat_activity visibility. */ static uint32 pgaudit_wait_main = 0; static uint32 pgaudit_wait_signal = 0; static uint32 pgaudit_wait_config = 0; static uint32 pgaudit_wait_rotate = 0; /* global settings */ /* flags set by signal handlers */ static volatile sig_atomic_t got_sigterm = false; static volatile sig_atomic_t got_sigusr1 = false; /* forward declaration private functions */ static void pgauditlogtofile_sigterm(SIGNAL_ARGS); static void pgauditlogtofile_sigusr1(SIGNAL_ARGS); static void pgauditlogtofile_rotate_file(uint32 wait_event_info); /** * @brief Main entry point for the background worker * @param arg: unused * @return void */ void PgAuditLogToFileMain(Datum arg) { int sleep_ms = SECS_PER_MINUTE * 1000; MemoryContext PgAuditLogToFileContext = NULL; /* Register custom wait events for visibility in pg_stat_activity */ if (pgaudit_wait_main == 0) { #if (PG_VERSION_NUM >= 170000) pgaudit_wait_main = WaitEventExtensionNew("PgAuditLogToFileMain"); pgaudit_wait_signal = WaitEventExtensionNew("PgAuditLogToFileSignal"); pgaudit_wait_config = WaitEventExtensionNew("PgAuditLogToFileConfig"); pgaudit_wait_rotate = WaitEventExtensionNew("PgAuditLogToFileRotate"); #else /* custom wait events for extensions were still not available */ pgaudit_wait_main = PG_WAIT_EXTENSION; pgaudit_wait_signal = PG_WAIT_EXTENSION; pgaudit_wait_config = PG_WAIT_EXTENSION; pgaudit_wait_rotate = PG_WAIT_EXTENSION; #endif } pqsignal(SIGHUP, SignalHandlerForConfigReload); pqsignal(SIGINT, SIG_IGN); pqsignal(SIGTERM, pgauditlogtofile_sigterm); pqsignal(SIGUSR1, pgauditlogtofile_sigusr1); BackgroundWorkerUnblockSignals(); pgstat_report_appname("pgauditlogtofile launcher"); PgAuditLogToFileContext = AllocSetContextCreate(pgaudit_ltf_memory_context, "pgauditlogtofile loop context", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); ereport(LOG_SERVER_ONLY, (errmsg("pgauditlogtofile worker started"))); MemoryContextSwitchTo(PgAuditLogToFileContext); while (1) { int rc; CHECK_FOR_INTERRUPTS(); /* Propagate SIGUSR1 from the bwg to all backends to force an audit file descriptor closure */ if (got_sigusr1) { int i; PGPROC *proc; got_sigusr1 = false; pgstat_report_wait_start(pgaudit_wait_signal); ereport(LOG, (errmsg("pgauditlogtofile bgw: received SIGUSR1, propagating to backends"))); /* * Acquire a shared lock on the ProcArray to safely iterate * through active backends. */ LWLockAcquire(ProcArrayLock, LW_SHARED); for (i = 0; i < ProcGlobal->allProcCount; i++) { proc = &ProcGlobal->allProcs[i]; /* Don't signal yourself (the background worker) */ if (proc->pid != MyProcPid && proc->pid != 0) { /* Send the actual signal via the OS */ kill(proc->pid, SIGUSR1); } } LWLockRelease(ProcArrayLock); pgstat_report_wait_end(); } if (guc_pgaudit_ltf_log_rotation_age > 0 && guc_pgaudit_ltf_log_rotation_age < 5) { // very small rotation, wake up frequently - this has a performance impact, // but rotation every a few minutes should only be done for testing sleep_ms = 10000; } else { sleep_ms = SECS_PER_MINUTE * 1000; } ereport(DEBUG5, (errmsg("pgauditlogtofile bgw loop"))); if (ConfigReloadPending) { ConfigReloadPending = false; ereport(DEBUG3, (errmsg("pgauditlogtofile bgw loop reload cfg"))); ProcessConfigFile(PGC_SIGHUP); pgauditlogtofile_rotate_file(pgaudit_wait_config); } else if (PgAuditLogToFile_needs_rotate_file()) { ereport(DEBUG3, (errmsg("pgauditlogtofile bgw loop needs rotation %s", pgaudit_ltf_shm->filename))); pgauditlogtofile_rotate_file(pgaudit_wait_rotate); } /* shutdown if requested */ if (got_sigterm) break; rc = WaitLatch(&MyProc->procLatch, WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH, sleep_ms, pgaudit_wait_main); if (rc & WL_POSTMASTER_DEATH) proc_exit(1); ResetLatch(&MyProc->procLatch); MemoryContextReset(PgAuditLogToFileContext); } ereport(LOG_SERVER_ONLY, (errmsg("pgauditlogtofile worker shutting down"))); proc_exit(0); } /* private functions */ /** * @brief Signal handler for SIGUSR1 * @param signal_arg: signal number * @return void */ static void pgauditlogtofile_sigusr1(SIGNAL_ARGS) { int save_errno = errno; got_sigusr1 = true; if (MyProc) SetLatch(&MyProc->procLatch); /* call standard handler to process other interrupts that are reusing the same signal */ procsignal_sigusr1_handler(postgres_signal_arg); errno = save_errno; } /** * @brief Signal handler for SIGHUP * @param signal_arg: signal number * @return void */ static void pgauditlogtofile_sigterm(SIGNAL_ARGS) { int save_errno = errno; got_sigterm = true; if (MyProc != NULL) SetLatch(&MyProc->procLatch); errno = save_errno; } /** * @brief Performs the actual log file rotation and cache advice. * @param wait_event_info: wait event to report during rotation */ static void pgauditlogtofile_rotate_file(uint32 wait_event_info) { pgstat_report_wait_start(wait_event_info); PgAuditLogToFile_calculate_current_filename(); PgAuditLogToFile_set_next_rotation_time(); pgstat_report_wait_end(); }pgauditlogtofile-1.8.4/logtofile_bgw.h000066400000000000000000000010261516515416300200700ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_bgw.h * Background worker for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_BGW_H_ #define _LOGTOFILE_BGW_H_ #include extern PGDLLEXPORT void PgAuditLogToFileMain(Datum arg); #endif pgauditlogtofile-1.8.4/logtofile_connect.c000066400000000000000000000033751516515416300207460ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_connect.c * Functions to parse connect and disconnect messages * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_connect.h" /** * @brief From a list of messages, optionally translated, get the unique prefixes * @param messages: list of messages * @param num_messages: number of messages * @param num_unique: number of unique prefixes * @return char **: list of unique prefixes */ char ** PgAuditLogToFile_connect_UniquePrefixes(const char **messages, const size_t num_messages, size_t *num_unique) { char **prefixes; size_t i; size_t count = 0; /* Allocate and zero the array; PostgreSQL's palloc handles OOM via ereport */ prefixes = (char **) palloc0(num_messages * sizeof(char *)); for (i = 0; i < num_messages; i++) { const char *message; const char *pct; size_t len; size_t j; bool is_unique = true; #ifdef ENABLE_NLS message = gettext(messages[i]); #else message = messages[i]; #endif /* Find the first % to determine the prefix length */ pct = strchr(message, '%'); len = pct ? (size_t) (pct - message) : strlen(message); /* Search only within the unique prefixes found so far (packed) */ for (j = 0; j < count; j++) { if (strncmp(prefixes[j], message, len) == 0 && prefixes[j][len] == '\0') { is_unique = false; break; } } if (is_unique) prefixes[count++] = pnstrdup(message, len); } *num_unique = count; return prefixes; } pgauditlogtofile-1.8.4/logtofile_connect.h000066400000000000000000000011721516515416300207440ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_connect.h * Functions to parse connect and disconnect messages * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_CONNECT_H_ #define _LOGTOFILE_CONNECT_H_ #include extern char ** PgAuditLogToFile_connect_UniquePrefixes(const char **messages, const size_t num_messages, size_t *num_unique); #endif pgauditlogtofile-1.8.4/logtofile_csv.c000066400000000000000000000213541516515416300201050ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_csv.c * Functions to create a csv audit record * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_csv.h" #include "logtofile_string_format.h" #include "logtofile_vars.h" #include #include #include #include #include #include #include #include #include /* forward declaration private functions */ static void pgauditlogtofile_pgaudit2csv(StringInfo buf, char *line); /** * @brief Creates a csv audit record * @param buf: buffer to write the csv line * @param edata: error data * @param exclude_nchars: number of characters to exclude from the pgaudit message * @return void */ void PgAuditLogToFile_csv_audit(StringInfo buf, const ErrorData *edata, int exclude_nchars) { char formatted_log_time[FORMATTED_TS_LEN]; const char *psdisp; int displen; instr_time now_instr; double total_time; instr_time duration; Size memory_usage; /* timestamp with nanoseconds */ INSTR_TIME_SET_CURRENT(now_instr); PgAuditLogToFile_format_instr_time_nanos(now_instr, formatted_log_time, sizeof(formatted_log_time)); escape_json(buf, formatted_log_time); appendStringInfoCharMacro(buf, ','); /* username */ if (MyProcPort && MyProcPort->user_name) escape_json(buf, MyProcPort->user_name); appendStringInfoCharMacro(buf, ','); /* database name */ if (MyProcPort && MyProcPort->database_name) escape_json(buf, MyProcPort->database_name); appendStringInfoCharMacro(buf, ','); /* Process id */ appendStringInfo(buf, "\"%d\"", MyProcPid); appendStringInfoCharMacro(buf, ','); /* Remote host and port */ if (MyProcPort && MyProcPort->remote_host) { if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0') appendStringInfo(buf, "\"%s:%s\"", MyProcPort->remote_host, MyProcPort->remote_port); else escape_json(buf, MyProcPort->remote_host); } appendStringInfoCharMacro(buf, ','); /* session id - hex representation of start time . session process id */ appendStringInfo(buf, "\"%lx.%x\"", (long)MyStartTime, MyProcPid); appendStringInfoCharMacro(buf, ','); /* PS display */ psdisp = get_ps_display(&displen); if (psdisp && displen > 0) { if (exclude_nchars == 0 && strncmp(edata->message, "disconnection", 13) == 0) escape_json(buf, "disconnection"); else if (exclude_nchars == 0 && (strncmp(edata->message, "connection authenticated", 24) == 0 || strncmp(edata->message, "connection authorized", 21) == 0)) escape_json(buf, "authentication"); else escape_json(buf, psdisp); } appendStringInfoCharMacro(buf, ','); /* Virtual transaction id */ #if (PG_VERSION_NUM >= 170000) if (MyProc != NULL && MyProc->vxid.procNumber != INVALID_PROC_NUMBER) appendStringInfo(buf, "\"%d/%u\"", MyProc->vxid.procNumber, MyProc->vxid.lxid); #else if (MyProc != NULL && MyProc->backendId != InvalidBackendId) appendStringInfo(buf, "\"%d/%u\"", MyProc->backendId, MyProc->lxid); #endif appendStringInfoCharMacro(buf, ','); /* Transaction id */ appendStringInfo(buf, "\"%u\"", GetTopTransactionIdIfAny()); appendStringInfoCharMacro(buf, ','); /* SQL state code */ escape_json(buf, unpack_sql_state(edata->sqlerrcode)); appendStringInfoCharMacro(buf, ','); /* errmessage - PGAUDIT formatted text, +7 exclude "AUDIT: " prefix */ if (exclude_nchars > 0) pgauditlogtofile_pgaudit2csv(buf, edata->message + exclude_nchars); else escape_json(buf, edata->message); appendStringInfoCharMacro(buf, ','); /* errdetail or errdetail_log */ if (edata->detail_log) escape_json(buf, edata->detail_log); else if (edata->detail) escape_json(buf, edata->detail); appendStringInfoCharMacro(buf, ','); /* errhint */ if (edata->hint) escape_json(buf, edata->hint); appendStringInfoCharMacro(buf, ','); /* internal query */ if (edata->internalquery) escape_json(buf, edata->internalquery); appendStringInfoCharMacro(buf, ','); /* if printed internal query, print internal pos too */ if (edata->internalpos > 0 && edata->internalquery != NULL) appendStringInfo(buf, "\"%d\"", edata->internalpos); appendStringInfoCharMacro(buf, ','); /* errcontext */ if (edata->context) escape_json(buf, edata->context); appendStringInfoCharMacro(buf, ','); /* user query and cursor position */ if (debug_query_string != NULL && !edata->hide_stmt) { escape_json(buf, debug_query_string); appendStringInfoCharMacro(buf, ','); if (edata->cursorpos > 0) appendStringInfo(buf, "\"%d\"", edata->cursorpos); appendStringInfoCharMacro(buf, ','); } else { appendStringInfo(buf, ",,"); } /* file error location */ if (Log_error_verbosity >= PGERROR_VERBOSE) { if (edata->funcname && edata->filename) appendStringInfo(buf, "\"%s, %s:%d\"", edata->funcname, edata->filename, edata->lineno); else if (edata->filename) appendStringInfo(buf, "\"%s:%d\"", edata->filename, edata->lineno); } appendStringInfoCharMacro(buf, ','); /* application name */ if (application_name) escape_json(buf, application_name); appendStringInfoCharMacro(buf, ','); /* execution time */ if (guc_pgaudit_ltf_log_execution_time && !INSTR_TIME_IS_ZERO(pgaudit_ltf_statement_start_time) && !INSTR_TIME_IS_ZERO(pgaudit_ltf_statement_end_time)) { /* start time */ PgAuditLogToFile_format_instr_time_nanos(pgaudit_ltf_statement_start_time, formatted_log_time, sizeof(formatted_log_time)); escape_json(buf, formatted_log_time); appendStringInfoCharMacro(buf, ','); /* end time */ PgAuditLogToFile_format_instr_time_nanos(pgaudit_ltf_statement_end_time, formatted_log_time, sizeof(formatted_log_time)); escape_json(buf, formatted_log_time); appendStringInfoCharMacro(buf, ','); /* execution time */ duration = pgaudit_ltf_statement_end_time; INSTR_TIME_SUBTRACT(duration, pgaudit_ltf_statement_start_time); total_time = INSTR_TIME_GET_DOUBLE(duration); appendStringInfo(buf, "\"%.9f\"", total_time); appendStringInfoCharMacro(buf, ','); /* Reset timing variables after logging */ INSTR_TIME_SET_ZERO(pgaudit_ltf_statement_start_time); INSTR_TIME_SET_ZERO(pgaudit_ltf_statement_end_time); } else { appendStringInfo(buf, ",,,"); } /* memory usage */ if (guc_pgaudit_ltf_log_execution_memory && pgaudit_ltf_statement_memory_start > 0 && pgaudit_ltf_statement_memory_end > 0) { memory_usage = pgaudit_ltf_statement_memory_end - pgaudit_ltf_statement_memory_start; appendStringInfo(buf, "\"%ld\",\"%ld\",\"%ld\",\"%ld\"", (long)pgaudit_ltf_statement_memory_start, (long)pgaudit_ltf_statement_memory_end, (long)pgaudit_ltf_statement_memory_peak, (long)(memory_usage < 0 ? 0 : memory_usage)); /* Reset memory variables */ pgaudit_ltf_statement_memory_start = 0; pgaudit_ltf_statement_memory_end = 0; } else { appendStringInfo(buf, ",,,"); } appendStringInfoCharMacro(buf, '\n'); } /* private functions */ /** * @brief Split and escapes each piece on pgaudit original message and writes it as CSV value. * @param buf Where to write * @param line original pgaudit message, it's modified in this function */ static void pgauditlogtofile_pgaudit2csv(StringInfo buf, char *line) { char *token; /* 1. AUDIT_TYPE */ token = strsep(&line, ","); if (token) escape_json(buf, token); appendStringInfoCharMacro(buf, ','); /* 2. STATEMENT_ID */ token = strsep(&line, ","); if (token) escape_json(buf, token); appendStringInfoCharMacro(buf, ','); /* 3. SUBSTATEMENT_ID */ token = strsep(&line, ","); if (token) escape_json(buf, token); appendStringInfoCharMacro(buf, ','); /* 4. CLASS */ token = strsep(&line, ","); if (token) escape_json(buf, token); appendStringInfoCharMacro(buf, ','); /* 5. COMMAND */ token = strsep(&line, ","); if (token) escape_json(buf, token); appendStringInfoCharMacro(buf, ','); /* 6. OBJECT_TYPE */ token = strsep(&line, ","); if (token) escape_json(buf, token); appendStringInfoCharMacro(buf, ','); /* 7. OBJECT_NAME */ token = strsep(&line, ","); if (token) escape_json(buf, token); appendStringInfoCharMacro(buf, ','); /* 8. Statement and parameters (the rest of the line) */ if (line && *line != '\0') escape_json(buf, line + (*line == ',' ? 1 : 0)); } pgauditlogtofile-1.8.4/logtofile_csv.h000066400000000000000000000011441516515416300201050ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_csv.h * Functions to create a csv audit record * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_CVS_H_ #define _LOGTOFILE_CVS_H_ #include #include extern void PgAuditLogToFile_csv_audit(StringInfo buf, const ErrorData *edata, int exclude_nchars); #endif pgauditlogtofile-1.8.4/logtofile_errordata.c000066400000000000000000000073771516515416300213060ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_errordata.c * Functions to work with ErrorData struct * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_errordata.h" #include "logtofile_vars.h" #include /** * @brief Copy ErrorData object [derived of CopyErrorData] * @param edata ErrorData object to duplicate */ void PgAuditLogToFile_CopyPendingErrorData(ErrorData *edata) { MemoryContext oldcontext; /* Keep the copied ErrorData in the extension-owned memory context. */ oldcontext = MemoryContextSwitchTo(pgaudit_ltf_memory_context); /* Free any previous entry to avoid leaks in the extension context. */ if (pgaudit_ltf_pending_audit.edata != NULL) PgAuditLogToFile_FreePendingErrorData(); pgaudit_ltf_pending_audit.edata = palloc_object(ErrorData); memcpy(pgaudit_ltf_pending_audit.edata, edata, sizeof(ErrorData)); /* * Make copies of separately-allocated strings. Note that we copy even * theoretically-constant strings such as filename. This is because those * could point into JIT-created code segments that might get unloaded at * transaction cleanup. In some cases we need the copied ErrorData to * survive transaction boundaries, so we'd better copy those strings too. */ if (edata->filename) pgaudit_ltf_pending_audit.edata->filename = pstrdup(edata->filename); if (edata->funcname) pgaudit_ltf_pending_audit.edata->funcname = pstrdup(edata->funcname); if (edata->domain) pgaudit_ltf_pending_audit.edata->domain = pstrdup(edata->domain); if (edata->context_domain) pgaudit_ltf_pending_audit.edata->context_domain = pstrdup(edata->context_domain); if (edata->message) pgaudit_ltf_pending_audit.edata->message = pstrdup(edata->message); if (edata->detail) pgaudit_ltf_pending_audit.edata->detail = pstrdup(edata->detail); if (edata->detail_log) pgaudit_ltf_pending_audit.edata->detail_log = pstrdup(edata->detail_log); if (edata->hint) pgaudit_ltf_pending_audit.edata->hint = pstrdup(edata->hint); if (edata->context) pgaudit_ltf_pending_audit.edata->context = pstrdup(edata->context); if (edata->backtrace) pgaudit_ltf_pending_audit.edata->backtrace = pstrdup(edata->backtrace); if (edata->message_id) pgaudit_ltf_pending_audit.edata->message_id = pstrdup(edata->message_id); if (edata->schema_name) pgaudit_ltf_pending_audit.edata->schema_name = pstrdup(edata->schema_name); if (edata->table_name) pgaudit_ltf_pending_audit.edata->table_name = pstrdup(edata->table_name); if (edata->column_name) pgaudit_ltf_pending_audit.edata->column_name = pstrdup(edata->column_name); if (edata->datatype_name) pgaudit_ltf_pending_audit.edata->datatype_name = pstrdup(edata->datatype_name); if (edata->constraint_name) pgaudit_ltf_pending_audit.edata->constraint_name = pstrdup(edata->constraint_name); if (edata->internalquery) pgaudit_ltf_pending_audit.edata->internalquery = pstrdup(edata->internalquery); /* Ensure assoc_context points to where we actually put it */ pgaudit_ltf_pending_audit.edata->assoc_context = pgaudit_ltf_memory_context; MemoryContextSwitchTo(oldcontext); /* mark the record as pending */ pgaudit_ltf_pending_audit.active = true; } /** * @brief Free ErrorData object [calls FreeErrorData] * @return void */ void PgAuditLogToFile_FreePendingErrorData(void) { pgaudit_ltf_pending_audit.active = false; if (pgaudit_ltf_pending_audit.edata != NULL) { FreeErrorData(pgaudit_ltf_pending_audit.edata); pgaudit_ltf_pending_audit.edata = NULL; } }pgauditlogtofile-1.8.4/logtofile_errordata.h000066400000000000000000000012071516515416300212750ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_errordata.h * Functions to work with ErrorData struct * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_ERRORDATA_H_ #define _LOGTOFILE_ERRORDATA_H_ #include #include extern void PgAuditLogToFile_CopyPendingErrorData(ErrorData *edata); extern void PgAuditLogToFile_FreePendingErrorData(void); #endifpgauditlogtofile-1.8.4/logtofile_execution_hook.c000066400000000000000000000073421516515416300223360ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_execution_hook.c * Functions to add Execution Hooks wrappers * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_execution_hook.h" #include "logtofile_execution_memory.h" #include "logtofile_execution_time.h" #include "logtofile_vars.h" #include "logtofile_signal_handler.h" #include "logtofile_log.h" #include #include #include static bool pgaudit_ltf_handler_setup = false; /** * @brief Hook for ExecutorStart to setup signal handlers and stats capture * @param queryDesc query descriptor * @param eflags executor flags */ void PgAuditLogToFile_ExecutorStart_Hook(QueryDesc *queryDesc, int eflags) { if (!pgaudit_ltf_handler_setup) { #if (PG_VERSION_NUM >= 180000) /* setup a signal handler for SIGUSR1 in the backend, and hope we don't lose another */ /* we will always call the default postgresql signal handler */ pqsignal(SIGUSR1, PgAuditLogToFile_SIGUSR1); #else /* setup a signal handler for SIGUSR1 in the backend, and save the existing */ pgaudit_ltf_prev_sigusr1_handler = pqsignal(SIGUSR1, PgAuditLogToFile_SIGUSR1); #endif pgaudit_ltf_handler_setup = true; /* only once */ } if (pgaudit_ltf_prev_ExecutorStart) pgaudit_ltf_prev_ExecutorStart(queryDesc, eflags); else standard_ExecutorStart(queryDesc, eflags); if (guc_pgaudit_ltf_log_execution_time) PgAuditLogToFile_ExecutorStart_Time(queryDesc, eflags); if (guc_pgaudit_ltf_log_execution_memory) PgAuditLogToFile_ExecutorStart_Memory(queryDesc, eflags); } /** * @brief Hook for ExecutorEnd to finalize stats and flush pending logs * @param queryDesc query descriptor */ void PgAuditLogToFile_ExecutorEnd_Hook(QueryDesc *queryDesc) { if (guc_pgaudit_ltf_log_execution_time) PgAuditLogToFile_ExecutorEnd_Time(queryDesc); if (guc_pgaudit_ltf_log_execution_memory) PgAuditLogToFile_ExecutorEnd_Memory(queryDesc); /* Flush buffered audit records now that we have the stats */ PgAuditLogToFile_Flush_Pending(); /* Reset timing and memory variables to 0 so unrelated logs (like disconnection) don't use them */ if (guc_pgaudit_ltf_log_execution_time) { INSTR_TIME_SET_ZERO(pgaudit_ltf_statement_start_time); INSTR_TIME_SET_ZERO(pgaudit_ltf_statement_end_time); } if (guc_pgaudit_ltf_log_execution_memory) { pgaudit_ltf_statement_memory_start = 0; pgaudit_ltf_statement_memory_end = 0; pgaudit_ltf_statement_memory_peak = 0; } if (pgaudit_ltf_prev_ExecutorEnd) pgaudit_ltf_prev_ExecutorEnd(queryDesc); else standard_ExecutorEnd(queryDesc); } #if (PG_VERSION_NUM >= 180000) #define EX_RUN_ARGS queryDesc, direction, count void PgAuditLogToFile_ExecutorRun_Hook(QueryDesc *queryDesc, ScanDirection direction, uint64 count) #else #define EX_RUN_ARGS queryDesc, direction, count, execute_once /** * @brief Hook for ExecutorRun to track peak memory usage * @param queryDesc query descriptor * @param direction scan direction * @param count tuple count * @param execute_once execution flag */ void PgAuditLogToFile_ExecutorRun_Hook(QueryDesc *queryDesc, ScanDirection direction, uint64 count, bool execute_once) #endif { if (guc_pgaudit_ltf_log_execution_memory) PgAuditLogToFile_ExecutorRun_Memory(EX_RUN_ARGS); if (pgaudit_ltf_prev_ExecutorRun) pgaudit_ltf_prev_ExecutorRun(EX_RUN_ARGS); else standard_ExecutorRun(EX_RUN_ARGS); if (guc_pgaudit_ltf_log_execution_memory) PgAuditLogToFile_ExecutorRun_Memory(EX_RUN_ARGS); }pgauditlogtofile-1.8.4/logtofile_execution_hook.h000066400000000000000000000017211516515416300223360ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_execution_hook.h * Functions to add Execution Hooks wrappers * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_EXECUTION_HOOK_H_ #define _LOGTOFILE_EXECUTION_HOOK_H_ #include #include extern void PgAuditLogToFile_ExecutorStart_Hook(QueryDesc *queryDesc, int eflags); extern void PgAuditLogToFile_ExecutorEnd_Hook(QueryDesc *queryDesc); #if (PG_VERSION_NUM >= 180000) extern void PgAuditLogToFile_ExecutorRun_Hook(QueryDesc *queryDesc, ScanDirection direction, uint64 count); #else extern void PgAuditLogToFile_ExecutorRun_Hook(QueryDesc *queryDesc, ScanDirection direction, uint64 count, bool execute_once); #endif #endif pgauditlogtofile-1.8.4/logtofile_execution_memory.c000066400000000000000000000066411516515416300227070ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_execution_memory.c * Partial hooks to measure memory footprint of execution * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_execution_memory.h" #include "logtofile_vars.h" #include /* forward declaration private functions */ inline static Size pgauditlogtofile_MemoryContextTotalAllocated(MemoryContext ctx) __attribute__((always_inline)); inline static MemoryContext pgauditlogtofile_get_query_memory_context(QueryDesc *queryDesc) __attribute__((always_inline)); inline static void pgauditlogtofile_update_peak_memory(Size current) __attribute__((always_inline)); /** * @brief ExecutorStart hook to record the memory usage at the start of a statement. * @param queryDesc * @param eflags */ void PgAuditLogToFile_ExecutorStart_Memory(QueryDesc *queryDesc, __attribute__((unused)) int eflags) { MemoryContext ctx = pgauditlogtofile_get_query_memory_context(queryDesc); pgaudit_ltf_statement_memory_start = pgauditlogtofile_MemoryContextTotalAllocated(ctx); pgaudit_ltf_statement_memory_peak = pgaudit_ltf_statement_memory_start; pgaudit_ltf_statement_memory_end = 0; } /** * @brief ExecutorEnd hook to calculate and log the statement memory usage. * @param queryDesc */ void PgAuditLogToFile_ExecutorEnd_Memory(QueryDesc *queryDesc) { MemoryContext ctx = pgauditlogtofile_get_query_memory_context(queryDesc); pgaudit_ltf_statement_memory_end = pgauditlogtofile_MemoryContextTotalAllocated(ctx); pgauditlogtofile_update_peak_memory(pgaudit_ltf_statement_memory_end); } /** * @brief ExecutorRun hook to capture peak of memory usage during run * @param queryDesc */ #if (PG_VERSION_NUM >= 180000) void PgAuditLogToFile_ExecutorRun_Memory(QueryDesc *queryDesc, __attribute__((unused)) ScanDirection direction, __attribute__((unused)) uint64 count) #else void PgAuditLogToFile_ExecutorRun_Memory(QueryDesc *queryDesc, __attribute__((unused)) ScanDirection direction, __attribute__((unused)) uint64 count, __attribute__((unused)) bool execute_once) #endif { MemoryContext ctx = pgauditlogtofile_get_query_memory_context(queryDesc); Size current = pgauditlogtofile_MemoryContextTotalAllocated(ctx); pgauditlogtofile_update_peak_memory(current); } /* private functions */ /** * @brief Obtains memory allocated * @param ctx * @return Size */ static Size pgauditlogtofile_MemoryContextTotalAllocated(MemoryContext ctx) { if (ctx == NULL) return 0; return MemoryContextMemAllocated(ctx, true); } /** * @brief Obtains the query context * @param queryDesc * @return MemoryContext */ static MemoryContext pgauditlogtofile_get_query_memory_context(QueryDesc *queryDesc) { return (queryDesc && queryDesc->estate) ? queryDesc->estate->es_query_cxt : NULL; } /** * @brief Update the peak memory value if required * @param current */ static void pgauditlogtofile_update_peak_memory(Size current) { if (current > pgaudit_ltf_statement_memory_peak) pgaudit_ltf_statement_memory_peak = current; } pgauditlogtofile-1.8.4/logtofile_execution_memory.h000066400000000000000000000017541516515416300227140ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_execution_memory.h * Partial hooks to measure memory footprint of execution * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_EXECUTION_MEMORY_H_ #define _LOGTOFILE_EXECUTION_MEMORY_H_ #include #include extern void PgAuditLogToFile_ExecutorStart_Memory(QueryDesc *queryDesc, int eflags); extern void PgAuditLogToFile_ExecutorEnd_Memory(QueryDesc *queryDesc); #if (PG_VERSION_NUM >= 180000) extern void PgAuditLogToFile_ExecutorRun_Memory(QueryDesc *queryDesc, ScanDirection direction, uint64 count); #else extern void PgAuditLogToFile_ExecutorRun_Memory(QueryDesc *queryDesc, ScanDirection direction, uint64 count, bool execute_once); #endif #endif pgauditlogtofile-1.8.4/logtofile_execution_time.c000066400000000000000000000017651516515416300223370ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_execution_time.c * Partial hooks to measure execution time * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_execution_time.h" #include "logtofile_vars.h" /** * @brief ExecutorStart hook to record the start time of a statement. * @param queryDesc * @param eflags */ void PgAuditLogToFile_ExecutorStart_Time(QueryDesc *queryDesc, int eflags) { INSTR_TIME_SET_CURRENT(pgaudit_ltf_statement_start_time); INSTR_TIME_SET_ZERO(pgaudit_ltf_statement_end_time); } /** * @brief ExecutorEnd hook to calculate and log the statement execution time. * @param queryDesc */ void PgAuditLogToFile_ExecutorEnd_Time(QueryDesc *queryDesc) { INSTR_TIME_SET_CURRENT(pgaudit_ltf_statement_end_time); } pgauditlogtofile-1.8.4/logtofile_execution_time.h000066400000000000000000000012701516515416300223330ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_execution_time.h * Partial hooks to measure execution time * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_EXECUTION_TIME_H_ #define _LOGTOFILE_EXECUTION_TIME_H_ #include #include extern void PgAuditLogToFile_ExecutorStart_Time(QueryDesc *queryDesc, int eflags); extern void PgAuditLogToFile_ExecutorEnd_Time(QueryDesc *queryDesc); #endif pgauditlogtofile-1.8.4/logtofile_filename.c000066400000000000000000000055741516515416300211000ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_filename.c * Functions to calculate the filename of the log file * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_filename.h" #include #include #include #include "logtofile_vars.h" /* forward declaration private functions */ static char *pgauditlogtofile_tm2filename(const struct pg_tm *tm); /** * @brief Calculate the current filename of the log file * @param void * @return char * - the current filename */ char * PgAuditLogToFile_current_filename(void) { pg_time_t timet = timestamptz_to_time_t(GetCurrentTimestamp()); struct pg_tm *tm = pg_localtime(&timet, log_timezone); return pgauditlogtofile_tm2filename(tm); } /** * @brief Set the next rotation time * @param void * @return void * @note Copied from src/backend/postmaster/syslogger.c */ void PgAuditLogToFile_set_next_rotation_time(void) { pg_time_t now; struct pg_tm *tm; int rotinterval; /* nothing to do if time-based rotation is disabled */ if (guc_pgaudit_ltf_log_rotation_age < 1) return; /* * The requirements here are to choose the next time > now that is a * "multiple" of the log rotation interval. "Multiple" can be interpreted * fairly loosely. In this version we align to log_timezone rather than * GMT. */ rotinterval = guc_pgaudit_ltf_log_rotation_age * SECS_PER_MINUTE; /* convert to seconds */ now = (pg_time_t)time(NULL); tm = pg_localtime(&now, log_timezone); now += tm->tm_gmtoff; now -= now % rotinterval; now += rotinterval; now -= tm->tm_gmtoff; LWLockAcquire(&pgaudit_ltf_shm->lock, LW_EXCLUSIVE); pgaudit_ltf_shm->next_rotation_time = now; LWLockRelease(&pgaudit_ltf_shm->lock); } /* private functions */ /** * @brief Convert a pg_tm structure to a filename * @param tm - the pg_tm structure * @return char * - the filename */ static char * pgauditlogtofile_tm2filename(const struct pg_tm *tm) { char *filename = NULL; int len; filename = palloc(MAXPGPATH); /* Write directory prefix */ pg_snprintf(filename, MAXPGPATH, "%s/", guc_pgaudit_ltf_log_directory); len = strlen(filename); /* Append formatted timestamp-based filename */ pg_strftime(filename + len, MAXPGPATH - len, guc_pgaudit_ltf_log_filename, tm); switch (guc_pgaudit_ltf_log_compression) { case PGAUDIT_LTF_COMPRESSION_GZIP: strlcat(filename, ".gz", MAXPGPATH); break; case PGAUDIT_LTF_COMPRESSION_LZ4: strlcat(filename, ".lz4", MAXPGPATH); break; case PGAUDIT_LTF_COMPRESSION_ZSTD: strlcat(filename, ".zst", MAXPGPATH); break; default: break; } return filename; } pgauditlogtofile-1.8.4/logtofile_filename.h000066400000000000000000000012121516515416300210660ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_filename.c * Functions to calculate the filename of the log file * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_FILENAME_H_ #define _LOGTOFILE_FILENAME_H_ #include "postgres.h" extern char *PgAuditLogToFile_current_filename(void); extern void PgAuditLogToFile_set_next_rotation_time(void); #endif // _LOGTOFILE_FILENAME_H_pgauditlogtofile-1.8.4/logtofile_guc.c000066400000000000000000000035571516515416300200750ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_guc.c * GUC variables for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_guc.h" #include #include #include "logtofile_shmem.h" #include "logtofile_vars.h" /** * @brief GUC Callback pgaudit.log_directory check path * @param newval: new value * @param extra: extra * @param source: source * @return bool: true if path is valid */ bool PgAuditLogToFile_guc_check_directory(char **newval, void **extra, GucSource source) { /* * Since canonicalize_path never enlarges the string, we can just modify * newval in-place. */ canonicalize_path(*newval); return true; } /** * @brief GUC Callback pgaudit.log_filename check value * @param newval: new value * @param extra: extra * @param source: source * @return bool: true if filename is valid */ bool PgAuditLogToFile_guc_check_filename(char **newval, void **extra, GucSource source) { size_t len = strlen(*newval); if ((len > 3 && strcmp(*newval + len - 3, ".gz") == 0) || (len > 4 && strcmp(*newval + len - 4, ".lz4") == 0) || (len > 4 && strcmp(*newval + len - 4, ".zst") == 0)) { GUC_check_errdetail("Log filename cannot end with compression extension (.gz, .lz4, .zst) as it is automatically added when compression is enabled."); return false; } return true; } /** * @brief GUC Callback pgaudit.log_file_mode * @param void * @return const char *: file mode */ const char *PgAuditLogToFile_guc_show_file_mode(void) { static char buf[12]; snprintf(buf, sizeof(buf), "%04o", guc_pgaudit_ltf_log_file_mode); return buf; }pgauditlogtofile-1.8.4/logtofile_guc.h000066400000000000000000000013571516515416300200760ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_guc.h * GUC variables for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_GUC_H_ #define _LOGTOFILE_GUC_H_ #include #include extern bool PgAuditLogToFile_guc_check_directory(char **newval, void **extra, GucSource source); extern bool PgAuditLogToFile_guc_check_filename(char **newval, void **extra, GucSource source); extern const char *PgAuditLogToFile_guc_show_file_mode(void); #endif pgauditlogtofile-1.8.4/logtofile_json.c000066400000000000000000000207061516515416300202630ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_json.c * Functions to create a json audit record * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_json.h" #include "logtofile_string_format.h" #include "logtofile_vars.h" #include #include #include #include #include #include #include #include #include /* forward declaration private functions */ inline static void pgauditlogtofile_pgaudit2json(StringInfo buf, char *message) __attribute__((always_inline)); /** * @brief Creates a json audit record * @param buf: buffer to write the json string * @param edata: error data * @param exclude_nchars: number of characters to exclude from pgaudit message * @return void */ void PgAuditLogToFile_json_audit(StringInfo buf, const ErrorData *edata, int exclude_nchars) { char formatted_log_time[FORMATTED_TS_LEN]; instr_time now_instr; const char *psdisp; int displen; double total_time; instr_time duration; Size memory_usage; /* json record start */ appendStringInfoString(buf, "{\"log.source\":\"pgauditlogtofile\""); appendStringInfoString(buf, ",\"severity\":\"audit\""); /* timestamp with nanoseconds */ INSTR_TIME_SET_CURRENT(now_instr); PgAuditLogToFile_format_instr_time_nanos(now_instr, formatted_log_time, sizeof(formatted_log_time)); appendStringInfoString(buf, ",\"timestamp\":"); escape_json(buf, formatted_log_time); /* username */ if (MyProcPort && MyProcPort->user_name) { appendStringInfoString(buf, ",\"db.user\":"); escape_json(buf, MyProcPort->user_name); } /* database name */ if (MyProcPort && MyProcPort->database_name) { appendStringInfoString(buf, ",\"db.name\":"); escape_json(buf, MyProcPort->database_name); } /* Process id */ appendStringInfo(buf, ",\"custom.process_id\":\"%d\"", MyProcPid); /* Remote host and port */ if (MyProcPort && MyProcPort->remote_host) { appendStringInfoString(buf, ",\"net.peer.name\":"); escape_json(buf, MyProcPort->remote_host); if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0') { appendStringInfoString(buf, ",\"net.peer.port\":"); escape_json(buf, MyProcPort->remote_port); } } /* session id - hex representation of start time . session process id */ appendStringInfo(buf, ",\"custom.session_id\":\"%lx.%x\"", (long)MyStartTime, MyProcPid); /* PS display */ psdisp = get_ps_display(&displen); if (psdisp && displen > 0) { appendStringInfoString(buf, ",\"custom.command_tag\":"); if (exclude_nchars == 0 && strncmp(edata->message, "disconnection", 13) == 0) escape_json(buf, "disconnection"); else if (exclude_nchars == 0 && (strncmp(edata->message, "connection authenticated", 24) == 0 || strncmp(edata->message, "connection authorized", 21) == 0)) escape_json(buf, "authentication"); else escape_json(buf, psdisp); } /* Virtual transaction id */ #if (PG_VERSION_NUM >= 170000) if (MyProc != NULL && MyProc->vxid.procNumber != INVALID_PROC_NUMBER) appendStringInfo(buf, ",\"custom.virtual_transaction_id\":\"%d/%u\"", MyProc->vxid.procNumber, MyProc->vxid.lxid); #else if (MyProc != NULL && MyProc->backendId != InvalidBackendId) appendStringInfo(buf, ",\"custom.virtual_transaction_id\":\"%d/%u\"", MyProc->backendId, MyProc->lxid); #endif /* Transaction id */ appendStringInfo(buf, ",\"custom.transaction_id\":\"%u\"", GetTopTransactionIdIfAny()); /* SQL state code */ appendStringInfoString(buf, ",\"custom.state_code\":"); escape_json(buf, unpack_sql_state(edata->sqlerrcode)); /* errmessage - PGAUDIT formatted text, +7 exclude "AUDIT: " prefix */ if (exclude_nchars > 0) pgauditlogtofile_pgaudit2json(buf, edata->message + exclude_nchars); else { appendStringInfoString(buf, ",\"content\":"); escape_json(buf, edata->message); } /* errdetail or errdetail_log */ if (edata->detail_log) { appendStringInfoString(buf, ",\"custom.detail_log\":"); escape_json(buf, edata->detail_log); } else if (edata->detail) { appendStringInfoString(buf, ",\"custom.detail_log\":"); escape_json(buf, edata->detail); } /* errhint */ if (edata->hint) { appendStringInfoString(buf, ",\"custom.err_hint\":"); escape_json(buf, edata->hint); } /* internal query and position */ if (edata->internalquery) { appendStringInfoString(buf, ",\"custom.internal_query\":"); escape_json(buf, edata->internalquery); if (edata->internalpos > 0) appendStringInfo(buf, ",\"custom.internal_query_pos\":\"%d\"", edata->internalpos); } if (edata->context) { appendStringInfoString(buf, ",\"custom.context\":"); escape_json(buf, edata->context); } if (guc_pgaudit_ltf_log_execution_time && !INSTR_TIME_IS_ZERO(pgaudit_ltf_statement_start_time) && !INSTR_TIME_IS_ZERO(pgaudit_ltf_statement_end_time)) { PgAuditLogToFile_format_instr_time_nanos(pgaudit_ltf_statement_start_time, formatted_log_time, sizeof(formatted_log_time)); appendStringInfoString(buf, ",\"custom.execution_start\":"); escape_json(buf, formatted_log_time); PgAuditLogToFile_format_instr_time_nanos(pgaudit_ltf_statement_end_time, formatted_log_time, sizeof(formatted_log_time)); appendStringInfoString(buf, ",\"custom.execution_end\":"); escape_json(buf, formatted_log_time); duration = pgaudit_ltf_statement_end_time; INSTR_TIME_SUBTRACT(duration, pgaudit_ltf_statement_start_time); total_time = INSTR_TIME_GET_DOUBLE(duration); appendStringInfo(buf, ",\"custom.execution_time\":\"%.9f\"", total_time); INSTR_TIME_SET_ZERO(pgaudit_ltf_statement_start_time); INSTR_TIME_SET_ZERO(pgaudit_ltf_statement_end_time); } if (guc_pgaudit_ltf_log_execution_memory && pgaudit_ltf_statement_memory_start > 0 && pgaudit_ltf_statement_memory_end > 0) { memory_usage = pgaudit_ltf_statement_memory_end - pgaudit_ltf_statement_memory_start; appendStringInfo(buf, ",\"custom.execution_memory.start\":\"%ld\"", (long)pgaudit_ltf_statement_memory_start); appendStringInfo(buf, ",\"custom.execution_memory.end\":\"%ld\"", (long)pgaudit_ltf_statement_memory_end); appendStringInfo(buf, ",\"custom.execution_memory.peak\":\"%ld\"", (long)pgaudit_ltf_statement_memory_peak); appendStringInfo(buf, ",\"custom.execution_memory.delta\":\"%ld\"", (long)(memory_usage < 0 ? 0 : memory_usage)); pgaudit_ltf_statement_memory_start = 0; pgaudit_ltf_statement_memory_end = 0; } appendStringInfoCharMacro(buf, '}'); appendStringInfoCharMacro(buf, '\n'); } /* private functions */ /** * @brief Split and escapes each piece on pgaudit original message and writes it as json key/value pair. * @param buf Where to write * @param line original pgaudit message, it's modified in this function */ static void pgauditlogtofile_pgaudit2json(StringInfo buf, char *line) { char *token; // AUDIT_TYPE token = strsep(&line, ","); if (token) { appendStringInfoString(buf, ",\"custom.audit_type\":"); escape_json(buf, token); } // STATEMENT_ID token = strsep(&line, ","); if (token) { appendStringInfoString(buf, ",\"custom.statement_id\":"); escape_json(buf, token); } // SUBSTATEMENT_ID token = strsep(&line, ","); if (token) { appendStringInfoString(buf, ",\"custom.substatement_id\":"); escape_json(buf, token); } // CLASS token = strsep(&line, ","); if (token) { appendStringInfoString(buf, ",\"custom.class\":"); escape_json(buf, token); } // COMMAND token = strsep(&line, ","); if (token) { appendStringInfoString(buf, ",\"custom.command\":"); escape_json(buf, token); } // OBJECT_TYPE token = strsep(&line, ","); if (token) { appendStringInfoString(buf, ",\"custom.object_type\":"); escape_json(buf, token); } // OBJECT_NAME token = strsep(&line, ","); if (token) { appendStringInfoString(buf, ",\"custom.object_name\":"); escape_json(buf, token); } // Statement and parameters as one field if (line && *line != '\0') { appendStringInfoString(buf, ",\"content\":"); escape_json(buf, line + (*line == ',' ? 1 : 0)); } } pgauditlogtofile-1.8.4/logtofile_json.h000066400000000000000000000011761516515416300202700ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_json.h * Functions to create a json audit record * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_JSON_H_ #define _LOGTOFILE_JSON_H_ #include #include /* Hook functions */ extern void PgAuditLogToFile_json_audit(StringInfo buf, const ErrorData *edata, int exclude_nchars); #endif pgauditlogtofile-1.8.4/logtofile_log.c000066400000000000000000000423361516515416300200760ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_log.c * Functions to write audit logs to file * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_log.h" #include "logtofile_autoclose.h" #include "logtofile_csv.h" #include "logtofile_errordata.h" #include "logtofile_guc.h" #include "logtofile_json.h" #include "logtofile_shmem.h" #include "logtofile_vars.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define ZSTD_STATIC_LINKING_ONLY #include /* Defines */ #define PGAUDIT_PREFIX_LINE "AUDIT: " #define PGAUDIT_PREFIX_LINE_LENGTH sizeof(PGAUDIT_PREFIX_LINE) - 1 #define PGAUDIT_LTF_AUDIT_BUFFER_INIT_SIZE (4 * 1024) /* variables to use only in this unit */ static char filename_in_use[MAXPGPATH]; static int autoclose_thread_status_debug = 0; // 0: new proc, 1: th running, 2: th running sleep used, 3: th closed static uint32 pgaudit_ltf_local_rotation_generation = 0; static z_stream *pgaudit_ltf_zstream = NULL; static int pgaudit_ltf_gzip_level = 0; static char *pgaudit_ltf_zbuf = NULL; static uLong pgaudit_ltf_zbuf_len = 0; static ZSTD_CCtx *pgaudit_ltf_zstd_cctx = NULL; /* forward declaration private functions */ static void pgauditlogtofile_close_file(void); static bool pgauditlogtofile_is_enabled(void); static bool pgauditlogtofile_is_open_file(void); static bool pgauditlogtofile_is_prefixed(const char *msg); static bool pgauditlogtofile_open_file(void); static bool pgauditlogtofile_record_audit(const ErrorData *edata, int exclude_nchars); static bool pgauditlogtofile_write_audit(const ErrorData *edata, int exclude_nchars); static void pgauditlogtofile_format_audit(StringInfo buf, const ErrorData *edata, int exclude_nchars); static bool pgauditlogtofile_compress_audit(const char *src, size_t src_len, char **dst, size_t *dst_len); static void *pgauditlogtofile_zstd_alloc(void *opaque, size_t size); static void pgauditlogtofile_zstd_free(void *opaque, void *address); /* public methods */ /** * @brief Flushes any pending audit record, injecting current execution stats. */ void PgAuditLogToFile_Flush_Pending(void) { int save_errno = errno; if (!pgaudit_ltf_pending_audit.active || pgaudit_ltf_pending_audit.edata == NULL) return; pgauditlogtofile_record_audit(pgaudit_ltf_pending_audit.edata, PGAUDIT_PREFIX_LINE_LENGTH); PgAuditLogToFile_FreePendingErrorData(); errno = save_errno; } /** * @brief Hook to emit_log - write the record to the audit or send it to the default logger * @param ErrorData: error data * @return void */ void PgAuditLogToFile_emit_log(ErrorData *edata) { int save_errno = errno; if (pgauditlogtofile_is_enabled()) { if (pg_strncasecmp(edata->message, PGAUDIT_PREFIX_LINE, PGAUDIT_PREFIX_LINE_LENGTH) == 0) { edata->output_to_server = false; if (guc_pgaudit_ltf_log_execution_time || guc_pgaudit_ltf_log_execution_memory) { /* * If we measure execution variables, * we buffer the message instead of writing it. * It will be flushed in ExecutorEnd with correct timing stats. */ /* free in case of strange scenarios were ExecutorEndHook is not called */ PgAuditLogToFile_FreePendingErrorData(); PgAuditLogToFile_CopyPendingErrorData(edata); } else { /* we don't waste cycles on buffering */ pgauditlogtofile_record_audit(edata, PGAUDIT_PREFIX_LINE_LENGTH); } } else if (pgauditlogtofile_is_prefixed(edata->message)) { /* connections/disconnection messages, audited immediately and without execution values */ edata->output_to_server = false; pgauditlogtofile_record_audit(edata, 0); } } /* * Restore errno before calling the next hook in the chain. This ensures * that any subsequent hook relying on the original errno (e.g. via %m) * receives the correct value. */ errno = save_errno; if (pgaudit_ltf_prev_emit_log_hook) pgaudit_ltf_prev_emit_log_hook(edata); } /* private functions */ /** * @brief Close the audit log file * @param void * @return void */ static void pgauditlogtofile_close_file(void) { if (pgaudit_ltf_file_handler != -1) { close(pgaudit_ltf_file_handler); pgaudit_ltf_file_handler = -1; } } /** * @brief Checks if pgauditlogtofile is completely started and configured * @param void * @return bool - true if pgauditlogtofile is enabled */ static bool pgauditlogtofile_is_enabled(void) { /* Check shared memory is attached and our struct is initialized */ if (UsedShmemSegAddr == NULL || pgaudit_ltf_shm == NULL) return false; /* * Check if shutdown is in progress. * Atomic check is fast enough for the hot path. */ if (!pg_atomic_unlocked_test_flag(&pgaudit_ltf_flag_shutdown)) return false; /* Check GUCs: verify pointers and ensure strings are not empty */ if (guc_pgaudit_ltf_log_directory == NULL || guc_pgaudit_ltf_log_directory[0] == '\0') return false; if (guc_pgaudit_ltf_log_filename == NULL || guc_pgaudit_ltf_log_filename[0] == '\0') return false; return true; } /** * @brief Checks if the audit log file is open * @param void * @return bool - true if the file is open */ static bool pgauditlogtofile_is_open_file(void) { return (pgaudit_ltf_file_handler != -1); } /** * @brief Checks if a message starts with one of our intercept prefixes * @param msg: message * @return bool - true if the message starts with a prefix */ static bool pgauditlogtofile_is_prefixed(const char *msg) { size_t i; for (i = 0; i < pgaudit_ltf_shm->num_prefixes; i++) { PgAuditLogToFilePrefix *p = pgaudit_ltf_shm->prefixes[i]; if (p->type == PGAUDIT_LTF_TYPE_CONNECTION && !guc_pgaudit_ltf_log_connections) continue; if (p->type == PGAUDIT_LTF_TYPE_DISCONNECTION && !guc_pgaudit_ltf_log_disconnections) continue; if (strncmp(msg, p->prefix, p->length) == 0) return true; } return false; } /** * @brief Open the audit log file * @param void * @return bool - true if the file was opened */ static bool pgauditlogtofile_open_file(void) { mode_t oumask; bool opened = false; char shm_filename[MAXPGPATH]; if (MyProc == NULL) { /* MyProc deinitialized, reuse filename_in_use */ strlcpy(shm_filename, filename_in_use, MAXPGPATH); } else { LWLockAcquire(&pgaudit_ltf_shm->lock, LW_SHARED); strlcpy(shm_filename, pgaudit_ltf_shm->filename, MAXPGPATH); LWLockRelease(&pgaudit_ltf_shm->lock); } // if the filename is empty, we short-circuit if (shm_filename[0] == '\0') return false; /* Create spool directory if not present; ignore errors */ (void)MakePGDirectory(guc_pgaudit_ltf_log_directory); /* * Note we do not let guc_pgaudit_ltf_log_file_mode disable IWUSR, since we certainly want * to be able to write the files ourselves. */ oumask = umask( (mode_t)((~(guc_pgaudit_ltf_log_file_mode | S_IWUSR)) & (S_IRWXU | S_IRWXG | S_IRWXO))); pgaudit_ltf_file_handler = open(shm_filename, O_CREAT | O_WRONLY | O_APPEND | PG_BINARY, guc_pgaudit_ltf_log_file_mode); umask(oumask); if (pgaudit_ltf_file_handler != -1) { opened = true; // File open, we update the filename we are using strlcpy(filename_in_use, shm_filename, MAXPGPATH); } else { ereport(LOG_SERVER_ONLY, (errcode_for_file_access(), errmsg("could not open log file \"%s\": %m", shm_filename))); } return opened; } /** * @brief Records an audit log * @param edata: error data * @param exclude_nchars: number of characters to exclude from the message * @return bool - true if the record was written */ static bool pgauditlogtofile_record_audit(const ErrorData *edata, int exclude_nchars) { bool rc; char shm_filename[MAXPGPATH]; uint32 current_generation; /* * If MyProc is NULL, we are likely in a process exit sequence. We can only * log if we already have a filename in use. We also cannot safely acquire * LWLocks to check for rotation, so we'll just stick with the current file. */ if (MyProc == NULL) { if (filename_in_use[0] == '\0') return false; } else { /* Check if a rotation has occurred or we haven't opened any file yet */ current_generation = pg_atomic_read_u32(&pgaudit_ltf_shm->rotation_generation); if (current_generation != pgaudit_ltf_local_rotation_generation || filename_in_use[0] == '\0') { pgauditlogtofile_close_file(); LWLockAcquire(&pgaudit_ltf_shm->lock, LW_SHARED); strlcpy(shm_filename, pgaudit_ltf_shm->filename, MAXPGPATH); LWLockRelease(&pgaudit_ltf_shm->lock); pgaudit_ltf_local_rotation_generation = current_generation; ereport(DEBUG3, (errmsg("pgauditlogtofile record audit file handler requires reopening - shm_filename %s filename_in_use %s", shm_filename, filename_in_use))); } } if (!pgauditlogtofile_is_open_file() && !pgauditlogtofile_open_file()) return false; rc = pgauditlogtofile_write_audit(edata, exclude_nchars); pgaudit_ltf_autoclose_active_ts = (pg_time_t)time(NULL); if (guc_pgaudit_ltf_auto_close_minutes > 0) { // only 1 auto-close thread if (pg_atomic_test_set_flag(&pgaudit_ltf_autoclose_flag_thread)) { ereport(DEBUG3, (errmsg("pgauditlogtofile record_audit - create autoclose thread"))); autoclose_thread_status_debug = 1; pthread_attr_init(&pgaudit_ltf_autoclose_thread_attr); pthread_attr_setdetachstate(&pgaudit_ltf_autoclose_thread_attr, PTHREAD_CREATE_DETACHED); pthread_create(&pgaudit_ltf_autoclose_thread, &pgaudit_ltf_autoclose_thread_attr, PgAuditLogToFile_autoclose_run, &autoclose_thread_status_debug); } } return rc; } /** * @brief Writes an audit record in the audit log file * @param edata: error data * @param exclude_nchars: number of characters to exclude from the message */ static bool pgauditlogtofile_write_audit(const ErrorData *edata, int exclude_nchars) { MemoryContext oldcontext; StringInfoData buf; char *data_to_write; size_t data_len; int rc; bool success = false; oldcontext = MemoryContextSwitchTo(pgaudit_ltf_memory_context); #if (PG_VERSION_NUM >= 180000) initStringInfoExt(&buf, PGAUDIT_LTF_AUDIT_BUFFER_INIT_SIZE); #else initStringInfo(&buf); #endif MemoryContextSwitchTo(oldcontext); pgauditlogtofile_format_audit(&buf, edata, exclude_nchars); // auto-close maybe has closed the file if (pgaudit_ltf_file_handler == -1) pgauditlogtofile_open_file(); data_to_write = buf.data; data_len = buf.len; if (pgaudit_ltf_file_handler != -1) { bool write_ready = true; if (guc_pgaudit_ltf_log_compression != PGAUDIT_LTF_COMPRESSION_OFF) write_ready = pgauditlogtofile_compress_audit(buf.data, buf.len, &data_to_write, &data_len); if (write_ready) { rc = write(pgaudit_ltf_file_handler, data_to_write, data_len); if (rc == (int)data_len) { success = true; } else { ereport(LOG_SERVER_ONLY, (errcode_for_file_access(), errmsg("could not write audit log file \"%s\": %m", filename_in_use))); pgauditlogtofile_close_file(); } } } /* failed write, do it on server here because the original log record has been modified in place */ if (!success) ereport(LOG_SERVER_ONLY, (errmsg("%s", buf.data))); pfree(buf.data); return success; } /** * @brief Helper to format the audit record based on configuration. */ static void pgauditlogtofile_format_audit(StringInfo buf, const ErrorData *edata, int exclude_nchars) { switch (guc_pgaudit_ltf_log_format) { case PGAUDIT_LTF_FORMAT_CSV: PgAuditLogToFile_csv_audit(buf, edata, exclude_nchars); break; case PGAUDIT_LTF_FORMAT_JSON: PgAuditLogToFile_json_audit(buf, edata, exclude_nchars); break; } } /** * @brief Helper to handle audit record compression. * Updates dst and dst_len pointers to the compressed buffer. */ static bool pgauditlogtofile_compress_audit(const char *src, size_t src_len, char **dst, size_t *dst_len) { size_t compressed_len_bound = 0; bool compression_success = true; /* 1. Calculate buffer size requirements */ switch (guc_pgaudit_ltf_log_compression) { case PGAUDIT_LTF_COMPRESSION_GZIP: compressed_len_bound = compressBound(src_len); break; case PGAUDIT_LTF_COMPRESSION_LZ4: compressed_len_bound = LZ4F_compressFrameBound(src_len, NULL); break; case PGAUDIT_LTF_COMPRESSION_ZSTD: compressed_len_bound = ZSTD_compressBound(src_len); break; default: return false; } /* 2. Ensure compression buffer is large enough */ if (pgaudit_ltf_zbuf == NULL || pgaudit_ltf_zbuf_len < compressed_len_bound) { if (pgaudit_ltf_zbuf) pfree(pgaudit_ltf_zbuf); pgaudit_ltf_zbuf_len = compressed_len_bound; pgaudit_ltf_zbuf = (char *)MemoryContextAlloc(pgaudit_ltf_memory_context, pgaudit_ltf_zbuf_len); } /* 3. Perform algorithm-specific compression */ switch (guc_pgaudit_ltf_log_compression) { case PGAUDIT_LTF_COMPRESSION_GZIP: { int ret; int level = guc_pgaudit_ltf_log_compression_level; if (level == 0) level = Z_BEST_SPEED; else if (level > 9) level = 9; if (pgaudit_ltf_zstream != NULL && pgaudit_ltf_gzip_level != level) { deflateEnd(pgaudit_ltf_zstream); pfree(pgaudit_ltf_zstream); pgaudit_ltf_zstream = NULL; } if (pgaudit_ltf_zstream == NULL) { pgaudit_ltf_zstream = (z_stream *)MemoryContextAlloc(pgaudit_ltf_memory_context, sizeof(z_stream)); pgaudit_ltf_zstream->zalloc = Z_NULL; pgaudit_ltf_zstream->zfree = Z_NULL; pgaudit_ltf_zstream->opaque = Z_NULL; ret = deflateInit2(pgaudit_ltf_zstream, level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); if (ret != Z_OK) { ereport(LOG_SERVER_ONLY, (errmsg("pgauditlogtofile: could not initialize compression stream: zlib error %d", ret))); pfree(pgaudit_ltf_zstream); pgaudit_ltf_zstream = NULL; return false; } pgaudit_ltf_gzip_level = level; } else deflateReset(pgaudit_ltf_zstream); pgaudit_ltf_zstream->avail_in = src_len; pgaudit_ltf_zstream->next_in = (Bytef *)src; pgaudit_ltf_zstream->avail_out = pgaudit_ltf_zbuf_len; pgaudit_ltf_zstream->next_out = (Bytef *)pgaudit_ltf_zbuf; ret = deflate(pgaudit_ltf_zstream, Z_FINISH); if (ret != Z_STREAM_END) { ereport(LOG_SERVER_ONLY, (errmsg("pgauditlogtofile: could not compress audit record: zlib error %d", ret))); compression_success = false; } else { *dst_len = pgaudit_ltf_zstream->total_out; *dst = pgaudit_ltf_zbuf; } break; } case PGAUDIT_LTF_COMPRESSION_LZ4: { LZ4F_preferences_t prefs; size_t cSize; memset(&prefs, 0, sizeof(prefs)); prefs.compressionLevel = guc_pgaudit_ltf_log_compression_level; cSize = LZ4F_compressFrame(pgaudit_ltf_zbuf, pgaudit_ltf_zbuf_len, src, src_len, &prefs); if (LZ4F_isError(cSize)) { ereport(LOG_SERVER_ONLY, (errmsg("pgauditlogtofile: could not compress audit record: lz4 error %s", LZ4F_getErrorName(cSize)))); compression_success = false; } else { *dst_len = cSize; *dst = pgaudit_ltf_zbuf; } break; } case PGAUDIT_LTF_COMPRESSION_ZSTD: { size_t cSize; int level = guc_pgaudit_ltf_log_compression_level; if (level == 0) level = 1; if (pgaudit_ltf_zstd_cctx == NULL) { ZSTD_customMem custom_mem; custom_mem.customAlloc = pgauditlogtofile_zstd_alloc; custom_mem.customFree = pgauditlogtofile_zstd_free; custom_mem.opaque = (void *)pgaudit_ltf_memory_context; pgaudit_ltf_zstd_cctx = ZSTD_createCCtx_advanced(custom_mem); if (pgaudit_ltf_zstd_cctx == NULL) { ereport(LOG_SERVER_ONLY, (errmsg("pgauditlogtofile: could not initialize zstd compression context"))); return false; } } cSize = ZSTD_compressCCtx(pgaudit_ltf_zstd_cctx, pgaudit_ltf_zbuf, pgaudit_ltf_zbuf_len, src, src_len, level); if (ZSTD_isError(cSize)) { ereport(LOG_SERVER_ONLY, (errmsg("pgauditlogtofile: could not compress audit record: zstd error %s", ZSTD_getErrorName(cSize)))); compression_success = false; } else { *dst_len = cSize; *dst = pgaudit_ltf_zbuf; } break; } default: compression_success = false; break; } return compression_success; } static void * pgauditlogtofile_zstd_alloc(void *opaque, size_t size) { MemoryContext context = (MemoryContext)opaque; if (size == 0) return NULL; // Use MCXT_ALLOC_NO_OOM to return nullptr on OOM, as external libraries expect. return MemoryContextAllocExtended(context, size, MCXT_ALLOC_NO_OOM); } static void pgauditlogtofile_zstd_free(__attribute__((unused)) void *opaque, void *address) { if (address != NULL) pfree(address); }pgauditlogtofile-1.8.4/logtofile_log.h000066400000000000000000000011441516515416300200730ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_log.h * Functions to write audit logs to file * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_LOG_H_ #define _LOGTOFILE_LOG_H_ #include /* Hook functions */ extern void PgAuditLogToFile_emit_log(ErrorData *edata); extern void PgAuditLogToFile_Flush_Pending(void); #endif pgauditlogtofile-1.8.4/logtofile_shmem.c000066400000000000000000000214341516515416300204220ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_shmem.c * Functions to manage shared memory * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_shmem.h" #include #include #include #include #include #include #include "logtofile_connect.h" #include "logtofile_filename.h" #include "logtofile_guc.h" #include "logtofile_vars.h" /* Extracted from src/backend/po */ static const char *postgresConnMsg[] = { "connection received: host=%s port=%s", "connection received: host=%s", "connection authorized: user=%s", "connection authenticated: identity=\"%s\" method=%s (%s:%d)", "connection authenticated: user=\"%s\" method=%s (%s:%d)", "replication connection authorized: user=%s", "replication connection authorized: user=%s SSL enabled (protocol=%s, cipher=%s, bits=%d, compression=%s)", "replication connection authorized: user=%s application_name=%s", "replication connection authorized: user=%s application_name=%s SSL enabled (protocol=%s, cipher=%s, bits=%d, compression=%s)", "password authentication failed for user \"%s\"", "authentication failed for user \"%s\": host rejected", "\"trust\" authentication failed for user \"%s\"", "Ident authentication failed for user \"%s\"", "Peer authentication failed for user \"%s\"", "password authentication failed for user \"%s\"", "SSPI authentication failed for user \"%s\"", "PAM authentication failed for user \"%s\"", "BSD authentication failed for user \"%s\"", "LDAP authentication failed for user \"%s\"", "certificate authentication failed for user \"%s\"", "RADIUS authentication failed for user \"%s\"", "authentication failed for user \"%s\": invalid authentication method", "connection authorized: user=%s database=%s", "connection authorized: user=%s database=%s SSL enabled (protocol=%s, cipher=%s, bits=%d, compression=%s)", "connection authorized: user=%s database=%s application_name=%s", "connection authorized: user=%s database=%s application_name=%s SSL enabled (protocol=%s, cipher=%s, bits=%d, compression=%s)", "role \"%s\" does not exist", "connection ready: setup total=%.3f ms, fork=%.3f ms, authentication=%.3f ms", }; /* Extracted from src/backend/po */ static const char *postgresDisconnMsg[] = { "disconnection: session time: %d:%02d:%02d.%03d user=%s database=%s host=%s%s%s"}; /* forward declaration private functions */ static void pgauditlogtofile_init_prefixes(const char **messages, size_t num_messages, PgAuditLogToFilePrefixType type); static size_t pgauditlogtofile_shm_main_struct_size(void); static size_t pgauditlogtofile_shmem_size(void); /** * @brief Request shared memory space */ void PgAuditLogToFile_shmem_request(void) { #if (PG_VERSION_NUM >= 150000) if (pgaudit_ltf_prev_shmem_request_hook) pgaudit_ltf_prev_shmem_request_hook(); #endif RequestAddinShmemSpace(pgauditlogtofile_shmem_size()); RequestNamedLWLockTranche("pgauditlogtofile", 1); } /** * @brief SHMEM startup hook - Initialize SHMEM structure */ void PgAuditLogToFile_shmem_startup(void) { bool found; if (pgaudit_ltf_prev_shmem_startup_hook) pgaudit_ltf_prev_shmem_startup_hook(); /* reset in case this is a restart within the postmaster */ pgaudit_ltf_shm = NULL; LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); pgaudit_ltf_shm = ShmemInitStruct("pgauditlogtofile", pgauditlogtofile_shm_main_struct_size(), &found); if (!found) { LWLockPadded *tranche; size_t conn_count = sizeof(postgresConnMsg) / sizeof(char *); size_t disconn_count = sizeof(postgresDisconnMsg) / sizeof(char *); pg_atomic_init_flag(&pgaudit_ltf_flag_shutdown); pgaudit_ltf_shm->num_prefixes = 0; pgauditlogtofile_init_prefixes(postgresConnMsg, conn_count, PGAUDIT_LTF_TYPE_CONNECTION); pgauditlogtofile_init_prefixes(postgresDisconnMsg, disconn_count, PGAUDIT_LTF_TYPE_DISCONNECTION); /* * Get the tranche ID from the named tranche we requested and * initialize our embedded lock. */ tranche = GetNamedLWLockTranche("pgauditlogtofile"); LWLockInitialize(&pgaudit_ltf_shm->lock, tranche->lock.tranche); pg_atomic_init_u32(&pgaudit_ltf_shm->rotation_generation, 0); PgAuditLogToFile_calculate_current_filename(); PgAuditLogToFile_set_next_rotation_time(); } LWLockRelease(AddinShmemInitLock); if (!IsUnderPostmaster) on_shmem_exit(PgAuditLogToFile_shmem_shutdown, (Datum)0); ereport(LOG, (errmsg("pgauditlogtofile extension initialized"))); } /** * @brief SHMEM shutdown hook * @param code: code * @param arg: arg * @return void */ void PgAuditLogToFile_shmem_shutdown(int code, Datum arg) { pg_atomic_test_set_flag(&pgaudit_ltf_flag_shutdown); } /** * @brief Generates the name for the audit log file * @param void * @return void */ void PgAuditLogToFile_calculate_current_filename(void) { char *filename = NULL; if (UsedShmemSegAddr == NULL || pgaudit_ltf_shm == NULL) return; filename = PgAuditLogToFile_current_filename(); if (filename == NULL) { ereport(WARNING, (errmsg("pgauditlogtofile failed to calculate filename"))); return; } LWLockAcquire(&pgaudit_ltf_shm->lock, LW_EXCLUSIVE); memset(pgaudit_ltf_shm->filename, 0, sizeof(pgaudit_ltf_shm->filename)); strlcpy(pgaudit_ltf_shm->filename, filename, MAXPGPATH); LWLockRelease(&pgaudit_ltf_shm->lock); /* increase generation */ if (pg_atomic_read_u32(&pgaudit_ltf_shm->rotation_generation) == PG_UINT32_MAX) pg_atomic_write_u32(&pgaudit_ltf_shm->rotation_generation, 0); else pg_atomic_add_fetch_u32(&pgaudit_ltf_shm->rotation_generation, 1); pfree(filename); } /** * @brief Checks if the audit log file needs to be rotated before we use it * @param void * @return bool: true if the file needs to be rotated */ bool PgAuditLogToFile_needs_rotate_file(void) { pg_time_t now; if (UsedShmemSegAddr == NULL || pgaudit_ltf_shm == NULL) return false; if (guc_pgaudit_ltf_log_rotation_age < 1) return false; now = (pg_time_t)time(NULL); if (now >= pgaudit_ltf_shm->next_rotation_time) { ereport(DEBUG3, (errmsg("pgauditlogtofile needs to rotate file %s", pgaudit_ltf_shm->filename))); return true; } return false; } /* private functions */ /** * @brief Helper to initialize a prefix list in shared memory */ static void pgauditlogtofile_init_prefixes(const char **messages, size_t num_messages, PgAuditLogToFilePrefixType type) { char **prefixes; size_t num_unique; size_t i; prefixes = PgAuditLogToFile_connect_UniquePrefixes(messages, num_messages, &num_unique); for (i = 0; i < num_unique; i++) { size_t len = strlen(prefixes[i]); size_t struct_size = offsetof(PgAuditLogToFilePrefix, prefix) + len + 1; PgAuditLogToFilePrefix *p; p = (PgAuditLogToFilePrefix *)ShmemAlloc(MAXALIGN(struct_size)); p->length = (int)len; p->type = type; memcpy(p->prefix, prefixes[i], len + 1); pgaudit_ltf_shm->prefixes[pgaudit_ltf_shm->num_prefixes++] = p; pfree(prefixes[i]); } pfree(prefixes); } /** * @brief Calculate total shared memory required */ static size_t pgauditlogtofile_shmem_size(void) { size_t size; size_t i; size_t conn_count = sizeof(postgresConnMsg) / sizeof(char *); size_t disconn_count = sizeof(postgresDisconnMsg) / sizeof(char *); size = pgauditlogtofile_shm_main_struct_size(); /* * Reserve worst-case space for all static strings. * This avoids double-calling the deduplication logic. */ for (i = 0; i < conn_count; i++) { size_t prefix_size = offsetof(PgAuditLogToFilePrefix, prefix) + strlen(postgresConnMsg[i]) + 1; size = add_size(size, MAXALIGN(prefix_size)); } for (i = 0; i < disconn_count; i++) { size_t prefix_size = offsetof(PgAuditLogToFilePrefix, prefix) + strlen(postgresDisconnMsg[i]) + 1; size = add_size(size, MAXALIGN(prefix_size)); } return size; } /** * @brief Calculate the size of the main SHM struct including the flexible array */ static size_t pgauditlogtofile_shm_main_struct_size(void) { size_t conn_count = sizeof(postgresConnMsg) / sizeof(char *); size_t disconn_count = sizeof(postgresDisconnMsg) / sizeof(char *); size_t size; size = offsetof(PgAuditLogToFileShm, prefixes); size = add_size(size, mul_size(add_size(conn_count, disconn_count), sizeof(PgAuditLogToFilePrefix *))); return MAXALIGN(size); }pgauditlogtofile-1.8.4/logtofile_shmem.h000066400000000000000000000014261516515416300204260ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_shmem.h * Functions to manage shared memory * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_SHMEM_H_ #define _LOGTOFILE_SHMEM_H_ #include /* Hook functions */ extern void PgAuditLogToFile_shmem_startup(void); extern void PgAuditLogToFile_shmem_shutdown(int code, Datum arg); extern void PgAuditLogToFile_shmem_request(void); extern void PgAuditLogToFile_calculate_current_filename(void); extern bool PgAuditLogToFile_needs_rotate_file(void); #endif pgauditlogtofile-1.8.4/logtofile_signal_handler.c000066400000000000000000000024111516515416300222550ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_signal_handler.c * Functions to override signal handlers * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_signal_handler.h" #include "logtofile_urgentclose.h" #include "logtofile_vars.h" #include #include #include #include /* public methods */ /** * @brief Signal handler for SIGUSR1 in backends * @param signal_arg: signal number * @return void */ void PgAuditLogToFile_SIGUSR1(SIGNAL_ARGS) { int save_errno = errno; PgAuditLogToFile_close_file_urgent(); /* Trigger any additional signal handler, minus ignore and default */ if (pgaudit_ltf_prev_sigusr1_handler && pgaudit_ltf_prev_sigusr1_handler != SIG_IGN && pgaudit_ltf_prev_sigusr1_handler != SIG_DFL) pgaudit_ltf_prev_sigusr1_handler(postgres_signal_arg); /* call standard handler to process other interrupts that are reusing the same signal */ procsignal_sigusr1_handler(postgres_signal_arg); errno = save_errno; }pgauditlogtofile-1.8.4/logtofile_signal_handler.h000066400000000000000000000011241516515416300222620ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_signal_handler.h * Functions to override signal handlers * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_SIGNAL_HANDLER_H_ #define _LOGTOFILE_SIGNAL_HANDLER_H_ #include extern void PgAuditLogToFile_SIGUSR1(SIGNAL_ARGS); #endif /* _LOGTOFILE_SIGNAL_HANDLER_H_ */pgauditlogtofile-1.8.4/logtofile_string_format.c000066400000000000000000000061641516515416300221720ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_string_format.c * Functions to format data as strings * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_string_format.h" #include "logtofile_autoclose.h" #include "logtofile_guc.h" #include "logtofile_shmem.h" #include "logtofile_vars.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * @brief Formats the record time * @param t instr_time to format * @param buf buffer to write the formatted timestamp * @param len length of the buffer * @return void */ void PgAuditLogToFile_format_instr_time_nanos(instr_time t, char *buf, size_t len) { instr_time now_instr; instr_time delta; TimestampTz now_ts; int64 delta_micro; TimestampTz t_ts; struct pg_tm tm; fsec_t fsec; const char *tzn; int tz; char tzbuf[16]; int64 nsec; size_t cur_len; size_t remaining; /* capture current wall time and compute delta from provided instr_time */ INSTR_TIME_SET_CURRENT(now_instr); delta = now_instr; INSTR_TIME_SUBTRACT(delta, t); #if (PG_VERSION_NUM >= 160000) /* Use nanosecond resolution where available */ { int64 delta_nano = INSTR_TIME_GET_NANOSEC(delta); delta_micro = delta_nano / INT64CONST(1000); } #else /* Fallback to microsecond resolution on older Postgres */ delta_micro = INSTR_TIME_GET_MICROSEC(delta); #endif now_ts = GetCurrentTimestamp(); t_ts = now_ts - (TimestampTz) delta_micro; if (timestamp2tm(t_ts, &tz, &tm, &fsec, &tzn, log_timezone) == 0) { if (tzn == NULL) { int hours = tz / 3600; int mins = abs(tz % 3600) / 60; snprintf(tzbuf, sizeof(tzbuf), "%+03d:%02d", hours, mins); tzn = tzbuf; } if (tzn == NULL) tzn = ""; /* Get nanoseconds from instr_time if available; fall back to microseconds*1000 */ #if (PG_VERSION_NUM >= 160000) nsec = INSTR_TIME_GET_NANOSEC(t) % INT64CONST(1000000000); #else nsec = INSTR_TIME_GET_MICROSEC(t) * INT64CONST(1000); #endif /* Print timestamp without timezone first */ pg_snprintf(buf, len, "%04d-%02d-%02d %02d:%02d:%02d.%09llu", tm.tm_year, tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, (unsigned long long) nsec); /* Safely append a space and the timezone, truncating if necessary */ cur_len = strnlen(buf, len); if (cur_len < len - 1) { buf[cur_len] = ' '; buf[cur_len + 1] = '\0'; remaining = len - cur_len - 1; strlcpy(buf + cur_len + 1, tzn, remaining); } } else { strlcpy(buf, "[invalid timestamp]", len); } } pgauditlogtofile-1.8.4/logtofile_string_format.h000066400000000000000000000013351516515416300221720ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_string_format.h * Functions to format data as strings * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_STRING_FORMAT_H_ #define _LOGTOFILE_STRING_FORMAT_H_ #include #include #include #define FORMATTED_TS_LEN 64 #define FORMATTED_NUMLINE_LEN 32 extern void PgAuditLogToFile_format_instr_time_nanos(instr_time t, char *buf, size_t len); #endif pgauditlogtofile-1.8.4/logtofile_urgentclose.c000066400000000000000000000017631516515416300216460ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_urgentclose.c * Functions to close the audit file descriptor immediately - async-safe * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_urgentclose.h" #include "logtofile_vars.h" #include #include /* public methods */ /** * @brief Close the audit log file immediately (Async-Signal-Safe) * @param void * @return void */ void PgAuditLogToFile_close_file_urgent(void) { /* This function is called from a signal handler. It must be async-signal-safe. */ if (pgaudit_ltf_file_handler != -1) { int save_errno = errno; /* close() is async-signal-safe */ close(pgaudit_ltf_file_handler); pgaudit_ltf_file_handler = -1; errno = save_errno; } } pgauditlogtofile-1.8.4/logtofile_urgentclose.h000066400000000000000000000011541516515416300216450ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_urgentclose.h * Functions to close the audit file descriptor immediately - async-safe * * Copyright (c) 2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_URGENTCLOSE_H_ #define _LOGTOFILE_URGENTCLOSE_H_ #include /* Async-Safe Signal Handler */ extern void PgAuditLogToFile_close_file_urgent(void); #endif pgauditlogtofile-1.8.4/logtofile_vars.c000066400000000000000000000052241516515416300202630ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_vars.c * Global variables for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "logtofile_vars.h" // Guc char *guc_pgaudit_ltf_log_directory = NULL; char *guc_pgaudit_ltf_log_filename = NULL; int guc_pgaudit_ltf_log_file_mode = 0600; int guc_pgaudit_ltf_log_rotation_age = HOURS_PER_DAY * MINS_PER_HOUR; // Default: 1 day bool guc_pgaudit_ltf_log_connections = false; // Default: off bool guc_pgaudit_ltf_log_disconnections = false; // Default: off int guc_pgaudit_ltf_auto_close_minutes = 0; // Default: off int guc_pgaudit_ltf_log_format = PGAUDIT_LTF_FORMAT_CSV; // Default: csv bool guc_pgaudit_ltf_log_execution_time = false; // Default: off bool guc_pgaudit_ltf_log_execution_memory = false; // Default: off int guc_pgaudit_ltf_log_compression = PGAUDIT_LTF_COMPRESSION_OFF; // Default: off int guc_pgaudit_ltf_log_compression_level = 0; // Default: 0 (Library default) // Audit log file handler int pgaudit_ltf_file_handler = -1; // Background auto-close file handler pg_atomic_flag pgaudit_ltf_autoclose_flag_thread; pthread_t pgaudit_ltf_autoclose_thread; pthread_attr_t pgaudit_ltf_autoclose_thread_attr; pg_time_t pgaudit_ltf_autoclose_active_ts; // Statement time measurement instr_time pgaudit_ltf_statement_start_time; instr_time pgaudit_ltf_statement_end_time; // Statement memory measurement Size pgaudit_ltf_statement_memory_start = 0; Size pgaudit_ltf_statement_memory_end = 0; Size pgaudit_ltf_statement_memory_peak = 0; // Pending audit data PendingAudit pgaudit_ltf_pending_audit = {0}; // Hook log emit_log_hook_type pgaudit_ltf_prev_emit_log_hook = NULL; // Executor Hook ExecutorStart_hook_type pgaudit_ltf_prev_ExecutorStart = NULL; ExecutorRun_hook_type pgaudit_ltf_prev_ExecutorRun = NULL; ExecutorEnd_hook_type pgaudit_ltf_prev_ExecutorEnd = NULL; // Signal handlers pqsigfunc pgaudit_ltf_prev_sigusr1_handler = NULL; // Shared memory PgAuditLogToFileShm *pgaudit_ltf_shm = NULL; pg_atomic_flag pgaudit_ltf_flag_shutdown; // Extension memory context MemoryContext pgaudit_ltf_memory_context = NULL; // Shared memory hook shmem_startup_hook_type pgaudit_ltf_prev_shmem_startup_hook = NULL; #if (PG_VERSION_NUM >= 150000) shmem_request_hook_type pgaudit_ltf_prev_shmem_request_hook = NULL; #endif pgauditlogtofile-1.8.4/logtofile_vars.h000066400000000000000000000071511516515416300202710ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile_vars.h * Global variables for logtofile * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #ifndef _LOGTOFILE_VARS_H_ #define _LOGTOFILE_VARS_H_ #include #include #include #include #include #include #include #include #include #include #include #include #include typedef enum { PGAUDIT_LTF_FORMAT_CSV, PGAUDIT_LTF_FORMAT_JSON } PgAuditLogToFileFormat; typedef enum { PGAUDIT_LTF_COMPRESSION_OFF, PGAUDIT_LTF_COMPRESSION_GZIP, PGAUDIT_LTF_COMPRESSION_LZ4, PGAUDIT_LTF_COMPRESSION_ZSTD } PgAuditLogToFileCompression; // Guc extern char *guc_pgaudit_ltf_log_directory; extern char *guc_pgaudit_ltf_log_filename; extern int guc_pgaudit_ltf_log_file_mode; extern int guc_pgaudit_ltf_log_rotation_age; extern bool guc_pgaudit_ltf_log_connections; extern bool guc_pgaudit_ltf_log_disconnections; extern int guc_pgaudit_ltf_auto_close_minutes; extern int guc_pgaudit_ltf_log_format; extern bool guc_pgaudit_ltf_log_execution_time; extern bool guc_pgaudit_ltf_log_execution_memory; extern int guc_pgaudit_ltf_log_compression; extern int guc_pgaudit_ltf_log_compression_level; // Audit log file handler extern int pgaudit_ltf_file_handler; // Background auto-close file handler extern pg_atomic_flag pgaudit_ltf_autoclose_flag_thread; extern pthread_t pgaudit_ltf_autoclose_thread; extern pthread_attr_t pgaudit_ltf_autoclose_thread_attr; extern pg_time_t pgaudit_ltf_autoclose_active_ts; // Statement time measurement extern instr_time pgaudit_ltf_statement_start_time; extern instr_time pgaudit_ltf_statement_end_time; // Statement memory measurement extern Size pgaudit_ltf_statement_memory_start; extern Size pgaudit_ltf_statement_memory_end; extern Size pgaudit_ltf_statement_memory_peak; // Pending audit data to capture stats at the end of execution typedef struct { ErrorData *edata; bool active; } PendingAudit; extern PendingAudit pgaudit_ltf_pending_audit; // Hook log extern emit_log_hook_type pgaudit_ltf_prev_emit_log_hook; // Executor Hook extern ExecutorStart_hook_type pgaudit_ltf_prev_ExecutorStart; extern ExecutorRun_hook_type pgaudit_ltf_prev_ExecutorRun; extern ExecutorEnd_hook_type pgaudit_ltf_prev_ExecutorEnd; // Signal handlers extern pqsigfunc pgaudit_ltf_prev_sigusr1_handler; // Shared Memory types typedef enum { PGAUDIT_LTF_TYPE_CONNECTION, PGAUDIT_LTF_TYPE_DISCONNECTION } PgAuditLogToFilePrefixType; typedef struct PgAuditLogToFilePrefix { int length; PgAuditLogToFilePrefixType type; char prefix[FLEXIBLE_ARRAY_MEMBER]; } PgAuditLogToFilePrefix; typedef struct pgAuditLogToFileShm { LWLock lock; char filename[MAXPGPATH]; pg_time_t next_rotation_time; pg_atomic_uint32 rotation_generation; size_t num_prefixes; PgAuditLogToFilePrefix *prefixes[FLEXIBLE_ARRAY_MEMBER]; } PgAuditLogToFileShm; // Shared Memory extern PgAuditLogToFileShm *pgaudit_ltf_shm; extern pg_atomic_flag pgaudit_ltf_flag_shutdown; // Extension memory context extern MemoryContext pgaudit_ltf_memory_context; // Shared Memory - Hook extern shmem_startup_hook_type pgaudit_ltf_prev_shmem_startup_hook; #if (PG_VERSION_NUM >= 150000) extern shmem_request_hook_type pgaudit_ltf_prev_shmem_request_hook; #endif #endif pgauditlogtofile-1.8.4/pgauditlogtofile--1.0--1.2.sql000066400000000000000000000003301516515416300221000ustar00rootroot00000000000000/* pgauditlogtofile/pgauditlogtofile--1.0--1.2.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION pgauditlogtofile UPDATE TO '1.2'" to load this file. \quit pgauditlogtofile-1.8.4/pgauditlogtofile--1.0.sql000066400000000000000000000003041516515416300215260ustar00rootroot00000000000000/* pgauditlogtofile/pgauditlogtofile--1.0.sql */ -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgauditlogtofile" to load this file. \quit pgauditlogtofile-1.8.4/pgauditlogtofile--1.2--1.3.sql000066400000000000000000000003271516515416300221110ustar00rootroot00000000000000/* pgauditlogtofile/pgauditlogtofile--1.2--1.3.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION pgauditlogtofile UPDATE TO '1.3'" to load this file. \quit pgauditlogtofile-1.8.4/pgauditlogtofile--1.3--1.4.sql000066400000000000000000000003271516515416300221130ustar00rootroot00000000000000/* pgauditlogtofile/pgauditlogtofile--1.3--1.4.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION pgauditlogtofile UPDATE TO '1.4'" to load this file. \quit pgauditlogtofile-1.8.4/pgauditlogtofile--1.4--1.5.sql000066400000000000000000000003271516515416300221150ustar00rootroot00000000000000/* pgauditlogtofile/pgauditlogtofile--1.4--1.5.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION pgauditlogtofile UPDATE TO '1.5'" to load this file. \quit pgauditlogtofile-1.8.4/pgauditlogtofile--1.5--1.6.sql000066400000000000000000000003271516515416300221170ustar00rootroot00000000000000/* pgauditlogtofile/pgauditlogtofile--1.5--1.6.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION pgauditlogtofile UPDATE TO '1.6'" to load this file. \quit pgauditlogtofile-1.8.4/pgauditlogtofile--1.6--1.7.sql000066400000000000000000000003271516515416300221210ustar00rootroot00000000000000/* pgauditlogtofile/pgauditlogtofile--1.6--1.7.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION pgauditlogtofile UPDATE TO '1.7'" to load this file. \quit pgauditlogtofile-1.8.4/pgauditlogtofile--1.7--1.8.sql000066400000000000000000000003271516515416300221230ustar00rootroot00000000000000/* pgauditlogtofile/pgauditlogtofile--1.7--1.8.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION pgauditlogtofile UPDATE TO '1.8'" to load this file. \quit pgauditlogtofile-1.8.4/pgauditlogtofile.c000066400000000000000000000013061516515416300206030ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pgauditlogtofile.c * pgaudit addon to redirect audit log lines to an independent file * * Copyright (c) 2020-2026, Francisco Miguel Biete Banon * Copyright (c) 2014, 2ndQuadrant Ltd. * * This code is released under the PostgreSQL licence, as given at * http://www.postgresql.org/about/licence/ *------------------------------------------------------------------------- */ #include "postgres.h" #include "utils/guc.h" #ifdef PG_MODULE_MAGIC_EXT // Added in 18 PG_MODULE_MAGIC_EXT(.name = "pgauditlogtofile", .version = "1.8"); #else PG_MODULE_MAGIC; // For PostgreSQL versions < 18 #endif #include "logtofile.h" pgauditlogtofile-1.8.4/pgauditlogtofile.control000066400000000000000000000003541516515416300220430ustar00rootroot00000000000000# pgauditlogtofile extension comment = 'pgAudit addon to redirect audit entries to an independent file' # version number also in pgauditlogtofile.c default_version = '1.8' module_pathname = '$libdir/pgauditlogtofile' relocatable = true pgauditlogtofile-1.8.4/test/000077500000000000000000000000001516515416300160545ustar00rootroot00000000000000pgauditlogtofile-1.8.4/test/.gitignore000066400000000000000000000000601516515416300200400ustar00rootroot00000000000000.vagrant results regression.diffs regression.outpgauditlogtofile-1.8.4/test/Dockerfile000066400000000000000000000036111516515416300200470ustar00rootroot00000000000000FROM almalinux:9 # podman run -ti --name pgauditlogtofile17 -v `pwd`:/usr/local/src/pgauditlogtofile:Z docker.io/library/almalinux:9 bash ENV PG_VERSION 17 ENV PATH $PATH:/usr/pgsql-${PG_VERSION}/bin RUN dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm \ && dnf install -y --enablerepo=crb --enablerepo=pgdg${PG_VERSION} \ postgresql${PG_VERSION}-server postgresql${PG_VERSION}-devel make openssl-devel llvm-toolset redhat-rpm-config krb5-devel zlib-devel lz4-devel libzstd-devel \ && dnf clean all -y # Compile & install pgaudit RUN dnf install -y --enablerepo=crb --enablerepo=pgdg${PG_VERSION} pgaudit_${PG_VERSION} # Compile & install pgauditlogtofile RUN make -C /pgauditlogtofile install USE_PGXS=1 USER postgres # Create PostgreSQL cluster RUN /usr/pgsql-${PG_VERSION}/bin/initdb -A trust -k /var/lib/pgsql/${PG_VERSION}/data \ && echo "shared_preload_libraries = 'pgaudit,pgauditlogtofile'" >> /var/lib/pgsql/${PG_VERSION}/data/postgresql.conf \ && /usr/pgsql-${PG_VERSION}/bin/pg_ctl start -D /var/lib/pgsql/${PG_VERSION}/data # Enable pgaudit RUN psql -Xc 'create extension pgaudit' \ && psql -Xc 'alter system set pgaudit.log = "all"' \ && psql -Xc 'alter system set pgaudit.log_parameter = on' \ && psql -Xc 'select pg_reload_conf()' # Enable pgauditlogtofile RUN psql -Xc 'create extension pgauditlogtofile' \ && psql -Xc 'alter system set log_connections = on' \ && psql -Xc 'alter system set log_disconnections = on' \ && psql -Xc 'alter system set pgaudit.log_connections = on' \ && psql -Xc 'alter system set pgaudit.log_disconnections = on' \ && psql -Xc 'select pg_reload_conf()' RUN psql -Xc "alter system set pgaudit.log_format = 'json'" \ && psql -Xc 'select pg_reload_conf()' RUN psql -Xc 'create database pgbench' \ && pgbench -i pgbench \ && pgbench -c 100 -j 10 -T 60 pgbench USER rootpgauditlogtofile-1.8.4/test/Vagrantfile000066400000000000000000000047601516515416300202500ustar00rootroot00000000000000Vagrant.configure(2) do |config| config.vm.box = "centos/7" #config.vm.box = "geerlingguy/centos7" config.vm.provider :virtualbox do |vb| vb.name = "pgauditlogtofile-centos7-test" end # Provision the VM config.vm.provision "shell", inline: <<-SHELL # Setup environment echo 'export PG_VERSION=12' >> /etc/bashrc echo 'export PATH=$PATH:/usr/pgsql-${PG_VERSION?}/bin' >> /etc/bashrc source /etc/bashrc # Install PostgreSQL rpm -ivh https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm yum install -y postgresql${PG_VERSION}-server # Install SCL llvm toolset 7 and enable it by default yum install -y centos-release-scl-rh epel-release yum install -y postgresql${PG_VERSION}-devel make openssl-devel llvm-toolset-7-clang llvm5.0 zlib-devel lz4-devel libzstd-devel echo 'source scl_source enable devtoolset-7' >> /etc/bashrc source /etc/bashrc # Compile & install pgaudit mkdir /pgaudit curl -sSL https://github.com/pgaudit/pgaudit/archive/1.4.0.tar.gz | tar xzf - --strip-components=1 -C /pgaudit make -C /pgaudit install USE_PGXS=1 # Compile & install pgauditlogtofile make -C /pgauditlogtofile install USE_PGXS=1 # Create PostgreSQL cluster /usr/bin/sudo -u postgres /usr/pgsql-${PG_VERSION}/bin/initdb -A trust -k /var/lib/pgsql/${PG_VERSION}/data echo "shared_preload_libraries = 'pgaudit,pgauditlogtofile'" >> /var/lib/pgsql/${PG_VERSION}/data/postgresql.conf /usr/bin/systemctl start postgresql-${PG_VERSION} /usr/bin/sudo -u postgres psql -Xc 'create user vagrant superuser' postgres # Configure pgaudit /usr/bin/sudo -u postgres psql -Xc 'alter system set pgaudit.log = "all"' postgres /usr/bin/sudo -u postgres psql -Xc 'alter system set pgaudit.log_parameter = on' postgres /usr/bin/sudo -u postgres psql -Xc 'select pg_reload_conf()' postgres # Enable pgauditlogtofile /usr/bin/sudo -u postgres psql -Xc 'create extension pgauditlogtofile' postgres /usr/bin/sudo -u postgres psql -Xc "select name, setting, unit from pg_settings where name like 'pgaudit%' order by name" postgres SHELL # Don't share the default vagrant folder config.vm.synced_folder ".", "/vagrant", disabled: true # Mount project path for testing config.vm.synced_folder "..", "/pgauditlogtofile", type: "virtualbox" end pgauditlogtofile-1.8.4/test/expected/000077500000000000000000000000001516515416300176555ustar00rootroot00000000000000pgauditlogtofile-1.8.4/test/expected/audit_file_content.out000066400000000000000000000160251516515416300242510ustar00rootroot00000000000000-- Validates that audit is written \i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \i test/sql/common/setup.sql -- pgauditlogtofile uses the log_timezone value for the date pattern DO $$ DECLARE tz text; BEGIN SELECT setting INTO tz FROM pg_settings WHERE name = 'log_timezone'; EXECUTE format('SET TIMEZONE = %L', tz); END$$; -- search for a text pattern in the current audit log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- audit log file exists CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_file_exists() RETURNS boolean AS $$ DECLARE compression text := current_setting('pgaudit.log_compression'); extension text; count integer; BEGIN IF compression = 'off' THEN extension := '.log'; ELSIF compression = 'gzip' THEN extension := '.log.gz'; ELSIF compression = 'lz4' THEN extension := '.log.lz4'; ELSIF compression = 'zstd' THEN extension := '.log.zst'; ELSE RAISE EXCEPTION 'Unknown compression: %', compression; RETURN false; END IF; SELECT count(*) INTO count FROM (SELECT pg_ls_dir( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory')) AS name) AS ls WHERE name LIKE 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || extension; IF count = 1 THEN RETURN true; ELSE RETURN false; END IF; END; $$ LANGUAGE plpgsql; -- search for a text pattern in the current postgresql server log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_server_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- Force a custom filename for the logs ALTER SYSTEM SET log_filename = 'regression-server-%Y%m%d%H.log'; ALTER SYSTEM SET pgaudit.log_filename = 'regression-audit-%Y%m%d%H.log'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT pg_rotate_logfile(); pg_rotate_logfile ------------------- t (1 row) DO $$ BEGIN -- Write one line RAISE LOG 'Dummy line to ensure we have file'; END$$; -- Set audit format to CSV ALTER SYSTEM SET pgaudit.log_format = 'csv'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT /* REGRESSION_CSV_TEST */ 1; ?column? ---------- 1 (1 row) SELECT pgauditlogtofile_regression_audit_log_content('REGRESSION_CSV_TEST'); pgauditlogtofile_regression_audit_log_content ----------------------------------------------- Found (1 row) SELECT pgauditlogtofile_regression_server_log_content('REGRESSION_CSV_TEST'); pgauditlogtofile_regression_server_log_content ------------------------------------------------ Not Found (1 row) -- Set audit format to JSON ALTER SYSTEM SET pgaudit.log_format = 'json'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT /* REGRESSION_JSON_TEST */ 1; ?column? ---------- 1 (1 row) SELECT pgauditlogtofile_regression_audit_log_content('REGRESSION_JSON_TEST'); pgauditlogtofile_regression_audit_log_content ----------------------------------------------- Found (1 row) SELECT pgauditlogtofile_regression_server_log_content('REGRESSION_JSON_TEST'); pgauditlogtofile_regression_server_log_content ------------------------------------------------ Not Found (1 row) -- Clean up \i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \i test/sql/common/teardown.sql -- Clean up SELECT pg_rotate_logfile(); pg_rotate_logfile ------------------- t (1 row) DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_server_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_file_exists(); -- delete audit file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.gz' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.lz4' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.zst' ) TO PROGRAM 'read path; rm -f "$path"'; -- delete server log file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; pgauditlogtofile-1.8.4/test/expected/audit_file_exists.out000066400000000000000000000165311516515416300241200ustar00rootroot00000000000000-- Verify that pgaudit.log_directory + pgaudit.log_filename exists with the correct datetime replacements \i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \i test/sql/common/setup.sql -- pgauditlogtofile uses the log_timezone value for the date pattern DO $$ DECLARE tz text; BEGIN SELECT setting INTO tz FROM pg_settings WHERE name = 'log_timezone'; EXECUTE format('SET TIMEZONE = %L', tz); END$$; -- search for a text pattern in the current audit log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- audit log file exists CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_file_exists() RETURNS boolean AS $$ DECLARE compression text := current_setting('pgaudit.log_compression'); extension text; count integer; BEGIN IF compression = 'off' THEN extension := '.log'; ELSIF compression = 'gzip' THEN extension := '.log.gz'; ELSIF compression = 'lz4' THEN extension := '.log.lz4'; ELSIF compression = 'zstd' THEN extension := '.log.zst'; ELSE RAISE EXCEPTION 'Unknown compression: %', compression; RETURN false; END IF; SELECT count(*) INTO count FROM (SELECT pg_ls_dir( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory')) AS name) AS ls WHERE name LIKE 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || extension; IF count = 1 THEN RETURN true; ELSE RETURN false; END IF; END; $$ LANGUAGE plpgsql; -- search for a text pattern in the current postgresql server log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_server_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- Force a custom filename for the logs ALTER SYSTEM SET log_filename = 'regression-server-%Y%m%d%H.log'; ALTER SYSTEM SET pgaudit.log_filename = 'regression-audit-%Y%m%d%H.log'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT pg_rotate_logfile(); pg_rotate_logfile ------------------- t (1 row) DO $$ BEGIN -- Write one line RAISE LOG 'Dummy line to ensure we have file'; END$$; ALTER SYSTEM SET pgaudit.log_compression = 'off'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT 1; ?column? ---------- 1 (1 row) SELECT pgauditlogtofile_regression_audit_file_exists(); pgauditlogtofile_regression_audit_file_exists ----------------------------------------------- t (1 row) -- Repeat the test with compression gzip ALTER SYSTEM SET pgaudit.log_compression = 'gzip'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT 1; ?column? ---------- 1 (1 row) SELECT pgauditlogtofile_regression_audit_file_exists(); pgauditlogtofile_regression_audit_file_exists ----------------------------------------------- t (1 row) -- Repeat the test with compression lz4 ALTER SYSTEM SET pgaudit.log_compression = 'lz4'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT 1; ?column? ---------- 1 (1 row) SELECT pgauditlogtofile_regression_audit_file_exists(); pgauditlogtofile_regression_audit_file_exists ----------------------------------------------- t (1 row) -- Repeat the test with compression zstd ALTER SYSTEM SET pgaudit.log_compression = 'zstd'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT 1; ?column? ---------- 1 (1 row) SELECT pgauditlogtofile_regression_audit_file_exists(); pgauditlogtofile_regression_audit_file_exists ----------------------------------------------- t (1 row) -- Clean up \i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \i test/sql/common/teardown.sql -- Clean up SELECT pg_rotate_logfile(); pg_rotate_logfile ------------------- t (1 row) DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_server_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_file_exists(); -- delete audit file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.gz' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.lz4' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.zst' ) TO PROGRAM 'read path; rm -f "$path"'; -- delete server log file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; pgauditlogtofile-1.8.4/test/expected/audit_file_mode.out000066400000000000000000000156751516515416300235350ustar00rootroot00000000000000-- Test file mode setting \i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \i test/sql/common/setup.sql -- pgauditlogtofile uses the log_timezone value for the date pattern DO $$ DECLARE tz text; BEGIN SELECT setting INTO tz FROM pg_settings WHERE name = 'log_timezone'; EXECUTE format('SET TIMEZONE = %L', tz); END$$; -- search for a text pattern in the current audit log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- audit log file exists CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_file_exists() RETURNS boolean AS $$ DECLARE compression text := current_setting('pgaudit.log_compression'); extension text; count integer; BEGIN IF compression = 'off' THEN extension := '.log'; ELSIF compression = 'gzip' THEN extension := '.log.gz'; ELSIF compression = 'lz4' THEN extension := '.log.lz4'; ELSIF compression = 'zstd' THEN extension := '.log.zst'; ELSE RAISE EXCEPTION 'Unknown compression: %', compression; RETURN false; END IF; SELECT count(*) INTO count FROM (SELECT pg_ls_dir( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory')) AS name) AS ls WHERE name LIKE 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || extension; IF count = 1 THEN RETURN true; ELSE RETURN false; END IF; END; $$ LANGUAGE plpgsql; -- search for a text pattern in the current postgresql server log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_server_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- Force a custom filename for the logs ALTER SYSTEM SET log_filename = 'regression-server-%Y%m%d%H.log'; ALTER SYSTEM SET pgaudit.log_filename = 'regression-audit-%Y%m%d%H.log'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT pg_rotate_logfile(); pg_rotate_logfile ------------------- t (1 row) DO $$ BEGIN -- Write one line RAISE LOG 'Dummy line to ensure we have file'; END$$; -- Create a new audit file ALTER SYSTEM SET pgaudit.log_filename = 'regression-audit-file-mode.log'; -- Set file mode to 0600 ALTER SYSTEM SET pgaudit.log_file_mode = '0600'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) -- Generate a log entry to create the file SELECT /* FILE-MODE-TEST */ pg_sleep(1); pg_sleep ---------- (1 row) -- 1. Use COPY to write the exact shell command to a temporary file COPY ( SELECT 'stat -c "%a" ' || current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || current_setting('pgaudit.log_filename') ) TO '/tmp/get_mode.sh'; -- 2. Execute that generated script file \! sh /tmp/get_mode.sh 600 -- 3. Clean up \! rm /tmp/get_mode.sh -- Clean up ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-file-mode.log' ) TO PROGRAM 'read path; rm -f "$path"'; \i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \i test/sql/common/teardown.sql -- Clean up SELECT pg_rotate_logfile(); pg_rotate_logfile ------------------- t (1 row) DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_server_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_file_exists(); -- delete audit file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.gz' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.lz4' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.zst' ) TO PROGRAM 'read path; rm -f "$path"'; -- delete server log file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; pgauditlogtofile-1.8.4/test/expected/extension_exists.out000066400000000000000000000140241516515416300240220ustar00rootroot00000000000000\i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \i test/sql/common/setup.sql -- pgauditlogtofile uses the log_timezone value for the date pattern DO $$ DECLARE tz text; BEGIN SELECT setting INTO tz FROM pg_settings WHERE name = 'log_timezone'; EXECUTE format('SET TIMEZONE = %L', tz); END$$; -- search for a text pattern in the current audit log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- audit log file exists CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_file_exists() RETURNS boolean AS $$ DECLARE compression text := current_setting('pgaudit.log_compression'); extension text; count integer; BEGIN IF compression = 'off' THEN extension := '.log'; ELSIF compression = 'gzip' THEN extension := '.log.gz'; ELSIF compression = 'lz4' THEN extension := '.log.lz4'; ELSIF compression = 'zstd' THEN extension := '.log.zst'; ELSE RAISE EXCEPTION 'Unknown compression: %', compression; RETURN false; END IF; SELECT count(*) INTO count FROM (SELECT pg_ls_dir( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory')) AS name) AS ls WHERE name LIKE 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || extension; IF count = 1 THEN RETURN true; ELSE RETURN false; END IF; END; $$ LANGUAGE plpgsql; -- search for a text pattern in the current postgresql server log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_server_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- Force a custom filename for the logs ALTER SYSTEM SET log_filename = 'regression-server-%Y%m%d%H.log'; ALTER SYSTEM SET pgaudit.log_filename = 'regression-audit-%Y%m%d%H.log'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT pg_rotate_logfile(); pg_rotate_logfile ------------------- t (1 row) DO $$ BEGIN -- Write one line RAISE LOG 'Dummy line to ensure we have file'; END$$; SELECT extname, extrelocatable FROM pg_extension WHERE extname LIKE 'pgaudit%' ORDER BY extname; extname | extrelocatable ------------------+---------------- pgaudit | t pgauditlogtofile | t (2 rows) -- Clean up \i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \i test/sql/common/teardown.sql -- Clean up SELECT pg_rotate_logfile(); pg_rotate_logfile ------------------- t (1 row) DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_server_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_file_exists(); -- delete audit file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.gz' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.lz4' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.zst' ) TO PROGRAM 'read path; rm -f "$path"'; -- delete server log file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; pgauditlogtofile-1.8.4/test/expected/guc_defaults.out000066400000000000000000000111601516515416300230520ustar00rootroot00000000000000-- Verify GUC defaults \i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) SELECT name, setting FROM pg_settings WHERE name IN ( 'pgaudit.log_directory', 'pgaudit.log_filename', 'pgaudit.log_file_mode', 'pgaudit.log_rotation_age', 'pgaudit.log_connections', 'pgaudit.log_disconnections', 'pgaudit.log_autoclose_minutes', 'pgaudit.log_format', 'pgaudit.log_execution_time', 'pgaudit.log_execution_memory', 'pgaudit.log_compression', 'pgaudit.log_compression_level' ) ORDER BY name; name | setting -------------------------------+----------------------- pgaudit.log_autoclose_minutes | 0 pgaudit.log_compression | off pgaudit.log_compression_level | 0 pgaudit.log_connections | off pgaudit.log_directory | log pgaudit.log_disconnections | off pgaudit.log_execution_memory | off pgaudit.log_execution_time | off pgaudit.log_file_mode | 0600 pgaudit.log_filename | audit-%Y%m%d_%H%M.log pgaudit.log_format | csv pgaudit.log_rotation_age | 1440 (12 rows) -- Clean up \i test/sql/common/reset.sql ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \i test/sql/common/teardown.sql -- Clean up SELECT pg_rotate_logfile(); pg_rotate_logfile ------------------- t (1 row) DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_log_content(text); psql:test/sql/common/teardown.sql:4: NOTICE: function pgauditlogtofile_regression_audit_log_content(text) does not exist, skipping DROP FUNCTION IF EXISTS pgauditlogtofile_regression_server_log_content(text); psql:test/sql/common/teardown.sql:6: NOTICE: function pgauditlogtofile_regression_server_log_content(text) does not exist, skipping DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_file_exists(); psql:test/sql/common/teardown.sql:8: NOTICE: function pgauditlogtofile_regression_audit_file_exists() does not exist, skipping -- delete audit file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.gz' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.lz4' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.zst' ) TO PROGRAM 'read path; rm -f "$path"'; -- delete server log file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; pgauditlogtofile-1.8.4/test/sql/000077500000000000000000000000001516515416300166535ustar00rootroot00000000000000pgauditlogtofile-1.8.4/test/sql/audit_file_content.sql000066400000000000000000000013721516515416300232360ustar00rootroot00000000000000-- Validates that audit is written \i test/sql/common/reset.sql \i test/sql/common/setup.sql -- Set audit format to CSV ALTER SYSTEM SET pgaudit.log_format = 'csv'; SELECT pg_reload_conf(); SELECT /* REGRESSION_CSV_TEST */ 1; SELECT pgauditlogtofile_regression_audit_log_content('REGRESSION_CSV_TEST'); SELECT pgauditlogtofile_regression_server_log_content('REGRESSION_CSV_TEST'); -- Set audit format to JSON ALTER SYSTEM SET pgaudit.log_format = 'json'; SELECT pg_reload_conf(); SELECT /* REGRESSION_JSON_TEST */ 1; SELECT pgauditlogtofile_regression_audit_log_content('REGRESSION_JSON_TEST'); SELECT pgauditlogtofile_regression_server_log_content('REGRESSION_JSON_TEST'); -- Clean up \i test/sql/common/reset.sql \i test/sql/common/teardown.sqlpgauditlogtofile-1.8.4/test/sql/audit_file_exists.sql000066400000000000000000000016571516515416300231110ustar00rootroot00000000000000-- Verify that pgaudit.log_directory + pgaudit.log_filename exists with the correct datetime replacements \i test/sql/common/reset.sql \i test/sql/common/setup.sql ALTER SYSTEM SET pgaudit.log_compression = 'off'; SELECT pg_reload_conf(); SELECT 1; SELECT pgauditlogtofile_regression_audit_file_exists(); -- Repeat the test with compression gzip ALTER SYSTEM SET pgaudit.log_compression = 'gzip'; SELECT pg_reload_conf(); SELECT 1; SELECT pgauditlogtofile_regression_audit_file_exists(); -- Repeat the test with compression lz4 ALTER SYSTEM SET pgaudit.log_compression = 'lz4'; SELECT pg_reload_conf(); SELECT 1; SELECT pgauditlogtofile_regression_audit_file_exists(); -- Repeat the test with compression zstd ALTER SYSTEM SET pgaudit.log_compression = 'zstd'; SELECT pg_reload_conf(); SELECT 1; SELECT pgauditlogtofile_regression_audit_file_exists(); -- Clean up \i test/sql/common/reset.sql \i test/sql/common/teardown.sqlpgauditlogtofile-1.8.4/test/sql/audit_file_mode.sql000066400000000000000000000022261516515416300225070ustar00rootroot00000000000000-- Test file mode setting \i test/sql/common/reset.sql \i test/sql/common/setup.sql -- Create a new audit file ALTER SYSTEM SET pgaudit.log_filename = 'regression-audit-file-mode.log'; -- Set file mode to 0600 ALTER SYSTEM SET pgaudit.log_file_mode = '0600'; SELECT pg_reload_conf(); -- Generate a log entry to create the file SELECT /* FILE-MODE-TEST */ pg_sleep(1); -- 1. Use COPY to write the exact shell command to a temporary file COPY ( SELECT 'stat -c "%a" ' || current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || current_setting('pgaudit.log_filename') ) TO '/tmp/get_mode.sh'; -- 2. Execute that generated script file \! sh /tmp/get_mode.sh -- 3. Clean up \! rm /tmp/get_mode.sh -- Clean up ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; SELECT pg_reload_conf(); COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-file-mode.log' ) TO PROGRAM 'read path; rm -f "$path"'; \i test/sql/common/reset.sql \i test/sql/common/teardown.sqlpgauditlogtofile-1.8.4/test/sql/common/000077500000000000000000000000001516515416300201435ustar00rootroot00000000000000pgauditlogtofile-1.8.4/test/sql/common/reset.sql000066400000000000000000000013651516515416300220130ustar00rootroot00000000000000ALTER SYSTEM RESET pgaudit.log_directory; ALTER SYSTEM RESET pgaudit.log_filename; ALTER SYSTEM RESET pgaudit.log_file_mode; ALTER SYSTEM RESET pgaudit.log_rotation_age; ALTER SYSTEM RESET pgaudit.log_connections; ALTER SYSTEM RESET pgaudit.log_disconnections; ALTER SYSTEM RESET pgaudit.log_autoclose_minutes; ALTER SYSTEM RESET pgaudit.log_format; ALTER SYSTEM RESET pgaudit.log_execution_time; ALTER SYSTEM RESET pgaudit.log_execution_memory; ALTER SYSTEM RESET pgaudit.log_compression; ALTER SYSTEM RESET pgaudit.log_compression_level; ALTER SYSTEM RESET log_directory; ALTER SYSTEM RESET log_filename; ALTER SYSTEM RESET log_file_mode; ALTER SYSTEM RESET log_connections; ALTER SYSTEM RESET log_disconnections; SELECT pg_reload_conf();pgauditlogtofile-1.8.4/test/sql/common/setup.sql000066400000000000000000000047751516515416300220410ustar00rootroot00000000000000-- pgauditlogtofile uses the log_timezone value for the date pattern DO $$ DECLARE tz text; BEGIN SELECT setting INTO tz FROM pg_settings WHERE name = 'log_timezone'; EXECUTE format('SET TIMEZONE = %L', tz); END$$; -- search for a text pattern in the current audit log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- audit log file exists CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_audit_file_exists() RETURNS boolean AS $$ DECLARE compression text := current_setting('pgaudit.log_compression'); extension text; count integer; BEGIN IF compression = 'off' THEN extension := '.log'; ELSIF compression = 'gzip' THEN extension := '.log.gz'; ELSIF compression = 'lz4' THEN extension := '.log.lz4'; ELSIF compression = 'zstd' THEN extension := '.log.zst'; ELSE RAISE EXCEPTION 'Unknown compression: %', compression; RETURN false; END IF; SELECT count(*) INTO count FROM (SELECT pg_ls_dir( current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory')) AS name) AS ls WHERE name LIKE 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || extension; IF count = 1 THEN RETURN true; ELSE RETURN false; END IF; END; $$ LANGUAGE plpgsql; -- search for a text pattern in the current postgresql server log file CREATE OR REPLACE FUNCTION pgauditlogtofile_regression_server_log_content(pattern text) RETURNS text AS $$ DECLARE content text; BEGIN content := pg_read_file( current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log'); IF strpos(content, pattern) > 0 THEN RETURN 'Found'; ELSE RETURN 'Not Found'; END IF; END; $$ LANGUAGE plpgsql; -- Force a custom filename for the logs ALTER SYSTEM SET log_filename = 'regression-server-%Y%m%d%H.log'; ALTER SYSTEM SET pgaudit.log_filename = 'regression-audit-%Y%m%d%H.log'; SELECT pg_reload_conf(); SELECT pg_rotate_logfile(); DO $$ BEGIN -- Write one line RAISE LOG 'Dummy line to ensure we have file'; END$$;pgauditlogtofile-1.8.4/test/sql/common/teardown.sql000066400000000000000000000030141516515416300225050ustar00rootroot00000000000000-- Clean up SELECT pg_rotate_logfile(); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_server_log_content(text); DROP FUNCTION IF EXISTS pgauditlogtofile_regression_audit_file_exists(); -- delete audit file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.gz' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.lz4' ) TO PROGRAM 'read path; rm -f "$path"'; COPY ( SELECT current_setting('data_directory') || '/' || current_setting('pgaudit.log_directory') || '/' || 'regression-audit-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log.zst' ) TO PROGRAM 'read path; rm -f "$path"'; -- delete server log file COPY ( SELECT current_setting('data_directory') || '/' || current_setting('log_directory') || '/' || 'regression-server-' || TO_CHAR(NOW(), 'YYYYMMDDHH24') || '.log' ) TO PROGRAM 'read path; rm -f "$path"'; pgauditlogtofile-1.8.4/test/sql/extension_exists.sql000066400000000000000000000001421516515416300230040ustar00rootroot00000000000000SELECT extname, extrelocatable FROM pg_extension WHERE extname LIKE 'pgaudit%' ORDER BY extname;pgauditlogtofile-1.8.4/test/sql/guc_defaults.sql000066400000000000000000000011041516515416300220350ustar00rootroot00000000000000-- Verify GUC defaults \i test/sql/common/reset.sql SELECT name, setting FROM pg_settings WHERE name IN ( 'pgaudit.log_directory', 'pgaudit.log_filename', 'pgaudit.log_file_mode', 'pgaudit.log_rotation_age', 'pgaudit.log_connections', 'pgaudit.log_disconnections', 'pgaudit.log_autoclose_minutes', 'pgaudit.log_format', 'pgaudit.log_execution_time', 'pgaudit.log_execution_memory', 'pgaudit.log_compression', 'pgaudit.log_compression_level' ) ORDER BY name; -- Clean up \i test/sql/common/reset.sql \i test/sql/common/teardown.sql