A modern, high-performance Serilog sink for SQLite databases. Built for .NET 8+ with full AnyCPU support.
- AnyCPU compatible - Uses
Microsoft.Data.Sqlite(no native SQLite required) - .NET 8.0, 9.0 & 10.0 support
- Asynchronous batching - Optimal performance through batch writing
- Automatic retention - By time, count, or database size
- Custom columns - Store structured data in dedicated columns
- WAL mode - Optimized for high write load
- Thread-safe - Fully suitable for parallel logging
- Configurable - Extensive options for every use case
dotnet add package Raycoon.Serilog.Sinks.SQLiteusing Serilog;
var logger = new LoggerConfiguration()
.WriteTo.SQLite("logs/app.db")
.CreateLogger();
logger.Information("Hello, SQLite!");
logger.Error(new Exception("Oops!"), "An error occurred");
// Important: Dispose the logger at the end
await Log.CloseAndFlushAsync();using Serilog;
using Serilog.Events;
using Raycoon.Serilog.Sinks.SQLite.Options;
var logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.WriteTo.SQLite("logs/app.db", options =>
{
// Table name
options.TableName = "ApplicationLogs";
// Retention: Delete logs older than 30 days
options.RetentionPeriod = TimeSpan.FromDays(30);
// Retention: Keep maximum 100,000 entries
options.RetentionCount = 100_000;
// Retention: Database max. 100 MB
options.MaxDatabaseSize = 100 * 1024 * 1024;
// Performance tuning
options.BatchSizeLimit = 200;
options.BatchPeriod = TimeSpan.FromSeconds(1);
options.QueueLimit = 50000;
// SQLite optimizations
options.JournalMode = SQLiteJournalMode.Wal;
options.SynchronousMode = SQLiteSynchronousMode.Normal;
// Store timestamps in UTC
options.StoreTimestampInUtc = true;
// Minimum log level for this sink
options.RestrictedToMinimumLevel = LogEventLevel.Information;
})
.CreateLogger();Store structured data in dedicated columns for better queries:
var logger = new LoggerConfiguration()
.WriteTo.SQLite("logs/app.db", options =>
{
options.CustomColumns.Add(new CustomColumn
{
ColumnName = "UserId",
DataType = "TEXT",
PropertyName = "UserId",
CreateIndex = true // Index for fast searches
});
options.CustomColumns.Add(new CustomColumn
{
ColumnName = "RequestId",
DataType = "TEXT",
PropertyName = "RequestId"
});
options.CustomColumns.Add(new CustomColumn
{
ColumnName = "Duration",
DataType = "REAL",
PropertyName = "DurationMs"
});
})
.CreateLogger();
// Usage
logger
.ForContext("UserId", "user123")
.ForContext("RequestId", Guid.NewGuid())
.ForContext("DurationMs", 42.5)
.Information("Request processed");var logger = new LoggerConfiguration()
.WriteTo.SQLite("logs/app.db", options =>
{
options.OnError = ex =>
{
Console.WriteLine($"SQLite Error: {ex.Message}");
// Or: Use a fallback logger
};
// Throw exception on critical errors
options.ThrowOnError = false; // Default: false
})
.CreateLogger();The sink supports full configuration via appsettings.json using Serilog.Settings.Configuration:
dotnet add package Serilog.Settings.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json{
"Serilog": {
"Using": ["Raycoon.Serilog.Sinks.SQLite"],
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "SQLite",
"Args": {
"databasePath": "logs/app.db"
}
}
]
}
}{
"Serilog": {
"Using": ["Raycoon.Serilog.Sinks.SQLite"],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "SQLite",
"Args": {
"databasePath": "logs/app.db",
"tableName": "ApplicationLogs",
"restrictedToMinimumLevel": "Information",
"storeTimestampInUtc": true,
"autoCreateDatabase": true,
"storePropertiesAsJson": true,
"storeExceptionDetails": true,
"maxMessageLength": 10000,
"maxExceptionLength": 20000,
"maxPropertiesLength": 10000,
"batchSizeLimit": 200,
"batchPeriod": "00:00:01",
"queueLimit": 50000,
"retentionPeriod": "30.00:00:00",
"retentionCount": 100000,
"maxDatabaseSize": 104857600,
"cleanupInterval": "01:00:00",
"journalMode": "Wal",
"synchronousMode": "Normal",
"throwOnError": false,
"customColumns": [
{
"columnName": "UserId",
"dataType": "TEXT",
"propertyName": "UserId",
"allowNull": true,
"createIndex": true
},
{
"columnName": "RequestId",
"dataType": "TEXT",
"propertyName": "RequestId",
"allowNull": true,
"createIndex": false
},
{
"columnName": "Duration",
"dataType": "REAL",
"propertyName": "DurationMs",
"allowNull": true,
"createIndex": false
}
]
}
}
],
"Enrich": ["FromLogContext"]
}
}using Microsoft.Extensions.Configuration;
using Serilog;
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
var logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();| Format | Example | Description |
|---|---|---|
hh:mm:ss |
"00:00:02" |
2 seconds |
hh:mm:ss.fff |
"00:00:00.500" |
500 milliseconds |
d.hh:mm:ss |
"7.00:00:00" |
7 days |
d.hh:mm:ss |
"30.00:00:00" |
30 days |
| Property | Valid Values |
|---|---|
journalMode |
"Delete", "Truncate", "Persist", "Memory", "Wal", "Off" |
synchronousMode |
"Off", "Normal", "Full", "Extra" |
restrictedToMinimumLevel |
"Verbose", "Debug", "Information", "Warning", "Error", "Fatal" |
Note: This is a fundamental limitation of the Serilog ecosystem, not specific to this sink. All Serilog sinks face this constraint because
Serilog.Settings.Configurationcan only bind serializable types from JSON.
The following options are not available via JSON configuration:
OnErrorcallback (delegates cannot be serialized)AdditionalConnectionParameters(dictionary binding is complex)
Option 1: Full Programmatic Configuration (Recommended when callbacks are needed)
Use the action-based overload for complete control:
var logger = new LoggerConfiguration()
.WriteTo.SQLite("logs/app.db", options =>
{
// All settings configured programmatically
options.TableName = "ApplicationLogs";
options.RetentionPeriod = TimeSpan.FromDays(30);
options.BatchSizeLimit = 200;
options.JournalMode = SQLiteJournalMode.Wal;
// Callback options (not available via JSON)
options.OnError = ex => Console.WriteLine($"SQLite error: {ex.Message}");
options.AdditionalConnectionParameters["Password"] = "mySecurePassword";
})
.CreateLogger();Option 2: Hybrid Approach (JSON for other sinks, programmatic for SQLite)
If you need JSON configuration for other parts of your logging pipeline:
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration) // Loads MinimumLevel, other sinks, enrichers
.WriteTo.SQLite("logs/app.db", options =>
{
// SQLite-specific settings with callback support
options.TableName = "ApplicationLogs";
options.OnError = ex => MyErrorHandler(ex);
})
.CreateLogger();Why can't I just add callbacks to a JSON-configured sink?
Serilog's architecture creates sinks during
ReadFrom.Configuration()and doesn't expose them for modification afterwards. This is by design for thread-safety and immutability.
The sink automatically creates the following table:
| Column | Type | Description |
|---|---|---|
Id |
INTEGER | Primary key (auto-increment) |
Timestamp |
TEXT | ISO 8601 timestamp |
Level |
INTEGER | Log level (0-5) |
LevelName |
TEXT | Log level name |
Message |
TEXT | Rendered message |
MessageTemplate |
TEXT | Original message template |
Exception |
TEXT | Exception details (if present) |
Properties |
TEXT | Properties as JSON |
SourceContext |
TEXT | Logger name / source |
MachineName |
TEXT | Computer name |
ThreadId |
INTEGER | Thread ID |
Plus all configured custom columns.
-- All errors from the last 24 hours
SELECT * FROM Logs
WHERE Level >= 4
AND Timestamp > datetime('now', '-1 day')
ORDER BY Timestamp DESC;
-- Logs by UserId (if custom column configured)
SELECT * FROM Logs
WHERE UserId = 'user123'
ORDER BY Timestamp DESC
LIMIT 100;
-- Aggregation by level
SELECT LevelName, COUNT(*) as Count
FROM Logs
GROUP BY Level;
-- Search through properties (JSON)
SELECT * FROM Logs
WHERE json_extract(Properties, '$.RequestId') = 'abc123';options.BatchSizeLimit = 500; // For high-volume
options.BatchPeriod = TimeSpan.FromMilliseconds(100);options.JournalMode = SQLiteJournalMode.Wal;// Faster, but less safe in case of power failure
options.SynchronousMode = SQLiteSynchronousMode.Normal;
// Or for maximum performance (only if data loss is acceptable)
options.SynchronousMode = SQLiteSynchronousMode.Off;// Prevents memory overflow during burst traffic
options.QueueLimit = 100000;| Feature | Raycoon.Serilog.Sinks.SQLite | Serilog.Sinks.SQLite |
|---|---|---|
| AnyCPU Support | Yes (Microsoft.Data.Sqlite) | No (System.Data.SQLite) |
| .NET Support | .NET 8 / 9 / 10 | .NET Standard 2.0 / 7 (last update 2023) |
| Async Batching | Yes | Yes |
| Retention Policies | Yes (time, count, size) | Partial (time, size) |
| Custom Columns | Yes | No |
| WAL Mode | Yes | No |
| Property | Type | Default | Description |
|---|---|---|---|
DatabasePath |
string | "logs.db" | Path to the SQLite database file |
TableName |
string | "Logs" | Name of the log table |
AutoCreateDatabase |
bool | true | Auto-create database file if not exists |
| Property | Type | Default | Description |
|---|---|---|---|
RestrictedToMinimumLevel |
LogEventLevel | Verbose | Minimum log level to capture |
StoreTimestampInUtc |
bool | true | Store timestamps in UTC (false = local time) |
StorePropertiesAsJson |
bool | true | Store log event properties as JSON |
StoreExceptionDetails |
bool | true | Store exception details in separate column |
| Property | Type | Default | Description |
|---|---|---|---|
MaxMessageLength |
int? | null | Max rendered message length (null = unlimited) |
MaxExceptionLength |
int? | null | Max exception text length (null = unlimited) |
MaxPropertiesLength |
int? | null | Max properties JSON length (null = unlimited) |
| Property | Type | Default | Description |
|---|---|---|---|
BatchSizeLimit |
int | 100 | Max events per batch write |
BatchPeriod |
TimeSpan | 2s | Interval between batch writes |
QueueLimit |
int? | 10000 | Max events in memory queue (null = unlimited) |
| Property | Type | Default | Description |
|---|---|---|---|
RetentionPeriod |
TimeSpan? | null | Delete logs older than this (null = disabled) |
RetentionCount |
long? | null | Keep only this many logs (null = disabled) |
MaxDatabaseSize |
long? | null | Max database size in bytes (null = disabled) |
CleanupInterval |
TimeSpan | 1h | Interval for retention cleanup checks |
| Property | Type | Default | Description |
|---|---|---|---|
JournalMode |
SQLiteJournalMode | Wal | SQLite journal mode (Wal recommended) |
SynchronousMode |
SQLiteSynchronousMode | Normal | SQLite synchronous mode |
AdditionalConnectionParameters |
IDictionary<string, string> | {} | Extra SQLite connection string parameters |
| Property | Type | Default | Description |
|---|---|---|---|
OnError |
Action<Exception>? | null | Callback invoked on write errors |
ThrowOnError |
bool | false | Throw exceptions on write errors (false = suppress) |
| Property | Type | Default | Description |
|---|---|---|---|
CustomColumns |
Collection<CustomColumn> | [] | Custom columns mapped from log event properties |
CustomColumn Properties:
| Property | Type | Default | Description |
|---|---|---|---|
ColumnName |
string | "" | Name of the database column |
DataType |
string | "TEXT" | SQLite data type (TEXT, INTEGER, REAL, BLOB) |
PropertyName |
string | "" | Serilog property to extract the value from |
AllowNull |
bool | true | Allow NULL values in the column |
CreateIndex |
bool | false | Create an index on this column |
Apache 2.0 - See LICENSE for details.