pax_global_header00006660000000000000000000000064144370551560014524gustar00rootroot0000000000000052 comment=47d63987eb2b40f6a6f78599c1caf22ace4e4cac pgauditlogtofile-1.5.12/000077500000000000000000000000001443705515600151545ustar00rootroot00000000000000pgauditlogtofile-1.5.12/.gitignore000066400000000000000000000000521443705515600171410ustar00rootroot00000000000000/*.o /*.so /*.bc /.vagrant /test/.vagrant pgauditlogtofile-1.5.12/COPYING000066400000000000000000000016761443705515600162210ustar00rootroot00000000000000pglog - 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.5.12/LICENSE000066400000000000000000000001511443705515600161560ustar00rootroot00000000000000This code is released under the PostgreSQL licence, as given at http://www.postgresql.org/about/licence/ pgauditlogtofile-1.5.12/Makefile000066400000000000000000000007221443705515600166150ustar00rootroot00000000000000# pgauditlogtofile/Makefile MODULE_big = pgauditlogtofile OBJS = pgauditlogtofile.o logtofile.o EXTENSION = pgauditlogtofile 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 PGFILEDESC = "pgAuditLogToFile - An addon for pgAudit logging extension for PostgreSQL" PG_LDFLAGS = -lz PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) pgauditlogtofile-1.5.12/README.md000066400000000000000000000040501443705515600164320ustar00rootroot00000000000000# 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 - Add pgauditlogtofile to "shared_preload_libraries" in postgresql.conf - 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) ``` postgres=# CREATE EXTENSION pgauditlogtofile; ``` ## Configuration ### 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_rotation_age Number of minutes after which the audit file will be rotated. **Scope**: System **Default**: 1440 minutes (1 day) 0 will disable the rotation ### 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 ### Test ``` cd test vagrant plugin install vagrant-vbguest vagrant up ``` pgauditlogtofile-1.5.12/logtofile.c000066400000000000000000000654471443705515600173240ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile.c * pgaudit addon to redirect audit log lines to an independent file * * Copyright (c) 2020-2023, 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 "access/xact.h" #include "libpq/libpq-be.h" #include "miscadmin.h" #include "postmaster/syslogger.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/pg_shmem.h" #include "storage/proc.h" #include "tcop/tcopprot.h" #include "utils/guc.h" #include "utils/memutils.h" #include "utils/ps_status.h" #include "logtofile.h" #include #include #include #include /* Defines */ #define PGAUDIT_PREFIX_LINE "AUDIT: " #define PGAUDIT_PREFIX_LINE_LENGTH sizeof(PGAUDIT_PREFIX_LINE) - 1 #define FORMATTED_TS_LEN 128 /* * We really want line-buffered mode for logfile output, but Windows does * not have it, and interprets _IOLBF as _IOFBF (bozos). So use _IONBF * instead on Windows. */ #ifdef WIN32 #define LBF_MODE _IONBF #else #define LBF_MODE _IOLBF #endif /* 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)", "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)", }; /* Extracted from src/backend/po */ static const char * postgresDisconnMsg[] = { "disconnection: session time: %d:%02d:%02d.%03d user=%s database=%s host=%s%s%s" }; /* Buffers for formatted timestamps */ static char formatted_start_time[FORMATTED_TS_LEN]; static char formatted_log_time[FORMATTED_TS_LEN]; /* SHM structure */ typedef struct pgAuditLogToFilePrefix { char *prefix; int length; } pgAuditLogToFilePrefix; typedef struct pgAuditLogToFileShm { LWLock *lock; bool force_rotation; pgAuditLogToFilePrefix **prefixes_connection; size_t num_prefixes_connection; pgAuditLogToFilePrefix **prefixes_disconnection; size_t num_prefixes_disconnection; } pgAuditLogToFileShm; static pgAuditLogToFileShm *pgaudit_log_shm = NULL; static bool pgAuditLogToFileShutdown = false; /* Audit log file handler */ static FILE *file_handler = NULL; static char filename_in_use[MAXPGPATH]; static char filename[MAXPGPATH]; pg_time_t next_rotation_time; /* GUC variables */ char *guc_pgaudit_log_directory = NULL; char *guc_pgaudit_log_filename = NULL; // Default 1 day rotation int guc_pgaudit_log_rotation_age = HOURS_PER_DAY * MINS_PER_HOUR; bool guc_pgaudit_log_connections = false; bool guc_pgaudit_log_disconnections = false; /* Old hook storage for loading/unloading of the extension */ static emit_log_hook_type prev_emit_log_hook = NULL; static shmem_startup_hook_type prev_shmem_startup_hook = NULL; #if (PG_VERSION_NUM >= 150000) static shmem_request_hook_type prev_shmem_request_hook = NULL; #endif /* Hook functions */ static void pgauditlogtofile_emit_log(ErrorData *edata); static void pgauditlogtofile_shmem_startup(void); #if (PG_VERSION_NUM >= 150000) static void pgauditlogtofile_shmem_request(void); #endif /* Internal functions */ static char ** pgauditlogtofile_unique_prefixes(const char **messages, const size_t num_messages, size_t *num_unique); static void guc_assign_directory(const char *newval, void *extra); static void guc_assign_filename(const char *newval, void *extra); static bool guc_check_directory(char **newval, void **extra, GucSource source); static void guc_assign_rotation_age(int newval, void *extra); static void pgauditlogtofile_request_rotation(void); static void pgauditlogtofile_calculate_filename(void); static void pgauditlogtofile_calculate_next_rotation_time(void); static void pgauditlogtofile_close_file(void); static void pgauditlogtofile_create_audit_line(StringInfo buf, const ErrorData *edata, int exclude_nchars); static void pgauditlogtofile_format_log_time(void); static void pgauditlogtofile_format_start_time(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_needs_rotate_file(void); static bool pgauditlogtofile_open_file(void); static bool pgauditlogtofile_record_audit(const ErrorData *edata, int exclude_nchars); static void pgauditlogtofile_shmem_shutdown(int code, Datum arg); static bool pgauditlogtofile_write_audit(const ErrorData *edata, int exclude_nchars); static void pgauditlogtofile_request_rotation(void) { if (UsedShmemSegAddr == NULL) return; /* Directory is changed force a rotation */ if (!pgaudit_log_shm->force_rotation) { LWLockAcquire(pgaudit_log_shm->lock, LW_EXCLUSIVE); pgaudit_log_shm->force_rotation = true; LWLockRelease(pgaudit_log_shm->lock); } } /* * GUC Callback pgaudit.log_directory changes */ static void guc_assign_directory(const char *newval, void *extra) { pgauditlogtofile_request_rotation(); } /* * GUC Callback pgaudit.log_filename changes */ static void guc_assign_filename(const char *newval, void *extra) { pgauditlogtofile_request_rotation(); } /* * GUC Callback pgaudit.log_directory check path */ static bool 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; } /* * GUC Callback pgaudit.rotation_age changes */ static void guc_assign_rotation_age(int newval, void *extra) { pgauditlogtofile_request_rotation(); } /* * Extension Init Callback */ void _PG_init(void) { DefineCustomStringVariable( "pgaudit.log_directory", "Directory where to spool log data", NULL, &guc_pgaudit_log_directory, "log", PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, guc_check_directory, guc_assign_directory, NULL); DefineCustomStringVariable( "pgaudit.log_filename", "Filename with time patterns (up to minutes) where to spool audit data", NULL, &guc_pgaudit_log_filename, "audit-%Y%m%d_%H%M.log", PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, guc_assign_filename, NULL); DefineCustomIntVariable( "pgaudit.log_rotation_age", "Automatic spool file rotation will occur after N minutes", NULL, &guc_pgaudit_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, guc_assign_rotation_age, NULL); DefineCustomBoolVariable( "pgaudit.log_connections", "Intercepts log_connections messages", NULL, &guc_pgaudit_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_log_disconnections, false, PGC_SIGHUP, GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY, NULL, NULL, NULL); EmitWarningsOnPlaceholders("pgauditlogtofile"); #if (PG_VERSION_NUM >= 150000) prev_shmem_request_hook = shmem_request_hook; shmem_request_hook = pgauditlogtofile_shmem_request; #else RequestAddinShmemSpace(MAXALIGN(sizeof(pgAuditLogToFileShm))); RequestNamedLWLockTranche("pgauditlogtofile", 1); #endif prev_shmem_startup_hook = shmem_startup_hook; shmem_startup_hook = pgauditlogtofile_shmem_startup; prev_emit_log_hook = emit_log_hook; emit_log_hook = pgauditlogtofile_emit_log; } /* * Extension Fin Callback */ void _PG_fini(void) { emit_log_hook = prev_emit_log_hook; shmem_startup_hook = prev_shmem_startup_hook; } #if (PG_VERSION_NUM >= 150000) static void pgauditlogtofile_shmem_request(void) { if (prev_shmem_request_hook) prev_shmem_request_hook(); RequestAddinShmemSpace(MAXALIGN(sizeof(pgAuditLogToFileShm))); RequestNamedLWLockTranche("pgauditlogtofile", 1); } #endif /* * SHMEM startup hook - Initialize SHMEM structure */ static void pgauditlogtofile_shmem_startup(void) { bool found; size_t num_messages, i, j; char **prefixes = NULL; if (prev_shmem_startup_hook) prev_shmem_startup_hook(); /* reset in case this is a restart within the postmaster */ pgaudit_log_shm = NULL; LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); pgaudit_log_shm = ShmemInitStruct("pgauditlogtofile", sizeof(pgAuditLogToFileShm), &found); if (!found) { // Get unique prefixes and copy them to SHMEM num_messages = sizeof(postgresConnMsg) / sizeof(char *); prefixes = pgauditlogtofile_unique_prefixes(postgresConnMsg, num_messages, &pgaudit_log_shm->num_prefixes_connection); pgaudit_log_shm->prefixes_connection = ShmemAlloc(pgaudit_log_shm->num_prefixes_connection * sizeof(pgAuditLogToFilePrefix *)); for (i = 0, j = 0; i < num_messages; i++) { if (prefixes != NULL && prefixes[i] != NULL) { pgaudit_log_shm->prefixes_connection[j] = ShmemAlloc(sizeof(pgAuditLogToFilePrefix)); pgaudit_log_shm->prefixes_connection[j]->length = strlen(prefixes[i]); pgaudit_log_shm->prefixes_connection[j]->prefix = ShmemAlloc( (pgaudit_log_shm->prefixes_connection[j]->length + 1) * sizeof(char) ); strcpy(pgaudit_log_shm->prefixes_connection[j]->prefix, prefixes[i]); free(prefixes[i]); j++; } } free(prefixes); num_messages = sizeof(postgresDisconnMsg) / sizeof(char *); prefixes = pgauditlogtofile_unique_prefixes(postgresDisconnMsg, num_messages, &pgaudit_log_shm->num_prefixes_disconnection); pgaudit_log_shm->prefixes_disconnection = ShmemAlloc(pgaudit_log_shm->num_prefixes_disconnection * sizeof(pgAuditLogToFilePrefix *)); for (i = 0, j = 0; i < num_messages; i++) { if (prefixes != NULL && prefixes[i] != NULL) { pgaudit_log_shm->prefixes_disconnection[j] = ShmemAlloc(sizeof(pgAuditLogToFilePrefix)); pgaudit_log_shm->prefixes_disconnection[j]->length = strlen(prefixes[i]); pgaudit_log_shm->prefixes_disconnection[j]->prefix = ShmemAlloc( (pgaudit_log_shm->prefixes_disconnection[j]->length + 1) * sizeof(char) ); strcpy(pgaudit_log_shm->prefixes_disconnection[j]->prefix, prefixes[i]); free(prefixes[i]); j++; } } free(prefixes); pgaudit_log_shm->lock = &(GetNamedLWLockTranche("pgauditlogtofile"))->lock; pgaudit_log_shm->force_rotation = false; if (guc_pgaudit_log_rotation_age > 0) pgauditlogtofile_calculate_next_rotation_time(); pgauditlogtofile_calculate_filename(); } LWLockRelease(AddinShmemInitLock); if (!IsUnderPostmaster) on_shmem_exit(pgauditlogtofile_shmem_shutdown, (Datum) 0); if (!found) { ereport(LOG, (errmsg("pgauditlogtofile extension initialized"))); } } static char ** pgauditlogtofile_unique_prefixes(const char **messages, const size_t num_messages, size_t *num_unique) { bool is_unique; char **prefixes = NULL; char *message, *prefix, *dup; size_t i, j; *num_unique = 0; prefixes = malloc(num_messages * sizeof(char *)); if (prefixes != NULL) { for (i = 0; i < num_messages; i++) { #ifdef ENABLE_NLS // Get translation - static copy message = gettext(messages[i]); #else // Pointer to original = static copy message = messages[i]; #endif // Get a copy that we can modify dup = strdup(message); if (dup != NULL) { prefix = strtok(dup, "%"); if (prefix != NULL) { // Search duplicated is_unique = true; for (j = 0; j < i; j++) { if (prefixes[j] != NULL) { if (strcmp(prefixes[j], prefix) == 0) { // Skip - prefix already present is_unique = false; } } } if (is_unique) { prefixes[i] = malloc((strlen(prefix) + 1) * sizeof(char)); if (prefixes[i] != NULL) { strcpy(prefixes[i], prefix); *num_unique += 1; } } else { prefixes[i] = NULL; } } free(dup); } } } return prefixes; } /* * Hook to emit_log - write the record to the audit or send it to the default * logger */ static void pgauditlogtofile_emit_log(ErrorData *edata) { int exclude_nchars = -1; bool close_file = false; if (pgauditlogtofile_is_enabled()) { // printf("ENABLE PRINTF\n"); if (pg_strncasecmp(edata->message, PGAUDIT_PREFIX_LINE, PGAUDIT_PREFIX_LINE_LENGTH) == 0) { exclude_nchars = PGAUDIT_PREFIX_LINE_LENGTH; edata->output_to_server = false; } else if (pgauditlogtofile_is_prefixed(edata->message)) { edata->output_to_server = false; exclude_nchars = 0; // We want to close the file for connection messages, since we could not be logging anything anymore and keeping // the file open close_file = true; } // Scenarios not contemplated above will be ignored if (exclude_nchars >= 0) { if (!pgauditlogtofile_record_audit(edata, exclude_nchars)) { // ERROR: failed to record in audit, record in server log edata->output_to_server = true; } if (close_file) { pgauditlogtofile_close_file(); } } } if (prev_emit_log_hook) prev_emit_log_hook(edata); } /* * Checks if pgauditlogtofile is completely started and configured */ static inline bool pgauditlogtofile_is_enabled(void) { if (UsedShmemSegAddr == NULL) return false; if (pgAuditLogToFileShutdown || !pgaudit_log_shm || guc_pgaudit_log_directory == NULL || guc_pgaudit_log_filename == NULL || strlen(guc_pgaudit_log_directory) == 0 || strlen(guc_pgaudit_log_filename) == 0) return false; return true; } /* * Records an audit log */ static bool pgauditlogtofile_record_audit(const ErrorData *edata, int exclude_nchars) { if (pgauditlogtofile_needs_rotate_file()) { // calculate_filename will generate a new global file pgauditlogtofile_calculate_filename(); pgauditlogtofile_close_file(); } if (!pgauditlogtofile_is_open_file()) { if (!pgauditlogtofile_open_file()) { // ERROR: unable to open file return false; } } return pgauditlogtofile_write_audit(edata, exclude_nchars); } /* * Close audit log file */ static inline void pgauditlogtofile_close_file(void) { if (file_handler) { fclose(file_handler); file_handler = NULL; } } /* * Checks if the audit log file is open */ static inline bool pgauditlogtofile_is_open_file(void) { if (file_handler) return true; else return false; } /* * Checks if a message starts with one of our intercept prefixes */ static inline bool pgauditlogtofile_is_prefixed(const char *msg) { bool found = false; size_t i; if (guc_pgaudit_log_connections) { for (i = 0; !found && i < pgaudit_log_shm->num_prefixes_connection; i++) { found = pg_strncasecmp(msg, pgaudit_log_shm->prefixes_connection[i]->prefix, pgaudit_log_shm->prefixes_connection[i]->length) == 0; } } if (!found && guc_pgaudit_log_disconnections) { for (i = 0; !found && i < pgaudit_log_shm->num_prefixes_disconnection; i++) { found = pg_strncasecmp(msg, pgaudit_log_shm->prefixes_disconnection[i]->prefix, pgaudit_log_shm->prefixes_disconnection[i]->length) == 0; } } return found; } /* * Checks if the audit log file needs to be rotated before we use it */ static bool pgauditlogtofile_needs_rotate_file(void) { /* Rotate if we are forcing */ if (pgaudit_log_shm->force_rotation && MyProc != NULL) { LWLockAcquire(pgaudit_log_shm->lock, LW_EXCLUSIVE); pgaudit_log_shm->force_rotation = false; LWLockRelease(pgaudit_log_shm->lock); return true; } /* Rotate if rotation_age is exceeded, and this backend is the first in notice * it */ if (guc_pgaudit_log_rotation_age > 0 && (pg_time_t)time(NULL) >= next_rotation_time) { pgauditlogtofile_calculate_next_rotation_time(); return true; } /* Rotate if the global name is different to this backend copy: it has been * rotated */ if (strcmp(filename_in_use, filename) != 0) { return true; } return false; } /* * Calculates next rotation time */ static void pgauditlogtofile_calculate_next_rotation_time(void) { pg_time_t now = (pg_time_t)time(NULL); struct pg_tm *tm = pg_localtime(&now, log_timezone); int rotinterval = guc_pgaudit_log_rotation_age * SECS_PER_MINUTE; /* Convert to seconds */ /* Calculate the new rotation date based in current date + rotation interval */ now += tm->tm_gmtoff; now -= now % rotinterval; /* try to get the o'clock hour of next rotation */ now += rotinterval; now -= tm->tm_gmtoff; next_rotation_time = now; } /* * Open audit log */ static bool pgauditlogtofile_open_file(void) { mode_t oumask; bool opened = true; /* Create spool directory if not present; ignore errors */ #if PG_MAJORVERSION_NUM < 11 mkdir(guc_pgaudit_log_directory, S_IRWXU); #else (void)MakePGDirectory(guc_pgaudit_log_directory); #endif /* * Note we do not let Log_file_mode disable IWUSR, since we certainly want * to be able to write the files ourselves. */ oumask = umask( (mode_t)((~(Log_file_mode | S_IWUSR)) & (S_IRWXU | S_IRWXG | S_IRWXO))); file_handler = fopen(filename, "a"); umask(oumask); if (file_handler) { /* 128K buffer and flush on demand or when full -> attempt to use only 1 IO operation per record */ setvbuf(file_handler, NULL, _IOFBF, 131072); #ifdef WIN32 /* use CRLF line endings on Windows */ _setmode(_fileno(file_handler), _O_TEXT); #endif // File open, we update the pgaudit_log_shm->filename we are using strcpy(filename_in_use, filename); } else { int save_errno = errno; opened = false; ereport(WARNING, (errcode_for_file_access(), errmsg("could not open log file \"%s\": %m", filename))); errno = save_errno; } return opened; } /* * Generates the name for the audit log file */ static void pgauditlogtofile_calculate_filename(void) { int len; pg_time_t current_rotation_time; if (guc_pgaudit_log_rotation_age > 0) { current_rotation_time = next_rotation_time - guc_pgaudit_log_rotation_age * SECS_PER_MINUTE; } else { current_rotation_time = (pg_time_t)time(NULL); } memset(filename, 0, sizeof(filename)); snprintf(filename, MAXPGPATH, "%s/", guc_pgaudit_log_directory); len = strlen(filename); /* treat Log_pgaudit_log_shm->filename as a strftime pattern */ pg_strftime(filename + len, MAXPGPATH - len, guc_pgaudit_log_filename, pg_localtime(¤t_rotation_time, log_timezone)); } /* * Writes an audit record in the audit log file */ static bool pgauditlogtofile_write_audit(const ErrorData *edata, int exclude_nchars) { StringInfoData buf; int rc; initStringInfo(&buf); /* create the log line */ pgauditlogtofile_create_audit_line(&buf, edata, exclude_nchars); fseek(file_handler, 0L, SEEK_END); rc = fwrite(buf.data, 1, buf.len, file_handler); pfree(buf.data); fflush(file_handler); /* If we failed to write the audit to our audit log, use PostgreSQL logger */ if (rc != buf.len) { int save_errno = errno; ereport(WARNING, (errcode_for_file_access(), errmsg("could not write audit log file \"%s\": %m", filename))); errno = save_errno; } return rc == buf.len; } /* * Formats an audit log line */ static void pgauditlogtofile_create_audit_line(StringInfo buf, const ErrorData *edata, int exclude_nchars) { bool print_stmt = false; /* static counter for line numbers */ static long log_line_number = 0; /* has counter been reset in current process? */ static int log_my_pid = 0; /* * This is one of the few places where we'd rather not inherit a static * variable's value from the postmaster. But since we will, reset it when * MyProcPid changes. */ if (log_my_pid != MyProcPid) { /* new session */ log_line_number = 0; log_my_pid = MyProcPid; /* start session timestamp */ pgauditlogtofile_format_start_time(); } log_line_number++; /* timestamp with milliseconds */ pgauditlogtofile_format_log_time(); appendStringInfoString(buf, formatted_log_time); appendStringInfoCharMacro(buf, ','); /* username */ if (MyProcPort && MyProcPort->user_name) appendStringInfoString(buf, MyProcPort->user_name); appendStringInfoCharMacro(buf, ','); /* database name */ if (MyProcPort && MyProcPort->database_name) appendStringInfoString(buf, MyProcPort->database_name); appendStringInfoCharMacro(buf, ','); /* Process id */ appendStringInfo(buf, "%d", log_my_pid); appendStringInfoCharMacro(buf, ','); /* Remote host and port */ if (MyProcPort && MyProcPort->remote_host) { appendStringInfoString(buf, MyProcPort->remote_host); if (MyProcPort->remote_port && MyProcPort->remote_port[0] != '\0') { appendStringInfoCharMacro(buf, ':'); appendStringInfoString(buf, MyProcPort->remote_port); } } appendStringInfoCharMacro(buf, ','); /* session id - hex representation of start time . session process id */ appendStringInfo(buf, "%lx.%x", (long)MyStartTime, log_my_pid); appendStringInfoCharMacro(buf, ','); /* Line number */ appendStringInfo(buf, "%ld", log_line_number); appendStringInfoCharMacro(buf, ','); /* PS display */ if (MyProcPort) { StringInfoData msgbuf; const char *psdisp; int displen; initStringInfo(&msgbuf); psdisp = get_ps_display(&displen); appendBinaryStringInfo(&msgbuf, psdisp, displen); appendStringInfoString(buf, msgbuf.data); pfree(msgbuf.data); } appendStringInfoCharMacro(buf, ','); /* session start timestamp */ appendStringInfoString(buf, formatted_start_time); appendStringInfoCharMacro(buf, ','); /* Virtual transaction id */ /* keep VXID format in sync with lockfuncs.c */ if (MyProc != NULL && MyProc->backendId != InvalidBackendId) appendStringInfo(buf, "%d/%u", MyProc->backendId, MyProc->lxid); appendStringInfoCharMacro(buf, ','); /* Transaction id */ appendStringInfo(buf, "%u", GetTopTransactionIdIfAny()); appendStringInfoCharMacro(buf, ','); /* SQL state code */ appendStringInfoString(buf, unpack_sql_state(edata->sqlerrcode)); appendStringInfoCharMacro(buf, ','); /* errmessage - PGAUDIT formatted text, +7 exclude "AUDIT: " prefix */ appendStringInfoString(buf, edata->message + exclude_nchars); appendStringInfoCharMacro(buf, ','); /* errdetail or errdetail_log */ if (edata->detail_log) appendStringInfoString(buf, edata->detail_log); else if (edata->detail) appendStringInfoString(buf, edata->detail); appendStringInfoCharMacro(buf, ','); /* errhint */ if (edata->hint) appendStringInfoString(buf, edata->hint); appendStringInfoCharMacro(buf, ','); /* internal query */ if (edata->internalquery) appendStringInfoString(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) appendStringInfoString(buf, edata->context); appendStringInfoCharMacro(buf, ','); /* user query --- only reported if not disabled by the caller */ if (debug_query_string != NULL && !edata->hide_stmt) print_stmt = true; if (print_stmt) appendStringInfoString(buf, debug_query_string); appendStringInfoCharMacro(buf, ','); if (print_stmt && edata->cursorpos > 0) appendStringInfo(buf, "%d", edata->cursorpos); appendStringInfoCharMacro(buf, ','); /* file error location */ if (Log_error_verbosity >= PGERROR_VERBOSE) { StringInfoData msgbuf; initStringInfo(&msgbuf); if (edata->funcname && edata->filename) appendStringInfo(&msgbuf, "%s, %s:%d", edata->funcname, edata->filename, edata->lineno); else if (edata->filename) appendStringInfo(&msgbuf, "%s:%d", edata->filename, edata->lineno); appendStringInfoString(buf, msgbuf.data); pfree(msgbuf.data); } appendStringInfoCharMacro(buf, ','); /* application name */ if (application_name) appendStringInfoString(buf, application_name); appendStringInfoCharMacro(buf, '\n'); } /* * Formats the session start time */ static inline void pgauditlogtofile_format_start_time(void) { /* * Note: we expect that guc.c will ensure that log_timezone is set up (at * least with a minimal GMT value) before Log_line_prefix can become * nonempty or CSV mode can be selected. */ pg_strftime(formatted_start_time, FORMATTED_TS_LEN, "%Y-%m-%d %H:%M:%S %Z", pg_localtime((pg_time_t *)&MyStartTime, log_timezone)); } /* * Formats the record time */ static inline void pgauditlogtofile_format_log_time(void) { struct timeval tv; char msbuf[5]; gettimeofday(&tv, NULL); /* * Note: we expect that guc.c will ensure that log_timezone is set up (at * least with a minimal GMT value) before Log_line_prefix can become * nonempty or CSV mode can be selected. */ pg_strftime(formatted_log_time, FORMATTED_TS_LEN, /* leave room for milliseconds... */ "%Y-%m-%d %H:%M:%S %Z", pg_localtime((pg_time_t *)&(tv.tv_sec), log_timezone)); /* 'paste' milliseconds into place... */ sprintf(msbuf, ".%03d", (int)(tv.tv_usec / 1000)); memcpy(formatted_log_time + 19, msbuf, 4); } /* * Identify when we are doing a shutdown */ static void pgauditlogtofile_shmem_shutdown(int code, Datum arg) { pgAuditLogToFileShutdown = true; } pgauditlogtofile-1.5.12/logtofile.h000066400000000000000000000011261443705515600173110ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * logtofile.h * pgaudit addon to redirect audit log lines to an independent file * * Copyright (c) 2020-2023, 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/ *------------------------------------------------------------------------- */ #ifndef PGAUDITLOGTOFILE_H #define PGAUDITLOGTOFILE_H /* initialization functions */ void _PG_fini(void); void _PG_init(void); #endif pgauditlogtofile-1.5.12/pgauditlogtofile--1.0--1.2.sql000066400000000000000000000003301443705515600221570ustar00rootroot00000000000000/* 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.5.12/pgauditlogtofile--1.0.sql000066400000000000000000000003041443705515600216050ustar00rootroot00000000000000/* 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.5.12/pgauditlogtofile--1.2--1.3.sql000066400000000000000000000003271443705515600221700ustar00rootroot00000000000000/* 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.5.12/pgauditlogtofile--1.3--1.4.sql000066400000000000000000000003271443705515600221720ustar00rootroot00000000000000/* 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.5.12/pgauditlogtofile--1.4--1.5.sql000066400000000000000000000003271443705515600221740ustar00rootroot00000000000000/* 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.5.12/pgauditlogtofile.c000066400000000000000000000010541443705515600206620ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pgauditlogtofile.c * pgaudit addon to redirect audit log lines to an independent file * * Copyright (c) 2020-2023, 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" PG_MODULE_MAGIC; #include "logtofile.h" pgauditlogtofile-1.5.12/pgauditlogtofile.control000066400000000000000000000002741443705515600221230ustar00rootroot00000000000000# pgauditlogtofile extension comment = 'pgAudit addon to redirect audit log to an independent file' default_version = '1.5' module_pathname = '$libdir/pgauditlogtofile' relocatable = true pgauditlogtofile-1.5.12/test/000077500000000000000000000000001443705515600161335ustar00rootroot00000000000000pgauditlogtofile-1.5.12/test/Vagrantfile000066400000000000000000000047151443705515600203270ustar00rootroot00000000000000Vagrant.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 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