Serilog.Sinks.AmazonS3 is a Serilog sink that buffers log events, writes them into a local file and
uploads that file to an Amazon S3 bucket. The batching itself is not implemented here, it comes from
the NuGet package Serilog.Sinks.PeriodicBatching,
and the rolling file name logic is a modified copy of
Serilog.Sinks.File. The repository is published
as the NuGet package Serilog.Sinks.AmazonS3: GeneratePackageOnBuild is on and
BuildAndPushPackage.bat pushes the result to nuget.org.
One solution src/Serilog.Sinks.AmazonS3.sln with exactly two projects:
src/Serilog.Sinks.AmazonS3/Serilog.Sinks.AmazonS3.csproj, the library and the package.src/Serilog.Sinks.AmazonS3.Tests/Serilog.Sinks.AmazonS3.Tests.csproj, MSTest.
Layout inside src/Serilog.Sinks.AmazonS3:
LoggerConfigurationAmazonS3Extensions.cs: the public entry point, tenAmazonS3overloads onLoggerSinkConfiguration. They differ in three dimensions:RegionEndpointvsserviceUrlvs a ready madeAmazonS3Client, with or without credentials, andoutputTemplateplusformatProvidervs a ready madeITextFormatter. Every overload does its own argument checking, builds anAmazonS3Options, wraps the sink in aPeriodicBatchingSinkand returnssinkConfiguration.Sink(...). New parameters therefore have to be added ten times, that repetition is the price of the overload matrix.Sinks/AmazonS3/AmazonS3Sink.cs: the sink itself, anIBatchedLogEventSink.EmitBatchAsyncopens the file, formats the batch into it, uploads it and deletes it.OpenFile,AlignCurrentFileTo,GetFileNameandUploadFileToS3do one thing each, keep new logic in that shape.Sinks/AmazonS3/AmazonS3Options.cs: all options in one class, including the three propertiesPathRoller,NextCheckpointandCurrentFileSequencethat are internal state and documented as "not to be set by the options".Sinks/AmazonS3/PathRoller.cs,RollingInterval.cs,RollingIntervalExtensions.cs,RollingLogFile.cs: the rolling file name logic taken from Serilog.Sinks.File. These four files carry the Apache 2.0 header of the Serilog contributors on top of the MIT header, do not replace it.Sinks/AmazonS3/FileInformation.cs: the file name and writer pair thatOpenFilereturns.Sinks/AmazonS3/ConfigurationValidator.csplusErrorMessageConstants.cs: the service url check, bothinternal.GlobalUsings.cs: all usings of the project.
Layout inside src/Serilog.Sinks.AmazonS3.Tests:
AmazonS3BasicTests.cs: five integration tests against a real bucket, driven by the environment variablesAwsAccessKeyId,AwsSecretAccessKeyandAwsBucketName. They report an inconclusive result when those are missing. Read "Known quirks" before you trust a green run.AmazonS3SinkTests.cs: the sink end to end without a network.AmazonS3Client.PutObjectAsyncis virtual, soFakeAmazonS3Clientoverrides it and captures bucket name, key and bytes. These tests pin the file names of consecutive batches, the bucket path in front of the key, the deletion of the local file and the encoding.PathRollerTests.csandRollingIntervalExtensionsTests.cs: the rolling file name logic.LoggerConfigurationAmazonS3ExtensionsTests.cs: what the ten overloads refuse. Every call there needs enough named arguments to pick one overload,outputTemplateorformatterusually does it, otherwise the call is ambiguous.GlobalUsings.cs: all usings of the test project.
Repository root: README.md (badges, target frameworks, install), HowToUse.md (the actual usage
documentation with all configuration options), Changelog.md, Updating.md (five lines on the
release process), License.txt (MIT), PolicyExample.json (a minimal S3 bucket policy),
Icon.png, .all-contributorsrc, BuildAndPushPackage.bat, Delete-BIN-OBJ-Folders.bat and
.gitattributes. README.md, HowToUse.md, Changelog.md, License.txt, PolicyExample.json
and Icon.png are packed into the NuGet package, so a change to them changes the package.
dotnet build src/Serilog.Sinks.AmazonS3.slndotnet test src/Serilog.Sinks.AmazonS3.sln- The library multi targets
net8.0;net10.0, the test project targetsnet10.0only. Both values live in the two.csprojfiles, there is noTargetFrameworksproperty anywhere else. Keep the test project on the newest framework the library targets. src/Directory.Build.propsexists but contains nothing exceptGenerateDocumentationFile. All other build properties live directly in the two.csprojfiles and are duplicated there. That one property covers the test project as well, so a test class without XML documentation breaks the build withCS1591, which counts as an error here.TreatWarningsAsErrorsis enabled in both 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. The NuGet audit runs with its defaults, so a vulnerable package fails the build too.- Versions come from GitVersion.MsBuild out of the git tags, for example
1.6.1-1for the first commit after tag1.6.0. Never edit a version property or an assembly version by hand. - Restore needs nuget.org. A private feed is configured globally on this machine
(
http://192.168.201.22:5000/v3/index.json) and is not always reachable, restore then fails withNU1301andNU1900. Then build with an explicit source:dotnet build src/Serilog.Sinks.AmazonS3.sln --source https://api.nuget.org/v3/index.json.dotnet testdoes not accept--source, restore first and then run it with--no-restore. - Tests are MSTest, in the single test project
src/Serilog.Sinks.AmazonS3.Tests, which follows the same package set as the sibling repositories:Microsoft.NET.Test.Sdk,MSTest.TestAdapter,MSTest.TestFrameworkandGitVersion.MsBuild. Never claim a test run happened without running it. - The package is built by every
dotnet buildbecause ofGeneratePackageOnBuild. The.nupkgand the.snupkgland insrc/Serilog.Sinks.AmazonS3/bin/<configuration>and are whatBuildAndPushPackage.batpushes.
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. The four files taken from Serilog.Sinks.File additionally carry the Apache 2.0 notice inside that block. - XML doc comments on every type and every member, private members included, no exceptions. Public
parameters of the
AmazonS3overloads are documented with<param>in the same wording in all ten overloads, keep them in sync. 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 except theSerilog004ones the test project already uses. 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.- The
RootNamespaceof the library isSerilog, not the assembly name. The extension class lives in namespaceSerilogso thatWriteTo.AmazonS3(...)works without an extra using, everything else lives inSerilog.Sinks.AmazonS3.
Do not silently "clean up" these, they are existing behaviour:
- A green
AmazonS3BasicTestsstill proves very little. Those five tests contain noAssert, they configure a logger, write 200 events and dispose it. Since the sink swallows every error (see below), they stay green whether the upload worked or not. They report an inconclusive result when the environment variables are missing, which at least separates "not configured" from "ran". The real assertions live in the other four test classes, which need neither credentials nor network. Never present a green run as proof that an upload arrived in the bucket. - Every exception inside the sink is swallowed.
PeriodicBatchingSinkcatches whateverEmitBatchAsyncthrows and writes it to Serilog'sSelfLog. A failing upload, missing credentials or a wrong bucket name are invisible unlessSelfLog.Enable(...)is called, which is exactly whatClassInitializein the test class does. That is also why the sink can throw freely instead of returning error codes. - A new file per batch, not per rolling interval.
EmitBatchAsynccallsAlignCurrentFileTo(DateTime.Now, true), always withnextSequence: true, so every batch gets the next sequence number and its own file, for examplelog20260817_001.txt,log20260817_002.txt. The rolling interval only controls the date part of the name. Thereturn string.Emptybranch inAlignCurrentFileTois therefore unreachable from the sink, it is a leftover of the Serilog file sink logic. - The first batch has no sequence number, and
RollingInterval.Infinitehas none at all.AlignCurrentFileToreturns the plain name as long asNextCheckpointis not set yet, so the keys of the first batches arelog20260817.txt,log20260817_001.txt,log20260817_002.txt. WithRollingInterval.Infinitethere is no checkpoint at all, so every batch is uploaded aslog.txtand overwrites the object of the batch before it.AmazonS3SinkTestspins the first half of that, the second half is a trap, not a feature. - The local file is a temporary file. It is written next to
path, uploaded and deleted right after. The bucket key isbucketPathplus the file name with backslashes replaced by slashes, the local directory structure never reaches S3. pathis a name pattern, not a file that is kept. Callers passlog.txtand getlog<date>_<sequence>.txton S3,PathRollersplits the name into prefix and extension and puts the period and the sequence number in between.- An empty file is refused.
UploadFileToS3throwsInvalidOperationExceptionwhen the file length is 0 instead of uploading it, because S3 would happily store an empty object. disablePayloadSigningalso turns off the SDK checksums. Since AWS SDK version 4 a checksum is calculated for every request by default. The providers that need the payload signing switched off, Cloudflare R2 for example, usually reject those checksums as well, soGetOrCreateClientsetsRequestChecksumCalculation.WHEN_REQUIREDon the config it builds. That only applies to a client the sink creates itself, a client passed in through the configuration keeps whatever its own config says.Serilog.Debuggingsits in the library'sGlobalUsings.csunused.SelfLogis only used in the test project. Unused global usings are not reported byIDE0005.- Ten overloads, one implementation. The bodies of the ten
AmazonS3extension methods are copies of each other. That is deliberate, the overloads exist so that the sink can be configured fromappsettings.json, where Serilog matches parameter names. - Every call needs
outputTemplateorformatterby name. The template overload and the formatter overload of a pair differ only in optional parameters, so a call that names neither isCS0121, ambiguous.outputTemplate: nullis enough, the sink falls back to its default template. That is not theory, two samples inHowToUse.mdwere written without it and did not compile. When you touch a sample or write a test, compile it. .gitattributessets* text=autoand every rule of the Visual Studio template below it is commented out. There is no binary file in this repository that needs its own rule,Icon.pngis detected by git itself.src/Serilog.Sinks.AmazonS3.sln.DotSettingsis tracked and holds nothing but a ReSharper user dictionary (amazonaws,destructors,Flushable,H_00E4mmer,Sepp,Serilog). Leave it alone.- No CI in the repository. There is no
.githubfolder and no pipeline file, the badges in the README point at services configured outside of this repository.
- Make the change.
- Add an entry at the top of
Changelog.mdin the existing format:* **Version 1.7.0.0 (2026-08-17)** : Short description. - Update
PackageReleaseNotesinsrc/Serilog.Sinks.AmazonS3/Serilog.Sinks.AmazonS3.csprojto the same text. It is the only place in the build files that names a version, and it is duplicated from the changelog by hand. - Commit that.
- Tag the commit with the plain version number, no
vprefix (1.6.0,1.5.3, ...). The existing tags are lightweight tags, create new ones the same way. - Push the commits and the tag.
- Only then build the package, and build it while the tag is the newest commit. GitVersion takes
the version from the tag plus the distance to it, so a build before the tag or after another
commit produces a prerelease version like
1.7.1-3+Branch.master.Sha..., and that version would end up on nuget.org.git describe --tagshas to answer with the plain tag. BuildAndPushPackage.batdeletes allbinandobjfolders, builds in release and pushes the.nupkgand the.snupkgto nuget.org with%NUGET_API_KEY%. It ends withPAUSE, so it wants a console. Because it always rebuilds, it is only correct as long as no commit sits on top of the tag. If one does, push the packages that were built from the tagged state instead:dotnet nuget push *.nupkg -s "nuget.org" --skip-duplicate -k %NUGET_API_KEY%insrc/Serilog.Sinks.AmazonS3/bin/Release. That command takes the matching.snupkgwith it, the second push line of the script is a no-op then.
The version in the Changelog.md has four parts (1.7.0.0), the tag has three (1.7.0).
- 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.