Serilog.Sinks.Telegram.Alternative is a Serilog sink. It takes log events, renders them as HTML and
posts them to a Telegram chat through the bot API (sendMessage). The repository is published as
the NuGet package
Serilog.Sinks.Telegram.Alternative,
that package is the only deliverable. There is no application, no installer and no CI configuration
in this repository, no .github folder either.
One solution src/Serilog.Sinks.Telegram.Alternative.sln with exactly two projects:
src/Serilog.Sinks.Telegram.Alternative/Serilog.Sinks.Telegram.Alternative.csproj, the library,GeneratePackageOnBuildis on.src/Serilog.Sinks.Telegram.Alternative.Tests/Serilog.Sinks.Telegram.Alternative.Tests.csproj, MSTest.
Layout inside src/Serilog.Sinks.Telegram.Alternative:
LoggerConfigurationTelegramExtensions.cs: the public entry point, twoTelegramextension methods onLoggerSinkConfiguration. One takes the individual arguments, the other a preparedTelegramSinkOptions. The first builds the options and calls the second, so new options are added in both places plus inTelegramSinkOptions.Sinks/Telegram/Alternative/TelegramSinkOptions.cs: every configuration value, all read only exceptOutputTemplate,HttpClientandTopicId.Sinks/Telegram/Alternative/TelegramSink.cs: theIBatchedLogEventSink. Groups a batch, renders it and posts it. HoldsMaxMessageLength.Sinks/Telegram/Alternative/TelegramClient.cs: builds the API url and posts the JSON payload.Sinks/Telegram/Alternative/ExtendedLogEvent.cs: a log event plus first and last occurrence, used for the deduplication of repeated exceptions.Sinks/Telegram/Alternative/HtmlEscaper.csandStringExtensions.cs: the escaping. Everything that ends up in a message goes throughHtmlEscaper.Escape, never throughstring.HtmlEscapedirectly, because only the escaper honours a custom formatter.Sinks/Telegram/Alternative/TelegramPropertyNames.cs: the one property name the sink adds itself,ApplicationName.Sinks/Telegram/Alternative/Output/*: the renderers used when anoutputTemplateis configured, one per token type plusOutputTemplateRendererwhich parses the template once in its constructor and turns it into an array of render actions.GlobalUsings.cs: all usings of the project.
Namespaces do not follow the folders here, and that is deliberate, it is the public API:
LoggerConfigurationTelegramExtensionssits in namespaceSerilogso thatWriteTo.Telegramworks without an extra using.RootNamespaceisSerilogfor the same reason.- Everything under
Sinks/Telegram/Alternativeis namespaceSerilog.Sinks.Telegram.Alternative. - Everything under
Sinks/Telegram/Alternative/Outputis namespaceSerilog.Sinks.Telegram.Output, withoutAlternative. Folder and namespace disagree, renaming the namespace would be a breaking change for anyone implementingIPropertyRenderer.
Repository root: README.md (badges, target frameworks, links), HowToUse.md (the actual user
documentation with the option table and the JSON configuration sample), Changelog.md,
Updating.md (five lines describing the release), License.txt (MIT), Icon.png,
BuildAndPushPackage.bat, Delete-BIN-OBJ-Folders.bat and .all-contributorsrc. Icon.png,
License.txt, README.md and Changelog.md are packed into the nupkg from the root, they are not
copies inside src.
dotnet build src/Serilog.Sinks.Telegram.Alternative.sln -c Releasedotnet test src/Serilog.Sinks.Telegram.Alternative.sln- The library multi-targets
net8.0;net10.0, the test project isnet10.0only. Nothing in the code is platform specific. A target framework is dropped from this list once Microsoft stops supporting it,net9.0went out in version 1.6.1.0 for that reason.net8.0is the LTS release whose support ends in November 2026, it is the next candidate. src/Directory.Build.propssets exactly one property,GenerateDocumentationFile. Everything else lives in the two.csprojfiles and is duplicated there.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. BecauseGenerateDocumentationFileis on, a missing XML doc comment on a public member is such a warning and therefore an error.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.- Versions come from GitVersion.MsBuild out of the git tags, for example
1.5.1-1for the first commit after tag1.5.0. Never edit a version property or an assembly version by hand. - Restore needs nuget.org. If a private feed is configured globally on the machine and refuses the
connection, restore fails with
NU1301and, because ofTreatWarningsAsErrors, additionally withNU1900. Then build with an explicit source:dotnet build src/Serilog.Sinks.Telegram.Alternative.sln --source https://api.nuget.org/v3/index.json. GeneratePackageOnBuildis on, so every Release build writes a.nupkgand a.snupkgintosrc/Serilog.Sinks.Telegram.Alternative/bin/Release. Building is harmless, pushing is not, see Releasing.- The test project holds two kinds of tests and they behave very differently:
TelegramClientTests,TelegramSinkTestsandOutputTemplateTestsrun offline, 30 tests in total.TelegramApiMockanswers every url throughRichardSzalay.MockHttpand records the requests,RecordedRequestdecodes the JSON payload,TestDataProviderbuilds the options and the log events. New tests belong here, run them withdotnet test src/Serilog.Sinks.Telegram.Alternative.sln --filter "FullyQualifiedName!~TestSink".TestSinkis the old test class. Its methods read the environment variablesTelegramBotTokenandTelegramChatId, post to a real chat and assert nothing, they are eyeball tests. Without those variables the sink options constructor throws and the tests fail. Do not add to this class and never present a run of it as evidence that anything works.
- Never assert that several log events end up in one batch.
PeriodicBatchingSinkdecides that by timing, so a test that logs three events and expects one request fails in maybe one run out of four. Everything about batch behaviour hands the batch toTelegramSink.EmitBatchAsyncdirectly, only single events go through a logger.TelegramSinkand itsEmitBatchAsyncare public, which is what makes that possible. - Tests dispose their logger to flush the sink, they do not sleep.
Log.CloseAndFlushdoes nothing for a logger that was not assigned toLog.Logger, which is why the old tests sleep for a second each. - Never claim a test run happened without running it.
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 only exception is the test classTestSink, whose header still saysHämmer Electronics. - XML doc comments on every type and every member, private members and constants included, no
exceptions. Implementations of
IPropertyRenderercarry<inheritdoc cref="IPropertyRenderer"/>. 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.- Anything that reaches a message goes through
HtmlEscaper.Escape(options, text). Writing an unescaped user string into the output is the bug class this sink keeps running into, see the linked issues inChangelog.md.
Do not silently "clean up" these, they are existing behaviour:
- Messages longer than 4096 characters are dropped, not split.
TelegramSink.SendMessagewrites a line toSelfLogand returns. Splitting existed until version 1.4.3.0 and was removed because it tears HTML tags apart, see issue 33.MaxMessageLengthis public because the tests use it. - The minimum level is checked twice. Serilog filters by
restrictedToMinimumLevelbefore the sink sees anything, andEmitBatchAsyncfilters again byoptions.MinimumLogEventLevel. The convenience extension method passes the same value into both, a caller who buildsTelegramSinkOptionsby hand can set them apart. ApplicationNameis added to every event before the level check, so an event that the sink then discards has still been mutated.LogEvent.AddPropertyIfAbsentmutates the event that other sinks in the same pipeline also see.- Repeated exceptions are merged inside one batch. Two events whose
Exception.Messageis equal become oneExtendedLogEventwith a first and a last occurrence. The comparison is the message text only, not the type and not the stack trace. With the defaultbatchSizeLimitof 1 a batch never holds two events, so this never triggers unless the caller raises the limit. - The merge uses
else if. A later event updates eitherFirstOccurrenceorLastOccurrence, never both, and an event that lies inside the existing window updates neither. ExtendedLogEventloses the time zone. Its properties areDateTimeOffset, its constructor takesDateTime, and the sink passeslogEvent.Timestamp.DateTime. The offset therefore becomes the local one of the machine, whateverdateFormatsays withzzz.- The exception message is printed twice, once as
<strong>and once asMessage: <code>, both inTelegramSink.RenderMessageand inExceptionRenderer. That is the shipped message layout, users recognise it. TelegramSinkOptions.HttpClientdefaults to a freshHttpClientper options object and is never disposed. That is the documented way to plug in a proxy or a mock, see issue 30.TelegramSink.SendMessagebuilds a newTelegramClientfor every single message, but that client only wraps the sharedHttpClient, so no socket is leaked.customHtmlFormatteris only stored whenuseCustomHtmlFormattingis true. Passing the formatter alone silently does nothing.ShouldEscapeis nothing but the negation ofUseCustomHtmlFormatting.System.Net.Httpis gone and must not come back. The package was a legacy shim, onnet8.0and newer the types come from the shared framework. Adding the targetnet10.0turned it intoNU1510("PackageReference is not trimmed, consider removing"), and warnings are errors here, so it was removed in version 1.6.0.0. That shortened the dependency list of the published package.- The remotes do not match the project url.
originishttps://github.com/SeppPenner/Serilog.Sinks.Telegram, GitHub redirects it toserilog-contrib/Serilog.Sinks.Telegram.Alternative, which is the url in the.csprojand in every badge. There is a second remotejessicah-originpointing at the fork the sink originally came from. README.mdbelow## Contributorsand.all-contributorsrcare generated by the all-contributors bot. Edit the part above the contributors table only.TestSink.TestExceptionWithArrowSignandTestSink.TestExceptionWithBiggerSignare the same test, both log>Something, andTestExceptionWithCommaSignlogs a backtick, not a comma. Legacy names, they are not worth a rename that would break nothing but also help nobody.src/Serilog.Sinks.Telegram.Alternative.sln.DotSettingsis tracked and holds nothing but a ReSharper user dictionary. Leave it alone.
- Make the change.
- Add an entry at the top of
Changelog.mdin the existing format:* **Version 1.6.0.0 (2026-08-17)** : Short description. - Copy the same sentence into
PackageReleaseNotesinsrc/Serilog.Sinks.Telegram.Alternative/Serilog.Sinks.Telegram.Alternative.csproj. The property holds only the newest entry, not a history. - If the set of target frameworks changed, update the
## Available forlist inREADME.md. If an option changed, update the table inHowToUse.md. - Commit that, then tag the commit with the plain version number, no
vprefix (1.5.0,1.4.3, ...). The existing tags are lightweight tags, create new ones the same way. - Push the commits and the tag. The tag has to be pushed before the package is built, GitVersion
turns it into the assembly version and an untagged commit produces a prerelease version like
1.6.1-1+Branch.master.Sha...inside the nupkg. - Only then, and only when explicitly asked to publish, run
BuildAndPushPackage.bat. It deletes everybinandobjbelowsrc, builds Release and pushes both the.nupkgand the.snupkgto nuget.org with%NUGET_API_KEY%. A push to nuget.org cannot be taken back, the version is burned even after unlisting.
Never turn ECHO on in BuildAndPushPackage.bat. Cmd echoes a command with its variables
already expanded, so an echoed push line puts %NUGET_API_KEY% in plain text into the console and
into every log or screenshot of it. That was the case until version 1.6.1.0. The same holds for any
other place the key is passed, never write it into a file and never print it.
The version in Changelog.md and in PackageReleaseNotes has four parts (1.6.0.0), the tag has
three (1.6.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. - Never
git add -Aorgit add .in this repository. Working notes live untracked in the repository root and intim_local/, only the second one is ignored. Stage the files you changed by name.
- 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.