A Go application that tails PostgreSQL JSON logs and routes audit logs to GCP Cloud Logging while streaming other logs to stdout.
Built with Chainguard Images - Uses minimal, secure, and regularly updated base images from Chainguard for enhanced security and reduced attack surface.
- Tails JSON-formatted log files continuously
- Detects audit logs (messages starting with "AUDIT:") and sends them to GCP Cloud Logging
- Streams non-audit logs to stdout
- Automatically retrieves GCP project ID from parent namespace annotation
- Extracts cluster name from pod metadata and adds it as database label in GCP logs
- Extracts additional fields (statement_class, user, table) from log entries as labels
- Handles log rotation automatically - detects when PostgreSQL rotates logs and reopens the file
- Sends logs to default log bucket (routing via log sinks can be configured in GCP)
- Graceful shutdown handling
- Seeks to end of file on startup to avoid reprocessing old logs
log-tailer <log-file-pattern>The application will automatically fetch the GCP project ID from the parent namespace annotation.
log-tailer <log-file-pattern> --project-id <gcp-project-id>When --project-id is provided, the application runs in local mode and skips Kubernetes API calls.
log-tailer <log-file-pattern> --project-id <gcp-project-id> --dry-runIn dry-run mode, audit logs are printed to stdout with [DRY-RUN AUDIT] prefix instead of being sent to GCP. Regular logs are prefixed with [DRY-RUN STDOUT]. This is useful for testing without GCP credentials.
<log-file-pattern>: Glob pattern for log files to tail (required, e.g.,/var/log/postgresql*.log)--project-id: GCP project ID (optional, for local testing; if not set, will be fetched from parent namespace)--dry-run: Print audit logs to stdout instead of sending to GCP
Kubernetes mode:
log-tailer "/home/postgres/pgdata/pgroot/pg_log/postgresql*.json"Local mode:
log-tailer "/var/log/postgresql*.json" --project-id my-gcp-projectDry run mode:
log-tailer "/var/log/postgresql*.json" --project-id my-gcp-project --dry-runThe application automatically determines the GCP project ID by:
- Reading the pod's namespace (e.g.,
pg-example) - Extracting the parent namespace name (e.g.,
example) - Reading the
cnrm.cloud.google.com/project-idannotation from the parent namespace - Sending audit logs to the default log bucket in that project
Log sinks can be configured in GCP to route logs to specific buckets based on filters.
The application handles PostgreSQL log rotation automatically:
- Every 5 seconds, it checks if the log file has been rotated
- Detection methods:
- Checks if the file inode has changed (different file)
- Checks if the file size has decreased (truncation)
- On rotation detection:
- Closes the old file handle
- Opens the new log file
- Continues tailing from the beginning of the new file
This ensures the sidecar continues to work indefinitely without pod restarts, even as PostgreSQL rotates logs (typically daily or when size limits are reached).
The application requires:
-
Environment variables:
POD_NAME: Name of the podPOD_NAMESPACE: Namespace of the pod (must followpg-*format)
-
Pod labels:
cluster-name: Name of the PostgreSQL cluster (used as database name in GCP logs)
-
Parent namespace:
- Must have annotation
cnrm.cloud.google.com/project-idwith the GCP project ID
- Must have annotation
Audit logs are sent to GCP Cloud Logging with the following structure:
Resource Labels (resource.labels):
database_id: Set to the cluster name from pod labelcluster-nameproject_id: Set to the GCP project IDlocation: "global"namespace: "postgres-audit"node_id: Set to the cluster name
Entry Labels (labels) - Extracted from log entry:
database_id: Cluster name from pod labelcluster-nameuser: Database user from root-leveluserfielddbname: Database name from root-leveldbnamefieldaudit_type: Audit type (SESSION, OBJECT, etc.) parsed from messagestatement_class: Statement class (READ, WRITE, etc.) parsed from messagecommand: SQL command (SELECT, INSERT, UPDATE, DELETE, etc.) parsed from messagebackend_type: Backend type (client backend, etc.) from root-level field
Payload: The full log entry JSON is sent as the payload.
Resource Type: generic_node
The application parses PostgreSQL audit logs with the following format:
AUDIT: {type},{session_line},{statement_id},{class},{command},{object_type},{object_name},{query},{params}
Example:
AUDIT: SESSION,15,1,READ,SELECT,,,SELECT pg_database_size($1),<not logged>
Extracted fields:
- Index 0:
audit_type(SESSION, OBJECT, etc.) - Index 3:
statement_class(READ, WRITE, etc.) - Index 4:
command(SELECT, INSERT, UPDATE, DELETE, etc.)
For local development and testing, you can run the application with Docker or nerdctl by providing the project ID explicitly:
Use the included test script:
./test-local.sh your-gcp-project-idThis will build the image and run it with the included test-logs.json file.
# Build the image
docker build -t log-tailer:local .
# Run with project ID for local testing
docker run --rm \
-v ~/.config/gcloud/application_default_credentials.json:/gcp/credentials.json:ro \
-e GOOGLE_APPLICATION_CREDENTIALS=/gcp/credentials.json \
-v /path/to/log-file.json:/logs/postgresql.json:ro \
log-tailer:local \
-log-file /logs/postgresql.json \
-project-id your-gcp-project-id# Build the image
nerdctl build -t log-tailer:local .
# Run with project ID for local testing
nerdctl run --rm \
-v ~/.config/gcloud/application_default_credentials.json:/gcp/credentials.json:ro \
-e GOOGLE_APPLICATION_CREDENTIALS=/gcp/credentials.json \
-v /path/to/log-file.json:/logs/postgresql.json:ro \
log-tailer:local \
-log-file /logs/postgresql.json \
-project-id your-gcp-project-id# Using the included test log file
docker run --rm \
-v ~/.config/gcloud/application_default_credentials.json:/gcp/credentials.json:ro \
-e GOOGLE_APPLICATION_CREDENTIALS=/gcp/credentials.json \
-v $(pwd)/test-logs.json:/logs/test.json:ro \
log-tailer:local \
-log-file /logs/test.json \
-project-id my-test-projectNote: When running locally with -project-id, the application will not attempt to connect to the Kubernetes API, making it suitable for testing without cluster access. You'll need to mount your GCP credentials file as shown above.
Run the automated integration tests:
mise run test-integrationFor verbose output showing each test:
mise run test-integration -- --verboseThe integration tests verify:
- Regular and AUDIT log entry capture
- Multiple files with glob patterns
- New file detection
- File truncation handling
- File deletion and re-creation
- Rapid consecutive entries
Use the interactive test for manual verification:
mise run test-interactiveCommands:
1- Add a regular log entry2- Add an AUDIT log entry3- Add 5 regular entries quickly4- Add 5 AUDIT entries quickly5- Show tailer output (last 30 lines)6- Show test file sizes7- Add a new log fileq- Quit and cleanup
mise run testUses mise for tooling. Configuration is in mise/config.toml.
mise install # Install dependencies
mise run build # Build the application
mise run test # Run unit tests
mise run test-integration # Run integration tests
mise run test-interactive # Run interactive test
mise run lint # Run staticcheck
mise run format # Format code with gofumpt
mise run check # Run all checks (test, lint, vulncheck, security)
mise run all # Run all checks plus format