Serilog.Sinks.Postgresql.Alternative is a Serilog sink that writes log events into a PostgreSQL
table. It is published as the NuGet package
Serilog.Sinks.Postgresql.Alternative,
so GeneratePackageOnBuild is on and every build drops a .nupkg and a .snupkg into
src/Serilog.Sinks.Postgresql.Alternative/bin/Release. The project is a maintained fork of
b00ted/serilog-sinks-postgresql.
One solution src/Serilog.Sinks.Postgresql.Alternative.sln with exactly three projects:
src/Serilog.Sinks.Postgresql.Alternative/Serilog.Sinks.Postgresql.Alternative.csproj, the library and the only packed project.src/Serilog.Sinks.Postgresql.Alternative.Tests/...csproj, MSTest, unit tests that need no database.src/Serilog.Sinks.Postgresql.Alternative.IntegrationTests/...csproj, MSTest, tests that write into a real PostgreSQL instance.
Layout inside src/Serilog.Sinks.Postgresql.Alternative:
LoggerConfigurationPostgreSQLExtensions.cs: the public entry point, fourPostgreSQLextension methods (two forLoggerSinkConfiguration, two forLoggerAuditSinkConfiguration) plus the internalGetOptionsandClearQuotationMarksFromColumnOptions. Everything a user configures ends up in aPostgreSqlOptions.Sinks/PostgreSQL/PostgreSQLSink.cs:IBatchedLogEventSink, hands batches to the sink helper.Sinks/PostgreSQL/PostgreSQLAuditSink.cs:ILogEventSink, writes single events.Sinks/PostgreSQL/SinkHelper.cs: the actual work, shared by both sinks. Opens the connection, creates schema and table on demand, writes viaCOPYorINSERT, deletes old rows when a retention time is set. The query buildersGetCopyCommand,GetInsertQueryandGetDeleteQueryeach do one thing, keep new SQL in that shape.Sinks/PostgreSQL/SchemaCreator.csandTableCreator.cs: theCREATE SCHEMAandCREATE TABLEstatements.SqlTypeHelper.cs:NpgsqlDbTypeto SQL type string.Sinks/PostgreSQL/ColumnWriters/:ColumnWriterBaseplus one writer per column kind.DefaultColumnWriteris not a writer at all, it is the DTO that JSON configuration binds to.Sinks/PostgreSQL/ColumnOptions.cs: the default column set.DefaultColumnNames.cs: its keys.Sinks/PostgreSQL/Configuration/: reads named connection strings out of anIConfiguration.Sinks/PostgreSQL/EventArgs/: the two callback argument types.Sinks/PostgreSQL/Async/:AsyncEventandAsyncEventInvocator, currently unused, see below.GlobalUsings.cs: all usings of the project, including the aliasSystemEventArgs.
Layout inside the test projects:
Serilog.Sinks.Postgresql.Alternative.Tests/ColumnWritersTests/: one test class per column writer, 12 tests in total, no database and no network needed.Serilog.Sinks.Postgresql.Alternative.IntegrationTests/:DbWriteTests,DbWriteWithSchemaTests, the twoJsonConfigTest*classes, the helperDbHelperandBaseTestswith the connection string. The fourPostgreSinkConfiguration*.jsonfiles are copied to the output directory withCopyToOutputDirectory=Always.
Repository root: README.md (badges, target frameworks, links), HowToUse.md (the actual user
documentation with all configuration options), Changelog.md, Updating.md (the five release
steps), License.txt (MIT), Icon.png, BuildAndPushPackage.bat, Delete-BIN-OBJ-Folders.bat,
.all-contributorsrc, .gitattributes and .gitignore. There is no .github folder and no
pipeline file.
dotnet build src/Serilog.Sinks.Postgresql.Alternative.slndotnet test src/Serilog.Sinks.Postgresql.Alternative.Tests/Serilog.Sinks.Postgresql.Alternative.Tests.csproj- The library multi-targets
net8.0;net10.0, the## Available forlist inREADME.mdhas to match. Both test projects are single targetnet10.0. - The test projects are on MSTest 4.
[ExpectedException]andAssert.ThrowsExceptiondo not exist there any more, useAssert.Throws<T>orAssert.ThrowsExactly<T>. - Restore needs nuget.org. A private feed is configured globally on this machine and answers
404 or refuses the connection for public packages, so a plain
dotnet buildfails withNU1301and, because warnings are errors, additionally withNU1900. Always build with an explicit source:dotnet build src/Serilog.Sinks.Postgresql.Alternative.sln -c Release --source https://api.nuget.org/v3/index.json. TreatWarningsAsErrorsis enabled in all three projects, so every warning breaks the build, NuGet warnings (NU****) from restore included. A clean build reports zero warnings, keep it that way.NU1803(HTTP source usage during restore) is the one warning suppressed viaNoWarn. Fix warnings instead of extending that list.NuGetAuditandNuGetAuditMode=allare on, so a vulnerable transitive package fails the build too.src/Directory.Build.propsexists but sets exactly one property,GenerateDocumentationFile. Every other build property is written out in each of the three.csprojfiles and duplicated there. Do not assume a property is inherited, check the csproj.- Versions come from GitVersion.MsBuild out of the git tags, for example
4.3.1-1for the first commit after tag4.3.0. Never edit a version property or an assembly version by hand. dotnet teston the solution also runs the 21 integration tests, which need a PostgreSQL server onlocalhost:5432with userpostgres, passwordpostgresand an existing databaseSerilog(seeIntegrationTests/BaseTests.cs). The schemasLogs2,Logs3andLogs4have to exist in that database as well, the test comments say so and onlyLogs1is created by the test that usesneedAutoCreateSchema. Without them two tests fail with3F000. For a quick check run the unit test project alone, that is the 12 tests above. Never claim a test run happened without running it.dotnet list package --outdatedignores--sourcefor its own restore step and therefore dies on the private feed. Queryhttps://api.nuget.org/v3-flatcontainer/<id>/index.jsoninstead.
Follow the surrounding code, it is consistent throughout every file:
- File header comment block with
<copyright file="..." company="SeppPenner and the Serilog contributors">and a<summary>, then the file-scoped namespace. - XML doc comments on every type and every member, private members included, no exceptions.
Implementations of an interface member additionally carry
<inheritdoc cref="..."/>and<seealso cref="..."/>pointing at that interface. Overrides ofColumnWriterBasedo the same against the base class. Nullable,ImplicitUsingsandLangVersion latestare enabled.- New
usingdirectives go into theGlobalUsings.csof the respective project, inside the existing#pragma warning disable IDE0065block, never at the top of a file. The editorconfig requires usings inside the namespace (csharp_using_directive_placement=inside_namespace:warning), which global usings cannot satisfy, that is what the pragma is for. Do not add other pragmas. The comment text in that block is German because Visual Studio generated it, leave it alone. - Fields, properties, methods and events are always accessed with
this.qualification (dotnet_style_qualification_for_*at severitywarning). src/.editorconfigalso enforces braces everywhere, no multiple blank lines, four spaces, CRLF, UTF-8, file scoped namespaces,Systemusings sorted first andIDE0005as warning. Analyzer warnings are fixed, not silenced.- Indentation inside the
.csprojfiles is four spaces, unlike the two spaces ofDirectory.Build.props.
Do not silently "clean up" these, they are existing behaviour:
- Three spellings of the same product. The package and assembly are
Serilog.Sinks.Postgresql.Alternative, the code namespace isSerilog.Sinks.PostgreSQLwith capitalSQL, and the test namespaces areSerilog.Sinks.Postgresql.Alternative.Testsand...IntegrationTests. TheRootNamespaceof the library is plainSerilogso thatLoggerConfigurationPostgreSqlExtensionslands in theSerilognamespace andWriteTo.PostgreSQL(...)works without an extra using. - File names do not match the type names.
PostgreSQLSink.csholdsPostgreSqlSink,PostgreSQLOptions.csholdsPostgreSqlOptions,LoggerConfigurationPostgreSQLExtensions.csholdsLoggerConfigurationPostgreSqlExtensions,IdAutoincrementColumnWriter.csholdsIdAutoIncrementColumnWriter. Some<copyright file="...">attributes use the type spelling rather than the real file name. This is the published public API, renaming files is churn and renaming types is a breaking change. - Two copyright headers.
LoggerConfigurationPostgreSQLExtensions.cscarries aSeppPenner and the Serilog contributorsblock and aTerumoBCTblock,PostgreSQLAuditSink.cscarries only theTerumoBCTone. That records who contributed the audit sink, leave it. Async/AsyncEvent.csandAsync/AsyncEventInvocator.csare public and unused. They are leftovers of thefailureCallbackthat version 4.2.0.0 deprecated and the commit "Removed failurecallback option." took out. Nothing in the library references them any more. They are public API, so deleting them is a breaking change and needs its own release note.- The audit sink blocks on purpose and every library await is
ConfigureAwait(false).PostgreSqlAuditSink.Emitimplements the synchronousILogEventSink.Emitand therefore waits onSinkHelper.EmitwithGetAwaiter().GetResult(), which is what lets an error reach the caller as Serilog'sAggregateException. That only stays deadlock free becauseSinkHelper,SchemaCreatorandTableCreatornever resume on a captured synchronization context. Any newawaitin the library needsConfigureAwait(false)for the same reason. Until version 4.3.0.0 the method wasasync void, which turned every failed write into an unhandled exception on the thread pool and took the whole process down. - The audit sink never uses
COPY. Both audit overloads passuseCopy: falseandperiod: TimeSpan.ZerotoGetOptions, the batched overloads default touseCopy: true. That is the point of the audit sink, one row per event, committed synchronously. TimestampColumnWriterignores thedbTypeyou pass. The parameterized constructor takes adbTypeand then overwritesthis.DbTypewithNpgsqlDbType.TimestampTzon purpose, see https://github.com/npgsql/npgsql/issues/2470.LevelColumnWriterdoes the same forNpgsqlDbType.TextwhenrenderAsTextis set.HowToUse.mdstill showsnew LevelColumnWriter(true, NpgsqlDbType.Varchar), which therefore writestext.- Identifiers are quoted, so table, schema and column names are case-sensitive.
TableCreator,GetCopyCommand,GetInsertQueryandGetDeleteQueryall wrap the names in double quotes, andClearQuotationMarksFromColumnOptionsplus theReplace("\"", string.Empty)calls inGetOptionsstrip quotes the user typed so they are not doubled. Any new query builder must quote the same way, an unquoted name is folded to lower case by PostgreSQL and then does not exist. - The default column names do not match the writer names.
DefaultColumnNames.RenderedMessageis"Message"andDefaultColumnNames.LogEventSerializedis"LogEvent".ColumnOptions.Defaultis an expression-bodied property, so every call hands out a fresh dictionary. IdAutoIncrementColumnWriter.GetValuethrows by design. ItsSkipOnInsertistrue, so the column never reaches an insert andGetValueis unreachable in normal operation. ItsGetSqlTypereturnsSERIAL PRIMARY KEYinstead of a plain type.- The two
PostgreSQLoverloads per sink kind differ only in their dictionary type. One takesIDictionary<string, ColumnWriterBase>, the other takesIDictionary<string, DefaultColumnWriter>plusIDictionary<string, SinglePropertyColumnWriter>for JSON configuration. A barenullargument is ambiguous, callers have to name the parameter.HowToUse.mdshows a positional example that relies on this. - JSON configuration can hand over a real
nullfor astringparameter. The extension methods are called through reflection bySerilog.Settings.Configuration, and since its version 10 a"schemaName": nullin the JSON no longer falls back to the parameter default.GetOptionstherefore treatstableNameandschemaNameas possibly null even though they are declared non-nullable. Every JSON sample inHowToUse.mdcontains thatnull. - JSON configuration silently drops unknown column writers. The
switchovercolumnOption.Value.Namein both JSON overloads has nodefaultbranch, so a typo in the config produces a missing column, not an error. The accepted names are listed inHowToUse.md. SqlTypeHelper.DefaultBitColumnsLength,DefaultCharColumnsLengthandDefaultVarcharColumnsLengthareconst. They cannot be changed at runtime, only recompiled. The "Adjusting column sizes" section ofHowToUse.mdshows assignments toTableCreator.Default*, which is wrong on both counts, wrong class and not settable.- A missing timestamp column only fails at the first flush.
GetDeleteQuerythrowsArgumentException("No timestamp column found.")when a retention time is configured but noTimestampColumnWriteris in the column options. That happens insideEmit, not at configuration time. - The origin remote is the old repository name.
git remote -vpoints athttps://github.com/SeppPenner/SerilogSinkForPostgreSQLwhile every link in the README, the changelog and the csproj usesserilog-contrib/Serilog.Sinks.Postgresql.Alternative. GitHub redirects, do not "fix" one side alone. PackageReleaseNotesduplicates the newestChangelog.mdentry. Both have to be updated for a release, seeUpdating.md.README.mdcontains generated blocks. TheALL-CONTRIBUTORS-BADGEandALL-CONTRIBUTORS-LISTsections and.all-contributorsrcbelong to the all-contributors bot, do not hand-edit them.src/Serilog.Sinks.Postgresql.Alternative.sln.DotSettingsis tracked and holds nothing but a ReSharper user dictionary (bytea,H_00E4mmer,Npgsql,Terumo, ...). Leave it alone..gitattributesis the Visual Studio template with everything commented out except* text=auto. There is no binary file in the repository that would need its own rule.
- Make the change.
- Add an entry at the top of
Changelog.mdin the existing format:* **Version 4.2.0.0 (2025-03-24)** : Short description. - Update
PackageReleaseNotesinsrc/Serilog.Sinks.Postgresql.Alternative/Serilog.Sinks.Postgresql.Alternative.csprojto the same text in its own format:Version 4.2.0.0 (2025-03-24): Short description. - Update the
## Available forlist inREADME.mdif the target frameworks changed. - Commit that.
- Tag the commit with the plain version number, no
vprefix (4.2.0,4.1.3, ...). The existing tags are lightweight tags, create new ones the same way. Tag before building the package, so that GitVersion does not bake a prerelease version into the shipped assembly. - Push the commits and the tag.
BuildAndPushPackage.batdeletes allbinandobjfolders, builds-c Releaseand pushes*.nupkgand*.snupkgto nuget.org with%NUGET_API_KEY%. It runsdotnet restorewithout an explicit source, so it needs the private feed to be reachable or removed from the machine configuration. Publishing to nuget.org is irreversible, only run it on request.
The version in Changelog.md and PackageReleaseNotes has four parts (4.2.0.0), the tag has
three (4.2.0). GitVersion turns the tag into the assembly and package version, so an untagged
commit produces something like 4.2.1-1.
- Never amend a commit. No
git commit --amend, not for a typo in the message, not to add a forgotten file, not even when the commit is still local. Write a follow-up commit instead. The release versions come from tags on exact commits, an amended commit leaves its tag pointing at a commit that no longer exists in the branch.
- Commit messages are written in English only: short, precise subject line, explanatory body when needed.
- Code comments and comments in project files such as
.csprojare always English, regardless of the language used in the conversation. - No em dashes or en dashes (
—,–), neither in prose, commit messages, code comments nor documentation. Use a regular hyphen, comma, colon, parentheses or a separate sentence. - German texts (documentation, chat replies) always use real umlauts and ß, never ASCII
transliterations such as
ae,oe,ueorss. Identifiers, file names and configuration keys stay unchanged where umlauts are technically undesirable.