A backend handler for apple/swift-log that sends log entries to a PostgreSQL database using vapor/postgres-nio. It integrates seamlessly with the swift-server/swift-service-lifecycle for robust application lifecycle management, including graceful shutdown.
- SwiftLog Backend: Implements the
LogHandlerprotocol fromswift-log. - Asynchronous Processing: Uses an actor (
PostgresLogProcessor) to buffer log entries and perform database writes asynchronously, minimizing impact on application performance. - Buffering & Batching: Configurable maximum batch size and flush interval for efficient database insertion.
- Graceful Shutdown: Conforms to the
Serviceprotocol fromswift-service-lifecycle. During shutdown, it stops accepting new logs and ensures all buffered logs are written to the database before terminating. - PostgresNIO Integration: Leverages the non-blocking
PostgresNIOlibrary for database interaction. - Customizable: Allows specifying the target PostgreSQL table name.
- Metadata Support: Persists
Logger.Metadataas aJSONBcolumn in the database.
- Swift 6.1+ (TODO: could be 5.9)
- An application using
swift-log. - Access to a PostgreSQL database.
- Dependencies:
swift-log,postgres-nio,swift-service-lifecycle.
You need to create a table in your PostgreSQL database to store the log entries. You can choose any name for the table. Here is a recommended schema:
-- Choose a name for your table (e.g., 'logs', 'application_logs')
-- Replace 'logs' below with your chosen name if different.
CREATE TABLE logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Unique identifier for each log entry
label TEXT, -- Label of the Logger instance (e.g., 'App', 'DatabaseService')
server_timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the logger log method is called
level TEXT NOT NULL, -- Log level (e.g., 'debug', 'info', 'error')
message TEXT NOT NULL, -- The log message itself
metadata JSONB, -- Metadata associated with the log message
source TEXT, -- Source module of the log message (if provided by swift-log)
file TEXT, -- File where the log message originated
function TEXT, -- Function where the log message originated
line INTEGER, -- Line number where the log message originated
-- The 'created_at' column below captures the DB insertion time, different than the server_timestamp as the logs are recorded in batches
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Optional indexes for better query performance:
CREATE INDEX idx_logs_level ON logs (level);
CREATE INDEX idx_logs_server_timestamp ON logs (server_timestamp DESC);
CREATE INDEX idx_logs_level_server_timestamp ON logs (level, server_timestamp DESC);
CREATE INDEX idx_logs_label ON logs (label); -- If querying by logger label often
CREATE INDEX idx_logs_metadata ON logs USING gin (metadata); -- If querying JSONB metadata oftenRemember the table name you choose, as you'll need it when configuring the PostgresLogProcessor.
- Configure
PostgresClient: Set up your database connection usingPostgresNIO. - Configure
PostgresLogProcessor: Create an instance, providing thePostgresClient, your chosen table name, and optional batching/flushing parameters. This processor is anactorand conforms to theServiceprotocol. - Create Logger: Instantiate a
Loggerusing thePostgresLogHandlerfactory, passing the processor instance. - Run Services: Both the
PostgresClientandPostgresLogProcessorneed to be run. Typically, this is done using aServiceGroupfromswift-service-lifecycle. - Log Messages: Use the logger instance as you would with any
swift-loglogger.
let postgresClient = PostgresNIO.PostgresClient(/*...*/)
let logProcessor = PostgresLogProcessor(
configuration: .init(
postgresClient: postgresClient,
tableName: "logs", // *** Use the table name you created ***
maxBatchSize: 50, // Optional: Max logs per DB write
flushInterval: .seconds(5), // Optional: How often to write to DB
logger: bootstrapLogger // Logger for the processor's internal logs
)
)
let logger = Logger(
label: "postgresActorLogHandler",
factory: { label in
PostgresLogHandler(
label: label,
logLevel: Logger.Level.debug,
metadata: Logger.Metadata(),
processor: postgresLogProcessor
)
}
)
logger.info("Hello, Postgres!", metadata: ["Example": .string("metadata")])The PostgresLogProcessor.Configuration allows you to customize:
postgresClient: ThePostgresNIO.PostgresClientinstance. (Required)tableName: The name of your PostgreSQL log table. (Required)maxBatchSize: The maximum number of log entries to accumulate before forcing a flush to the database. Defaults to100.flushInterval: The maximum time interval between database flushes, even ifmaxBatchSizeis not reached. Defaults to5seconds.logger: An optional separateLoggerinstance to log the internal operations of thePostgresLogProcessoritself (e.g., for debugging).
- Metadata provided directly to a log call (
logger.info("Message", metadata: ["key": "value"])) is merged with any metadata attached to thePostgresLogHandlerinstance itself. - The combined metadata is stored in the
metadataJSONB column in your PostgreSQL table. - If metadata is empty or
nil, themetadatacolumn will beNULL.
When the ServiceGroup initiates a shutdown (e.g., upon receiving SIGINT or SIGTERM), the PostgresLogProcessor:
- Immediately stops accepting new log entries via
enqueueLog. - Signals its internal processing loop to stop after the current sleep/flush cycle.
- Performs a final flush of any remaining entries in its buffer to the database.
- Shuts down cleanly.
This ensures that logs generated right before shutdown are not lost.
Add LoggingToPostgres as a dependency to your Package.swift file:
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "YourPackageName",
platforms: [
.macOS(.v13) // Or your target platform
],
dependencies: [
.package(url: "https://github.com/atacan/postgres-for-swift-log.git", from: "0.0.1"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.2"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"),
.package(url: "https://github.com/vapor/postgres-nio.git", from: "1.25.0"),
// ... other dependencies
],
targets: [
.target(
name: "YourTargetName",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "PostgresNIO", package: "postgres-nio"),
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
.product(name: "LoggingToPostgres", package: "postgres-for-swift-log"),
// ... other dependencies
]
),
// ... other targets
]
)