├── .all-contributorsrc ├── .gitattributes ├── .gitignore ├── BuildAndPushPackage.bat ├── Changelog.md ├── Delete-BIN-OBJ-Folders.bat ├── HowToUse.md ├── Icon.png ├── License.txt ├── README.md ├── Updating.md └── src ├── .editorconfig ├── Directory.Build.props ├── Serilog.Sinks.MicrosoftTeams.Alternative.Tests ├── Extensions │ └── WireMockExtensions.cs ├── GlobalUsings.cs ├── MicrosoftTeamsSinkTest.cs ├── Serilog - Backup.Sinks.MicrosoftTeams.Alternative.Tests.csproj ├── Serilog.Sinks.MicrosoftTeams.Alternative.Tests.csproj ├── TestException.txt ├── TestHelper.cs └── appsettings.TwoChannelsExample.json ├── Serilog.Sinks.MicrosoftTeams.Alternative.sln ├── Serilog.Sinks.MicrosoftTeams.Alternative.sln.DotSettings └── Serilog.Sinks.MicrosoftTeams.Alternative ├── GlobalUsings.cs ├── LoggerConfigurationMicrosoftTeamsExtensions.cs ├── Serilog.Sinks.MicrosoftTeams.Alternative.csproj └── Sinks └── MicrosoftTeams └── Alternative ├── Constants └── SinkConstants.cs ├── Core ├── MicrosoftTeamsAttachment.cs ├── MicrosoftTeamsMessage.cs ├── MicrosoftTeamsMessageAction.cs ├── MicrosoftTeamsMessageActionTarget.cs ├── MicrosoftTeamsMessageActionTargetUri.cs ├── MicrosoftTeamsMessageCard.cs ├── MicrosoftTeamsMessageFact.cs └── MicrosoftTeamsMessageSection.cs ├── Enumerations └── MicrosoftTeamsColors.cs ├── Exceptions └── LoggingException.cs ├── Extensions └── EnumerableExtensions.cs ├── MicrosoftExtendedLogEvent.cs ├── MicrosoftTeamsSink.cs ├── MicrosoftTeamsSinkChannelHandlerOptions.cs ├── MicrosoftTeamsSinkOptions.cs └── MicrosoftTeamsSinkOptionsButton.cs /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "ruisantos", 10 | "name": "ruisantos", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/218613?v=4", 12 | "profile": "https://github.com/ruisantos", 13 | "contributions": [ 14 | "code", 15 | "doc" 16 | ] 17 | }, 18 | { 19 | "login": "Appelg", 20 | "name": "Appelg", 21 | "avatar_url": "https://avatars.githubusercontent.com/u/50763504?v=4", 22 | "profile": "https://github.com/Appelg", 23 | "contributions": [ 24 | "code", 25 | "doc" 26 | ] 27 | }, 28 | { 29 | "login": "maestroMike", 30 | "name": "maestroMike", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/2403754?v=4", 32 | "profile": "https://github.com/maestroMike", 33 | "contributions": [ 34 | "code", 35 | "doc" 36 | ] 37 | }, 38 | { 39 | "login": "SeppPenner", 40 | "name": "HansM", 41 | "avatar_url": "https://avatars.githubusercontent.com/u/9639361?v=4", 42 | "profile": "https://franzhuber23.blogspot.de/", 43 | "contributions": [ 44 | "code", 45 | "doc", 46 | "example", 47 | "maintenance", 48 | "projectManagement", 49 | "test" 50 | ] 51 | }, 52 | { 53 | "login": "serilog-contrib", 54 | "name": "Serilog Contrib", 55 | "avatar_url": "https://avatars.githubusercontent.com/u/78050538?v=4", 56 | "profile": "https://github.com/serilog-contrib", 57 | "contributions": [ 58 | "code", 59 | "doc", 60 | "example", 61 | "maintenance", 62 | "projectManagement", 63 | "test" 64 | ] 65 | }, 66 | { 67 | "login": "nelsonroliveira", 68 | "name": "Nelson Oliveira", 69 | "avatar_url": "https://avatars.githubusercontent.com/u/10381502?v=4", 70 | "profile": "https://github.com/nelsonroliveira", 71 | "contributions": [ 72 | "code", 73 | "doc" 74 | ] 75 | }, 76 | { 77 | "login": "mggrand", 78 | "name": "Goman", 79 | "avatar_url": "https://avatars.githubusercontent.com/u/32844137?v=4", 80 | "profile": "https://goman.ch", 81 | "contributions": [ 82 | "code" 83 | ] 84 | }, 85 | { 86 | "login": "veraw", 87 | "name": "Wayne Vera", 88 | "avatar_url": "https://avatars.githubusercontent.com/u/2069157?v=4", 89 | "profile": "https://github.com/veraw", 90 | "contributions": [ 91 | "code", 92 | "doc", 93 | "test" 94 | ] 95 | }, 96 | { 97 | "login": "jordan-reato", 98 | "name": "jordan-reato", 99 | "avatar_url": "https://avatars.githubusercontent.com/u/48877695?v=4", 100 | "profile": "https://github.com/jordan-reato", 101 | "contributions": [ 102 | "doc" 103 | ] 104 | } 105 | ], 106 | "contributorsPerLine": 7, 107 | "projectName": "Serilog.Sinks.MicrosoftTeams.Alternative", 108 | "projectOwner": "serilog-contrib", 109 | "repoType": "github", 110 | "repoHost": "https://github.com", 111 | "skipCi": true, 112 | "commitConvention": "angular", 113 | "commitType": "docs" 114 | } 115 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #Ignore thumbnails created by Windows 3 | Thumbs.db 4 | #Ignore files built by Visual Studio 5 | *.obj 6 | *.exe 7 | *.pdb 8 | *.user 9 | *.aps 10 | *.pch 11 | *.vspscc 12 | *_i.c 13 | *_p.c 14 | *.ncb 15 | *.suo 16 | *.tlb 17 | *.tlh 18 | *.bak 19 | *.cache 20 | *.ilk 21 | *.log 22 | [Bb]in 23 | [Dd]ebug*/ 24 | *.lib 25 | *.sbr 26 | obj/ 27 | [Rr]elease*/ 28 | _ReSharper*/ 29 | [Tt]est[Rr]esult* 30 | .vs/ 31 | #Nuget packages folder 32 | packages/ 33 | private-packages/ 34 | InstallFiles.wxs 35 | node_modules/ 36 | package-lock.json -------------------------------------------------------------------------------- /BuildAndPushPackage.bat: -------------------------------------------------------------------------------- 1 | cd .\src 2 | 3 | @ECHO off 4 | cls 5 | 6 | FOR /d /r . %%d in (bin,obj) DO ( 7 | IF EXIST "%%d" ( 8 | ECHO %%d | FIND /I "\node_modules\" > Nul && ( 9 | ECHO.Skipping: %%d 10 | ) || ( 11 | ECHO.Deleting: %%d 12 | rd /s/q "%%d" 13 | ) 14 | ) 15 | ) 16 | 17 | @ECHO on 18 | @ECHO.Building solution... 19 | @dotnet restore 20 | @dotnet build -c Release 21 | @cd .\Serilog.Sinks.MicrosoftTeams.Alternative\bin\Release 22 | @ECHO.Build successful. 23 | dotnet nuget push *.nupkg -s "nuget.org" --skip-duplicate -k "%NUGET_API_KEY%" 24 | dotnet nuget push *.snupkg -s "nuget.org" --skip-duplicate -k "%NUGET_API_KEY%" 25 | @ECHO.Upload success. Press any key to exit. 26 | PAUSE -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | Change history 2 | -------------- 3 | 4 | * **Version 1.5.0.0 (2025-03-24)** : Fixes https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative/issues/83, updates NuGet packages, deprecates `failureCallback`. 5 | * **Version 1.4.9.0 (2024-12-26)**: Fixed issue with Serilog.Configuration, fixes https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/pull/37. Smaller changes in docs. 6 | * **Version 1.4.8.0 (2024-12-11)**: Removed support for Net6.0, added support for Net9.0, updated NuGet packages, fixes https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/issues/33. Add option to use PowerAutomate Workflow and update card JSON structure. 7 | * **Version 1.4.7.0 (2024-06-04)**: Updated NuGet packages, fixes https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/issues/32. 8 | * **Version 1.4.6.0 (2024-05-16)**: Removed support for Net7.0. 9 | * **Version 1.4.5.0 (2024-04-22)**: Updated NuGet packages, fixes https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/issues/31. 10 | * **Version 1.4.4.0 (2024-03-03)**: Updated NuGet packages. 11 | * **Version 1.4.3.0 (2023-11-30)**: Added custom exception to handle error codes better in failure callback. 12 | * **Version 1.4.2.0 (2023-11-30)**: Fixed issue with System.Runtime, https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/issues/30. 13 | * **Version 1.4.1.0 (2023-11-23)**: Tried to fix issue with System.Runtime, https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/issues/30. 14 | * **Version 1.4.0.0 (2023-11-21)**: Updated NuGet packages, removed support for NetCore3.1, removed support for netstandard, added support for Net8.0. 15 | * **Version 1.3.0.0 (2022-11-20)**: Updated NuGet packages, removed support for Net5.0, added support for Net7.0. 16 | * **Version 1.2.20.0 (2022-10-30)** : Updated NuGet packages. 17 | * **Version 1.2.19.0 (2022-10-18)** : Wrap exception into code block (Thanks to [mggrand](https://github.com/mggrand)), updated NuGet packages. 18 | * **Version 1.2.18.0 (2022-08-31)** : Updated NuGet packages. 19 | * **Version 1.2.17.0 (2022-08-03)** : Updated NuGet packages. 20 | * **Version 1.2.16.0 (2022-06-01)** : Updated NuGet packages, added filter on property and multi channel support. 21 | * **Version 1.2.15.0 (2022-04-03)** : Updated NuGet packages, hopefully fixes https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/issues/21. 22 | * **Version 1.2.14.0 (2022-03-02)** : Just a new version because of a NuGet upload issue. 23 | * **Version 1.2.13.0 (2022-02-02)** : Invalid version, something went wrong with NuGet. Don't use this version. 24 | * **Version 1.2.12.0 (2022-02-16)** : NuGet packages updated, added nullable checks, added editorconfig, added file scoped namespaces, added global usings, removed native support for Net Framework (Breaking change), fixed Serilog PeriodicBatchingSink issues. 25 | * **Version 1.2.11.0 (2022-01-12)** : NuGet packages updated. 26 | * **Version 1.2.10.0 (2021-11-09)** : NuGet packages updated, added support for Net6.0. 27 | * **Version 1.2.9.0 (2021-11-04)** : Updated NuGet packages, removed support for netstandard2.0. 28 | * **Version 1.2.8.0 (2021-09-30)** : Smaller changes, added new sink that uses Utf8Json and is only available for NetFramework 4.8. 29 | * **Version 1.2.7.0 (2021-09-12)** : Added functionality to format title with a template (Breaking change!), smaller changes due to move to serilog-contrib organization. 30 | * **Version 1.2.6.0 (2021-09-03)** : Updated license to fit the new owning repository, updated readme and so on to fit new package name. 31 | * **Version 1.2.5.0 (2021-08-09)** : Removed support for soon deprecated NetCore 2.1. 32 | * **Version 1.2.4.0 (2021-07-25)** : Updated NuGet packages, enabled source linking for debugging. 33 | * **Version 1.2.3.0 (2021-06-04)** : Updated NuGet packages. 34 | * **Version 1.2.2.0 (2021-04-29)** : Updated NuGet packages. 35 | * **Version 1.2.1.0 (2021-02-21)** : Updated NuGet packages. 36 | * **Version 1.2.0.0 (2021-01-04)** : Updated NuGet packages, updated project files, updated logging for exceptions in self log, simplified test classes, added hint to reference from Microsoft, added option to use code tags for complex messages to avoid strange formatting. 37 | * **Version 1.1.1.0 (2020-11-19)** : Added output template. 38 | * **Version 1.1.0.0 (2020-11-19)** : Updated NuGet packages, moved to Net 5.0, added possibility to add static buttons to each message (Thanks to [Appelg](https://github.com/Appelg)), added failure callback. 39 | * **Version 1.0.11.0 (2020-07-29)** : Fixed a bug where the omitPropertiesSection flag wasn't set when configuration was read. 40 | * **Version 1.0.10.0 (2020-07-25)** : Fixed a logical error with the messagesToSend list (Thanks to [mahdiman](https://github.com/mahdiman)). 41 | * **Version 1.0.9.0 (2020-07-15)** : Updated NuGet packages, fixed a null reference exception (Thanks to [mahdiman](https://github.com/mahdiman)). 42 | * **Version 1.0.8.0 (2020-06-05)** : Updated NuGet packages, adjusted build to Visual Studio, moved changelog to extra file. 43 | * **Version 1.0.7.1 (2020-06-04)** : Updated NuGet packages, added option for proxy (Thanks to [ruisantos78](https://github.com/ruisantos78)). 44 | * **Version 1.0.7.0 (2020-05-10)** : Updated NuGet packages. 45 | * **Version 1.0.6.0 (2020-03-26)** : Updated NuGet packages. 46 | * **Version 1.0.5.0 (2019-11-08)** : Updated NuGet packages. 47 | * **Version 1.0.3.2 (2019-11-04)** : Adjusted license to the MIT license. 48 | * **Version 1.0.3.1 (2019-10-22)** : Removed invalid fields from nuspec file, added dependency information to NuGet package, added build for netcore. 49 | * **Version 1.0.3.0 (2019-10-15)** : Added option for omitting properties section in message (Thanks to [maestroMike](https://github.com/maestroMike)), added GitVersionTask, updated NuGet packages. 50 | * **Version 1.0.2.1 (2019-06-24)** : Added option to only show from and to dates when the dates are not equal. 51 | * **Version 1.0.2.0 (2019-06-23)** : Fixed icon in NuGet package. 52 | * **Version 1.0.0.1 (2019-06-21)** : Added option for minimal log level. 53 | * **Version 1.0.0.0 (2019-06-21)** : 1.0 release. 54 | -------------------------------------------------------------------------------- /Delete-BIN-OBJ-Folders.bat: -------------------------------------------------------------------------------- 1 | @ECHO off 2 | cls 3 | 4 | ECHO Deleting all BIN and OBJ folders... 5 | ECHO. 6 | 7 | FOR /d /r . %%d in (bin,obj) DO ( 8 | IF EXIST "%%d" ( 9 | ECHO %%d | FIND /I "\node_modules\" > Nul && ( 10 | ECHO.Skipping: %%d 11 | ) || ( 12 | ECHO.Deleting: %%d 13 | rd /s/q "%%d" 14 | ) 15 | ) 16 | ) 17 | 18 | ECHO. 19 | ECHO.BIN and OBJ folders have been successfully deleted. Press any key to exit. 20 | pause > nul -------------------------------------------------------------------------------- /HowToUse.md: -------------------------------------------------------------------------------- 1 | ## Basic usage 2 | You need to add an "Incoming Webhook" connector to your Teams channel and get it's URL. `titleTemplate` is optional but can help your distinguish logs coming from different sources. 3 | Check https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/connectors/connectors-using. 4 | 5 | ```csharp 6 | var logger = new LoggerConfiguration() 7 | .WriteTo.MicrosoftTeams(webHookUri, titleTemplate: titleTemplate) 8 | .CreateLogger(); 9 | ``` 10 | 11 | The project can be found on [nuget](https://www.nuget.org/packages/Serilog.Sinks.MicrosoftTeams.Alternative/). 12 | 13 | ## Configuration options 14 | |Parameter|Meaning|Example|Default value| 15 | |-|-|-|-| 16 | |webHookUri|The Microsoft teams weebhook uri.|`https://outlook.office.com/webhook/1234567890`|None, is mandatory.| 17 | |titleTemplate|The title template of the card. The Serilog templating can be used to customize the title (Same as the `outputTemplate` property).|`"Some Message"`|None, but is optional.| 18 | |batchSizeLimit|The maximum number of events to include in a single batch.|`batchSizeLimit: 40`|`1`| 19 | |period|The time to wait between checking for event batches.|`period: new TimeSpan(0, 0, 20)`|`00:00:01`| 20 | |outputTemplate|The output template for a log event.|`outputTemplate:"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"`|`null`| 21 | |formatProvider|The `IFormatProvider` to use. Supplies culture-specific formatting information. Check https://docs.microsoft.com/en-us/dotnet/api/system.iformatprovider?view=net-5.0.|`new CultureInfo("de-DE")`|`null`| 22 | |restrictedToMinimumLevel|The minimum level of the logging.|`restrictedToMinimumLevel: LogEventLevel.Verbose`|`LogEventLevel.Verbose`| 23 | |proxy|The proxy addresss used.|`proxy: "http://test.de/proxy"`|`null`| 24 | |omitPropertiesSection|Indicates whether the properties section should be omitted or not.|`omitPropertiesSection: true`|`false`| 25 | |useCodeTagsForMessage|A value indicating whether code tags are used for the message template or not. This is useful if you have complex messages that might get formatted as unwanted markdown elements.|`useCodeTagsForMessage:true`|`false`| 26 | |usePowerAutomateWorkflows|A value indicating whether Power Automate workflows are used or not.|`usePowerAutomateWorkflows:true`|`false`| 27 | |buttons|Option to add static clickable buttons to each message.|`buttons: new[] { new MicrosoftTeamsSinkOptionsButton("Google", "https://google.de") }`|`null`| 28 | |~~failureCallback~~|~~Adds an option to add a failure callback action.~~ (Deprecated, use fallback logging instead.Check https://nblumhardt.com/2024/10/fallback-logging/.)|~~`failureCallback: e => Console.WriteLine($"Sink error: {e.Message}")`~~|~~`null`~~| 29 | |queueLimit|The maximum number of events that should be stored in the batching queue.|`queueLimit: 10`|`int.MaxValue` or `2147483647`| 30 | |channelHandler|Configuration for dispatching events to multiple channels.|See [Support for multiple channels](#support-for-multiple-channels)|`null`| 31 | 32 | ### Support for multiple channels 33 | It's possible to send messages for multiple channels based on the value 34 | of a property for the event. 35 | 36 | |Parameter|Meaning|Default value| 37 | |-|-|-| 38 | |filterOnProperty|Send **only** the events that have a property with this name.|`null`| 39 | |channelList|Mapping for the target channels Uri and the filter property value. If the filter property for the event is not in this list, the webHookUri will be used.|`null`| 40 | 41 | Example configuration: 42 | 43 | ```json 44 | { 45 | "Serilog": { 46 | "Using": [ "Serilog.Sinks.MicrosoftTeams.Alternative" ], 47 | "MinimumLevel": "Debug", 48 | "WriteTo": [ 49 | { 50 | "Name": "MicrosoftTeams", 51 | "Args": { 52 | "webHookUri": "http://example.com/", 53 | "channelHandler": 54 | { 55 | "filterOnProperty": "MsTeams", 56 | "channelList": { 57 | "ITTeam": "http://example.com/ITTeam/", 58 | "SupportTeam": "http://example.com/SupportTeam/" 59 | } 60 | } 61 | } 62 | } 63 | ] 64 | } 65 | } 66 | ``` 67 | 68 | > **Note**: filterOnProperty can be used with an empty channelList to send 69 | > only some log events to Teams. 70 | 71 | #### Adding the required property for filterOnProperty to events 72 | 73 | Once filterOnProperty is set, some events will need to be marked to be sent 74 | to Teams. 75 | 76 | Using Serilog: 77 | 78 | ```csharp 79 | var loggerForChannel = logger.ForContext("filterOnPropertyValue", "ChannelName"); 80 | loggerForChannel.Information("Hello"); 81 | ``` 82 | 83 | Using Microsoft ILogger: 84 | 85 | ```csharp 86 | using (logger.BeginScope( 87 | new Dictionary { ["filterOnPropertyValue"] = "ChannelName" })) 88 | { 89 | logger.LogInformation("Hello"); 90 | } 91 | ``` 92 | 93 | ## Further information 94 | This project is a fork of https://github.com/DixonDs/serilog-sinks-teams but is maintained. 95 | Do not hesitate to create [issues](https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/issues) or [pull requests](https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/pulls). 96 | The relevant reference from Microsoft can be found here: https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference. -------------------------------------------------------------------------------- /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/2f3e3c0891c7007cba23cad88d63f9a07ed4f0f7/Icon.png -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) SeppPenner (https://github.com/SeppPenner) and the Serilog contributors (https://github.com/serilog-contrib). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Serilog.Sinks.MicrosoftTeams.Alternative 2 | ==================================== 3 | 4 | Serilog.Sinks.MicrosoftTeams.Alternative is a library to save logging information from [Serilog](https://github.com/serilog/serilog) to [Microsoft Teams](https://products.office.com/en-us/microsoft-teams/group-chat-software). 5 | 6 | [![GitHub issues](https://img.shields.io/github/issues/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative.svg)](https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/issues) 7 | [![GitHub forks](https://img.shields.io/github/forks/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative.svg)](https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/network) 8 | [![GitHub stars](https://img.shields.io/github/stars/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative.svg)](https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/stargazers) 9 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://raw.githubusercontent.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/master/License.txt) 10 | [![Nuget](https://img.shields.io/badge/Serilog.Sinks.MicrosoftTeams.Alternative-Nuget-brightgreen.svg)](https://www.nuget.org/packages/Serilog.Sinks.MicrosoftTeams.Alternative/) 11 | [![NuGet Downloads](https://img.shields.io/nuget/dt/Serilog.Sinks.MicrosoftTeams.Alternative.svg)](https://www.nuget.org/packages/Serilog.Sinks.MicrosoftTeams.Alternative/) 12 | [![Known Vulnerabilities](https://snyk.io/test/github/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/badge.svg)](https://snyk.io/test/github/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative) 13 | [![Gitter](https://img.shields.io/matrix/Serilog-Sinks-MicrosoftTeams_community%3Agitter.im?server_fqdn=matrix.org)](https://matrix.to/#/#Serilog-Sinks-MicrosoftTeams_community:gitter.im) 14 | 15 | [![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors-) 16 | 17 | 18 | ## Available for 19 | * Net 8.0 20 | * Net 9.0 21 | 22 | ## Net Core and Net Framework latest and LTS versions 23 | * https://dotnet.microsoft.com/download/dotnet 24 | 25 | ## Basic usage 26 | Check out the how to use file [here](https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/blob/master/HowToUse.md). 27 | 28 | ## Install 29 | ```bash 30 | dotnet add package Serilog.Sinks.MicrosoftTeams.Alternative 31 | ``` 32 | 33 | Change history 34 | -------------- 35 | 36 | See the [Changelog](https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/blob/master/Changelog.md). 37 | 38 | ## Contributors ✨ 39 | 40 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
ruisantos
ruisantos

💻 📖
Appelg
Appelg

💻 📖
maestroMike
maestroMike

💻 📖
HansM
HansM

💻 📖 💡 🚧 📆 ⚠️
Serilog Contrib
Serilog Contrib

💻 📖 💡 🚧 📆 ⚠️
Nelson Oliveira
Nelson Oliveira

💻 📖
Goman
Goman

💻
Wayne Vera
Wayne Vera

💻 📖 ⚠️
jordan-reato
jordan-reato

📖
62 | 63 | 64 | 65 | 66 | 67 | 68 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 69 | -------------------------------------------------------------------------------- /Updating.md: -------------------------------------------------------------------------------- 1 | ## How to update the package 2 | 3 | 1. Add your changes. 4 | 2. Update the release notes in the *.csproj file. 5 | 3. Update version using a Git tag. 6 | 4. Build the project with Visual Studio. 7 | 5. Upload nuget package. -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = crlf 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | csharp_using_directive_placement=inside_namespace:warning 9 | csharp_prefer_braces=true:warning 10 | dotnet_style_allow_multiple_blank_lines_experimental=false:warning 11 | dotnet_style_allow_statement_immediately_after_block_experimental=false:warning 12 | dotnet_style_qualification_for_field=true:warning 13 | dotnet_style_qualification_for_property=true:warning 14 | dotnet_style_qualification_for_method=true:warning 15 | dotnet_style_qualification_for_event=true:warning 16 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental=false:warning 17 | dotnet_sort_system_directives_first=true:warning 18 | dotnet_diagnostic.IDE0005.severity=warning 19 | 20 | [*.cs] 21 | csharp_style_namespace_declarations = file_scoped:warning -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests/Extensions/WireMockExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // An extension class for WireMock tests. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Tests.Extensions; 11 | 12 | /// 13 | /// An extension class for WireMock tests. 14 | /// 15 | internal static class WireMockExtensions 16 | { 17 | /// 18 | /// Adds a default channel to the WireMock server. 19 | /// 20 | /// The WireMock server. 21 | public static void AddDefaultChannel(this WireMockServer server) 22 | { 23 | server.Given( 24 | Request 25 | .Create() 26 | .WithPath("/") 27 | .WithHeader("content-type", "application/json; charset=utf-8") 28 | .UsingPost() 29 | ) 30 | .RespondWith( 31 | Response 32 | .Create() 33 | .WithStatusCode(200) 34 | ); 35 | } 36 | 37 | /// 38 | /// Adds a channel to the WireMock server. 39 | /// 40 | /// The WireMock server. 41 | /// The channel. 42 | public static void AddChannel(this WireMockServer server, string channel) 43 | { 44 | server.Given( 45 | Request 46 | .Create() 47 | .WithPath($"/{channel}/") 48 | .WithHeader("content-type", "application/json; charset=utf-8") 49 | .UsingPost() 50 | ) 51 | .RespondWith( 52 | Response 53 | .Create() 54 | .WithStatusCode(200) 55 | ); 56 | } 57 | } -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0065 // Die using-Anweisung wurde falsch platziert. 2 | global using System.Diagnostics; 3 | 4 | global using Microsoft.Extensions.Configuration; 5 | global using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | global using Serilog.Sinks.MicrosoftTeams.Alternative.Tests.Extensions; 8 | 9 | global using Serilog.Debugging; 10 | global using Serilog.Events; 11 | 12 | global using WireMock.RequestBuilders; 13 | global using WireMock.ResponseBuilders; 14 | global using WireMock.Server; 15 | #pragma warning restore IDE0065 // Die using-Anweisung wurde falsch platziert. -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests/MicrosoftTeamsSinkTest.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // A test class to test the Microsoft Teams sink for basic functionality. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Tests; 11 | 12 | /// 13 | /// A test class to test the Microsoft Teams sink for basic functionality. 14 | /// 15 | [TestClass] 16 | public class MicrosoftTeamsSinkTest 17 | { 18 | /// 19 | /// The buttons. 20 | /// 21 | private readonly List buttons = 22 | [ 23 | new MicrosoftTeamsSinkOptionsButton { Name = "Google", Uri = "https://google.com" }, 24 | new MicrosoftTeamsSinkOptionsButton { Name = "DuckDuckGo", Uri = "https://duckduckgo.com" } 25 | ]; 26 | 27 | /// 28 | /// The logger. 29 | /// 30 | private ILogger? logger; 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | public MicrosoftTeamsSinkTest() 36 | { 37 | SelfLog.Enable(s => Debug.WriteLine(s)); 38 | } 39 | 40 | /// 41 | /// Tests the emitting of messages with all log event levels. 42 | /// 43 | [TestMethod] 44 | public void EmitMessagesWithAllLogEventLevels() 45 | { 46 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 47 | this.logger = TestHelper.CreateLogger(); 48 | 49 | var counter = 0; 50 | 51 | for (var i = 0; i < 6; i++) 52 | { 53 | var template = $"{Guid.NewGuid()} {{counter}}"; 54 | #pragma warning disable Serilog004 // Constant MessageTemplate verifier 55 | this.logger.Write((LogEventLevel)counter, template, i); 56 | #pragma warning restore Serilog004 // Constant MessageTemplate verifier 57 | counter++; 58 | Thread.Sleep(500); 59 | } 60 | 61 | Thread.Sleep(1000); 62 | Log.CloseAndFlush(); 63 | 64 | Assert.IsTrue( 65 | mockServer 66 | .LogEntries 67 | .All(t => t.PartialMatchResult.IsPerfectMatch), 68 | "Invalid requests made to the mock server" 69 | ); 70 | 71 | Assert.AreEqual( 72 | 6, 73 | mockServer.LogEntries.Count(), 74 | "Wrong number of events sent to teams"); 75 | } 76 | 77 | /// 78 | /// Tests the emitting of messages with the omit properties feature enabled. 79 | /// 80 | [TestMethod] 81 | public void EmitMessagesWithOmittedProperties() 82 | { 83 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 84 | this.logger = TestHelper.CreateLogger(true); 85 | this.logger.Debug("Message text {Property}", 4); 86 | Thread.Sleep(1000); 87 | Log.CloseAndFlush(); 88 | 89 | Assert.IsTrue( 90 | mockServer 91 | .LogEntries 92 | .All(t => t.PartialMatchResult.IsPerfectMatch), 93 | "Invalid requests made to the mock server" 94 | ); 95 | 96 | Assert.AreEqual( 97 | 1, 98 | mockServer.LogEntries.Count(), 99 | "Wrong number of events sent to teams"); 100 | } 101 | 102 | /// 103 | /// Tests the emitting of messages with zero buttons. 104 | /// 105 | [TestMethod] 106 | public void EmitMessagesWithZeroButtons() 107 | { 108 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 109 | this.logger = TestHelper.CreateLoggerWithButtons(this.buttons.Take(0)); 110 | this.logger.Debug("Message text {Property}", 1); 111 | Thread.Sleep(1000); 112 | Log.CloseAndFlush(); 113 | 114 | Assert.IsTrue( 115 | mockServer 116 | .LogEntries 117 | .All(t => t.PartialMatchResult.IsPerfectMatch), 118 | "Invalid requests made to the mock server" 119 | ); 120 | 121 | Assert.AreEqual( 122 | 1, 123 | mockServer.LogEntries.Count(), 124 | "Wrong number of events sent to teams"); 125 | } 126 | 127 | /// 128 | /// Tests the emitting of messages with one button. 129 | /// 130 | [TestMethod] 131 | public void EmitMessagesWithOneButton() 132 | { 133 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 134 | this.logger = TestHelper.CreateLoggerWithButtons(this.buttons.Take(1)); 135 | this.logger.Debug("Message text {Property}", 2); 136 | Thread.Sleep(1000); 137 | Log.CloseAndFlush(); 138 | 139 | Assert.IsTrue( 140 | mockServer 141 | .LogEntries 142 | .All(t => t.PartialMatchResult.IsPerfectMatch), 143 | "Invalid requests made to the mock server" 144 | ); 145 | 146 | Assert.AreEqual( 147 | 1, 148 | mockServer.LogEntries.Count(), 149 | "Wrong number of events sent to teams"); 150 | } 151 | 152 | /// 153 | /// Tests the emitting of messages with two buttons. 154 | /// 155 | [TestMethod] 156 | public void EmitMessagesWithTwoButtons() 157 | { 158 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 159 | this.logger = TestHelper.CreateLoggerWithButtons(this.buttons.Take(2)); 160 | this.logger.Debug("Message text {Property}", 3); 161 | Thread.Sleep(1000); 162 | Log.CloseAndFlush(); 163 | 164 | Assert.IsTrue( 165 | mockServer 166 | .LogEntries 167 | .All(t => t.PartialMatchResult.IsPerfectMatch), 168 | "Invalid requests made to the mock server" 169 | ); 170 | 171 | Assert.AreEqual( 172 | 1, 173 | mockServer.LogEntries.Count(), 174 | "Wrong number of events sent to teams"); 175 | } 176 | 177 | /// 178 | /// Tests the emitting of messages with complex data. 179 | /// 180 | [DeploymentItem("TestException.txt")] 181 | [TestMethod] 182 | public void EmitMessagesWithComplexData() 183 | { 184 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 185 | this.logger = TestHelper.CreateLoggerWithCodeTags(); 186 | var data = File.ReadAllText("TestException.txt"); 187 | #pragma warning disable Serilog004 // Constant MessageTemplate verifier 188 | this.logger.Debug(data); 189 | #pragma warning restore Serilog004 // Constant MessageTemplate verifier 190 | Thread.Sleep(1000); 191 | Log.CloseAndFlush(); 192 | } 193 | 194 | /// 195 | /// Tests the emitting of messages with a title template (As requested in https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative/issues/12). 196 | /// 197 | [TestMethod] 198 | public void EmitMessagesWithTitleTemplate() 199 | { 200 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 201 | this.logger = TestHelper.CreateLogger("My title: {Tenant}"); 202 | this.logger.Debug("Message text {Property} for tenant {Tenant}", 1, "Tenant1"); 203 | Thread.Sleep(1000); 204 | Log.CloseAndFlush(); 205 | 206 | Assert.IsTrue( 207 | mockServer 208 | .LogEntries 209 | .All(t => t.PartialMatchResult.IsPerfectMatch), 210 | "Invalid requests made to the mock server" 211 | ); 212 | 213 | Assert.AreEqual( 214 | 1, 215 | mockServer.LogEntries.Count(), 216 | "Wrong number of events sent to teams"); 217 | } 218 | 219 | /// 220 | /// Tests the emitting of messages only with the required filter property. 221 | /// 222 | [TestMethod] 223 | public void EmitMessagesFilteredByProperty() 224 | { 225 | // Arrange. 226 | const string filterOnProperty = "MsTeams"; 227 | 228 | var logLevels = Enum.GetValues(); 229 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 230 | int counter = 0; 231 | 232 | this.logger = TestHelper.CreateLoggerWithChannels(new MicrosoftTeamsSinkChannelHandlerOptions(filterOnProperty)); 233 | 234 | // Act. 235 | // A message for every loglevel with the required property. 236 | foreach (var logEventLevel in logLevels) 237 | { 238 | var loggerWithContext = this.logger.ForContext(filterOnProperty, $"Channel{logEventLevel}"); 239 | 240 | var template = $"{Guid.NewGuid()} {{counter}}"; 241 | #pragma warning disable Serilog004 // Constant MessageTemplate verifier 242 | loggerWithContext.Write(logEventLevel, template, counter); 243 | #pragma warning restore Serilog004 // Constant MessageTemplate verifier 244 | counter++; 245 | } 246 | 247 | // A message for every loglevel without the required property. 248 | foreach (var logEventLevel in logLevels) 249 | { 250 | var template = $"{Guid.NewGuid()} {{counter}}"; 251 | #pragma warning disable Serilog004 // Constant MessageTemplate verifier 252 | this.logger.Write(logEventLevel, template, counter); 253 | #pragma warning restore Serilog004 // Constant MessageTemplate verifier 254 | counter++; 255 | } 256 | 257 | Thread.Sleep(1000); 258 | Log.CloseAndFlush(); 259 | 260 | // Assert. 261 | Assert.IsTrue( 262 | mockServer 263 | .LogEntries 264 | .All(t => t.PartialMatchResult.IsPerfectMatch), 265 | "Invalid requests made to the mock server" 266 | ); 267 | 268 | Assert.AreEqual( 269 | logLevels.Length, 270 | mockServer.LogEntries.Count(), 271 | "Wrong number of events sent to teams"); 272 | } 273 | 274 | /// 275 | /// Tests the emitting of messages for a specific channel 276 | /// 277 | [TestMethod] 278 | public void EmitMessagesForSpecificChannel() 279 | { 280 | // Arrange. 281 | const string filterOnProperty = "MsTeams"; 282 | const string channelName = "ITTeam"; 283 | 284 | using var mockServer = TestHelper.CreateMockServer(); 285 | mockServer.AddChannel(channelName); 286 | 287 | this.logger = TestHelper.CreateLoggerWithChannels( 288 | new MicrosoftTeamsSinkChannelHandlerOptions( 289 | filterOnProperty, 290 | new Dictionary 291 | { 292 | [channelName] = $"{mockServer.Url}/{channelName}/" 293 | } 294 | ) 295 | ); 296 | 297 | // Act. 298 | var loggerWithContext = this.logger.ForContext(filterOnProperty, channelName); 299 | loggerWithContext.Information("Demo for a specific channel"); 300 | 301 | Thread.Sleep(1000); 302 | Log.CloseAndFlush(); 303 | 304 | // Assert. 305 | Assert.IsTrue( 306 | mockServer 307 | .LogEntries 308 | .All(t => t.PartialMatchResult.IsPerfectMatch), 309 | "Invalid requests made to the mock server" 310 | ); 311 | 312 | Assert.AreEqual( 313 | 1, 314 | mockServer.LogEntries.Count(), 315 | "Wrong number of events sent to teams"); 316 | } 317 | 318 | /// 319 | /// Tests the emitting of messages for multiple channels 320 | /// 321 | [TestMethod] 322 | public void EmitMessagesForMultipleChannels() 323 | { 324 | // Arrange. 325 | const string filterOnProperty = "MsTeams"; 326 | const string channelName = "ITTeam"; 327 | const string alternativeChannelName = "SupportTeam"; 328 | const string missingChannelName = "HelpdeskTeam"; 329 | 330 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 331 | mockServer.AddChannel(channelName); 332 | mockServer.AddChannel(alternativeChannelName); 333 | 334 | var channelDictionary = new Dictionary 335 | { 336 | [channelName] = $"{mockServer.Url}/{channelName}/", 337 | [alternativeChannelName] = $"{mockServer.Url}/{alternativeChannelName}/" 338 | }; 339 | 340 | this.logger = TestHelper.CreateLoggerWithChannels( 341 | new MicrosoftTeamsSinkChannelHandlerOptions( 342 | filterOnProperty, 343 | channelDictionary 344 | ) 345 | ); 346 | 347 | // Act. 348 | var loggerForDefaultChannel = this.logger.ForContext(filterOnProperty, missingChannelName); 349 | loggerForDefaultChannel.Information("Demo for the default channel"); 350 | 351 | foreach (var channelPair in channelDictionary) 352 | { 353 | var loggerForChannel = this.logger.ForContext(filterOnProperty, channelPair.Key); 354 | loggerForChannel.Information("Demo for the channel {Channel}", channelPair.Key); 355 | } 356 | 357 | Thread.Sleep(1000); 358 | Log.CloseAndFlush(); 359 | 360 | // Assert. 361 | Assert.IsTrue( 362 | mockServer 363 | .LogEntries 364 | .All(t => t.PartialMatchResult.IsPerfectMatch), 365 | "Invalid requests made to the mock server" 366 | ); 367 | 368 | foreach (var channelPair in channelDictionary) 369 | { 370 | Assert.AreEqual( 371 | 1, 372 | mockServer.LogEntries.Count(t => t.RequestMessage.Url == channelPair.Value), 373 | $"Wrong event count for the channel {channelPair.Key}" 374 | ); 375 | } 376 | 377 | Assert.AreEqual( 378 | channelDictionary.Count + 1, 379 | mockServer.LogEntries.Count(), 380 | "Wrong number of events sent to teams"); 381 | } 382 | 383 | /// 384 | /// Tests the emitting of messages for multiple channels using the appsettings.TwoChannelsExample.json configuration 385 | /// 386 | [TestMethod] 387 | public void EmitMessagesForMultipleChannelsUsingAppSettingsTwoChannelsExample() 388 | { 389 | // Arrange. 390 | const string filterOnProperty = "MsTeams"; 391 | const string channelName = "ITTeam"; 392 | const string alternativeChannelName = "SupportTeam"; 393 | const string missingChannelName = "HelpdeskTeam"; 394 | 395 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 396 | mockServer.AddChannel(channelName); 397 | mockServer.AddChannel(alternativeChannelName); 398 | 399 | var channelDictionary = new Dictionary 400 | { 401 | [channelName] = $"{mockServer.Url}/{channelName}/", 402 | [alternativeChannelName] = $"{mockServer.Url}/{alternativeChannelName}/" 403 | }; 404 | 405 | this.logger = TestHelper.CreateLoggerFromConfiguration("appsettings.TwoChannelsExample.json"); 406 | 407 | // Act. 408 | var loggerForDefaultChannel = this.logger.ForContext(filterOnProperty, missingChannelName); 409 | loggerForDefaultChannel.Information("Demo for the default channel"); 410 | 411 | foreach (var channelPair in channelDictionary) 412 | { 413 | var loggerForChannel = this.logger.ForContext(filterOnProperty, channelPair.Key); 414 | loggerForChannel.Information("Demo for the channel {Channel}", channelPair.Key); 415 | } 416 | 417 | Thread.Sleep(1000); 418 | Log.CloseAndFlush(); 419 | 420 | // Assert. 421 | Assert.IsTrue( 422 | mockServer 423 | .LogEntries 424 | .All(t => t.PartialMatchResult.IsPerfectMatch), 425 | "Invalid requests made to the mock server" 426 | ); 427 | 428 | foreach (var channelPair in channelDictionary) 429 | { 430 | Assert.AreEqual( 431 | 1, 432 | mockServer.LogEntries.Count(t => t.RequestMessage.Url == channelPair.Value), 433 | $"Wrong event count for the channel {channelPair.Key}" 434 | ); 435 | } 436 | 437 | Assert.AreEqual( 438 | channelDictionary.Count + 1, 439 | mockServer.LogEntries.Count(), 440 | "Wrong number of events sent to teams"); 441 | } 442 | 443 | /// 444 | /// Tests the emitting of messages to the default channel when the specific channel is not found 445 | /// 446 | [TestMethod] 447 | public void EmitMessagesForDefaultChannel() 448 | { 449 | // Arrange. 450 | const string filterOnProperty = "MsTeams"; 451 | const string channelName = "ITTeam"; 452 | const string missingChannelName = "SupportTeam"; 453 | 454 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 455 | 456 | this.logger = TestHelper.CreateLoggerWithChannels( 457 | new MicrosoftTeamsSinkChannelHandlerOptions( 458 | filterOnProperty, 459 | new Dictionary 460 | { 461 | [channelName] = $"{mockServer.Url}/{channelName}/" 462 | } 463 | ) 464 | ); 465 | 466 | // Act. 467 | var loggerWithContext = this.logger.ForContext(filterOnProperty, missingChannelName); 468 | loggerWithContext.Information("Demo for a missing channel emiting to default"); 469 | 470 | Thread.Sleep(1000); 471 | Log.CloseAndFlush(); 472 | 473 | // Assert. 474 | Assert.IsTrue( 475 | mockServer 476 | .LogEntries 477 | .All(t => t.PartialMatchResult.IsPerfectMatch), 478 | "Invalid requests made to the mock server" 479 | ); 480 | 481 | Assert.AreEqual( 482 | 1, 483 | mockServer.LogEntries.Count(), 484 | "Wrong number of events sent to teams"); 485 | } 486 | 487 | /// 488 | /// Tests the emitting of messages to the default channel when using Power Automate workflows. 489 | /// 490 | [TestMethod] 491 | public void EmitMessagesWithPowerAutomateWorkflow() 492 | { 493 | using var mockServer = TestHelper.CreateMockServerWithDefaultChannel(); 494 | this.logger = TestHelper.CreateLogger(false, true); 495 | this.logger.Error("Message text {Property}", 4); 496 | Thread.Sleep(2000); 497 | Log.CloseAndFlush(); 498 | 499 | Assert.IsTrue( 500 | mockServer 501 | .LogEntries 502 | .All(t => t.PartialMatchResult.IsPerfectMatch), 503 | "Invalid requests made to the mock server" 504 | ); 505 | 506 | Assert.AreEqual( 507 | 1, 508 | mockServer.LogEntries.Count(), 509 | "Wrong number of events sent to teams"); 510 | } 511 | 512 | ///// 513 | ///// Tests the emitting of messages with the possibility to debug. 514 | ///// 515 | //[TestMethod] 516 | //public async Task TestWithDebugging() 517 | //{ 518 | //var webHook = Environment.GetEnvironmentVariable("MicrosoftTeamsWebhookUrlPowerAutomate") ?? string.Empty; 519 | //var sink = new MicrosoftTeamsSink(new MicrosoftTeamsSinkOptions(webHook, usePowerAutomateWorkflows: true)); 520 | //var data = new List { new(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate("Test", []), []) }; 521 | //await sink.EmitBatchAsync(data); 522 | //} 523 | } 524 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests/Serilog - Backup.Sinks.MicrosoftTeams.Alternative.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | net8.0 6 | latest 7 | enable 8 | enable 9 | NU1803 10 | true 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Always 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests/Serilog.Sinks.MicrosoftTeams.Alternative.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | net8.0 6 | latest 7 | enable 8 | enable 9 | NU1803 10 | true 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Always 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests/TestException.txt: -------------------------------------------------------------------------------- 1 | Microsoft.Data.SqlClient.SqlException (0x80131904): A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: SSL Provider, error: 31 - Encryption(ssl/tls) handshake failed) 2 | ---> System.IO.EndOfStreamException: End of stream reached 3 | at Microsoft.Data.SqlClient.SNI.SslOverTdsStream.ReadInternal(Byte[] buffer, Int32 offset, Int32 count, CancellationToken token, Boolean async) 4 | at Microsoft.Data.SqlClient.SNI.SslOverTdsStream.Read(Byte[] buffer, Int32 offset, Int32 count) 5 | at System.Net.FixedSizeReader.ReadPacket(Stream transport, Byte[] buffer, Int32 offset, Int32 count) 6 | at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest) 7 | at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest) 8 | at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest) 9 | at System.Net.Security.SslStream.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest) 10 | at System.Net.Security.SslStream.ProcessAuthentication(LazyAsyncResult lazyResult, CancellationToken cancellationToken) 11 | at System.Net.Security.SslStream.AuthenticateAsClient(SslClientAuthenticationOptions sslClientAuthenticationOptions) 12 | at System.Net.Security.SslStream.AuthenticateAsClient(String targetHost, X509CertificateCollection clientCertificates, SslProtocols enabledSslProtocols, Boolean checkCertificateRevocation) 13 | at System.Net.Security.SslStream.AuthenticateAsClient(String targetHost) 14 | `at Microsoft.Data.SqlClient.SNI.SNITCPHandle.EnableSsl(UInt32 options)` 15 | at Microsoft.Data.SqlClient.SNI.SNIProxy.EnableSsl(SNIHandle handle, UInt32 options) 16 | at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action1 wrapCloseInAction) at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose) at Microsoft.Data.SqlClient.TdsParser.ConsumePreLoginHandshake(Boolean encrypt, Boolean trustServerCert, Boolean integratedSecurity, Boolean& marsCapable, Boolean& fedAuthRequired) at Microsoft.Data.SqlClient.TdsParser.Connect(ServerInfo serverInfo, SqlInternalConnectionTds connHandler, Boolean ignoreSniOpenTimeout, Int64 timerExpire, Boolean encrypt, Boolean trustServerCert, Boolean integratedSecurity, Boolean withFailover, SqlAuthenticationMethod authType) at Microsoft.Data.SqlClient.SqlInternalConnectionTds.AttemptOneLogin(ServerInfo serverInfo, String newPassword, SecureString newSecurePassword, Boolean ignoreSniOpenTimeout, TimeoutTimer timeout, Boolean withFailover) at Microsoft.Data.SqlClient.SqlInternalConnectionTds.LoginNoFailover(ServerInfo serverInfo, String newPassword, SecureString newSecurePassword, Boolean redirectedUserInstance, SqlConnectionString connectionOptions, SqlCredential credential, TimeoutTimer timeout) at Microsoft.Data.SqlClient.SqlInternalConnectionTds.OpenLoginEnlist(TimeoutTimer timeout, SqlConnectionString connectionOptions, SqlCredential credential, String newPassword, SecureString newSecurePassword, Boolean redirectedUserInstance) at Microsoft.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionString connectionOptions, SqlCredential credential, Object providerInfo, String newPassword, SecureString newSecurePassword, Boolean redirectedUserInstance, SqlConnectionString userConnectionOptions, SessionData reconnectSessionData, Boolean applyTransientFaultHandling, String accessToken, DbConnectionPool pool) at Microsoft.Data.SqlClient.SqlConnectionFactory.CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, Object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection, DbConnectionOptions userOptions) at Microsoft.Data.ProviderBase.DbConnectionFactory.CreatePooledConnection(DbConnectionPool pool, DbConnection owningObject, DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionOptions userOptions) at Microsoft.Data.ProviderBase.DbConnectionPool.CreateObject(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection) at Microsoft.Data.ProviderBase.DbConnectionPool.UserCreateRequest(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection) at Microsoft.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection) at Microsoft.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource1 retry, DbConnectionOptions userOptions, DbConnectionInternal& connection) 17 | at Microsoft.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection) at Microsoft.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource1 retry, DbConnectionOptions userOptions) 18 | at Microsoft.Data.ProviderBase.DbConnectionClosed.TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource1 retry, DbConnectionOptions userOptions) at Microsoft.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource1 retry, SqlConnectionOverrides overrides) 19 | at Microsoft.Data.SqlClient.SqlConnection.Open(SqlConnectionOverrides overrides) 20 | at Microsoft.Data.SqlClient.SqlConnection.Open() 21 | at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenDbConnection(Boolean errorsExpected) 22 | at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.Open(Boolean errorsExpected) 23 | at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject) 24 | at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable1.Enumerator.InitializeReader(DbContext _, Boolean result) at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func3 operation, Func3 verifySucceeded) at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable1.Enumerator.MoveNext() 25 | at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source) 26 | at TEST.Api.DAL.EnqueuingToDb.DbMilor2Context.EnqueueData(ApiCallIdentification metadatas, String json) in /src/TEST/DAL/EnqueuingToDb/DbMilor2Context.cs:line 57 27 | at TEST.Api.Business.EnqueuingToDb.Enqueuing.Create() in /src/TEST/Business/EnqueuingToDb/Enqueuing.cs:line 51 28 | at TEST.Api.Models.ActionFilters.DataEnqueuingFilterAttribute.OnActionExecuting(ActionExecutingContext context) in /src/TEST/Models/ActionFilters/DataEnqueuingFilter.cs:line 109 29 | at Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) 30 | at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) 31 | at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) 32 | at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) 33 | at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() 34 | --- End of stack trace from previous location where exception was thrown --- 35 | at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) 36 | ClientConnectionId:490aab56-2f44-4824-b5a1-907042f6e801 Microsoft.Data.SqlClient.SqlException (0x80131904): A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: SSL Provider, error: 31 - Encryption(ssl/tls) handshake failed) -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // A helper class for the tests. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Tests; 11 | 12 | /// 13 | /// A helper class for the tests. 14 | /// 15 | public static class TestHelper 16 | { 17 | /// 18 | /// The default port for the mock http server. 19 | /// 20 | private static readonly int MockServerPort = 63210; 21 | 22 | /// 23 | /// The test web hook URL. 24 | /// 25 | private static readonly string TestWebHook = Environment.GetEnvironmentVariable("MicrosoftTeamsWebhookUrl") ?? $"http://localhost:{MockServerPort}"; 26 | 27 | /// 28 | /// The test web hook URL for Power Automate. 29 | /// 30 | private static readonly string TestWebHookPowerAutomate = Environment.GetEnvironmentVariable("MicrosoftTeamsWebhookUrlPowerAutomate") ?? $"http://localhost:{MockServerPort}"; 31 | 32 | /// 33 | /// Creates the logger. 34 | /// 35 | /// The title template. 36 | /// An . 37 | public static ILogger CreateLogger(string titleTemplate) 38 | { 39 | var logger = new LoggerConfiguration() 40 | .MinimumLevel.Verbose() 41 | .WriteTo.MicrosoftTeams(new MicrosoftTeamsSinkOptions(TestWebHook, titleTemplate)) 42 | .CreateLogger(); 43 | 44 | return logger; 45 | } 46 | 47 | /// 48 | /// Creates the logger. 49 | /// 50 | /// A value indicating whether the properties should be omitted or not. 51 | /// A value indicating whether Power Automate workflows are used or not. 52 | /// An . 53 | public static ILogger CreateLogger(bool omitPropertiesSection = false, bool usePowerAutomateWorkflows = false) 54 | { 55 | if (usePowerAutomateWorkflows) 56 | { 57 | var logger = new LoggerConfiguration() 58 | .MinimumLevel.Verbose() 59 | .WriteTo.MicrosoftTeams(new MicrosoftTeamsSinkOptions(TestWebHookPowerAutomate, "Integration Tests", omitPropertiesSection: omitPropertiesSection, usePowerAutomateWorkflows: usePowerAutomateWorkflows)) 60 | .CreateLogger(); 61 | 62 | return logger; 63 | } 64 | 65 | var logger2 = new LoggerConfiguration() 66 | .MinimumLevel.Verbose() 67 | .WriteTo.MicrosoftTeams(new MicrosoftTeamsSinkOptions(TestWebHook, "Integration Tests", omitPropertiesSection: omitPropertiesSection, usePowerAutomateWorkflows: usePowerAutomateWorkflows)) 68 | .CreateLogger(); 69 | 70 | return logger2; 71 | } 72 | 73 | /// 74 | /// Creates the logger with an URL. 75 | /// 76 | /// The URL. 77 | /// A value indicating whether the properties should be omitted or not. 78 | /// A value indicating whether Power Automate workflows are used or not. 79 | /// An . 80 | public static ILogger CreateLoggerWithUrl(string url, bool omitPropertiesSection = false, bool usePowerAutomateWorkflows = false) 81 | { 82 | var logger = new LoggerConfiguration() 83 | .MinimumLevel.Verbose() 84 | .WriteTo.MicrosoftTeams(new MicrosoftTeamsSinkOptions(url, "Integration Tests", omitPropertiesSection: omitPropertiesSection, usePowerAutomateWorkflows: usePowerAutomateWorkflows)) 85 | .CreateLogger(); 86 | 87 | return logger; 88 | } 89 | 90 | /// 91 | /// Creates the logger with buttons. 92 | /// 93 | /// ´The buttons to output 94 | /// An . 95 | public static ILogger CreateLoggerWithButtons(IEnumerable buttons) 96 | { 97 | var logger = new LoggerConfiguration() 98 | .MinimumLevel.Verbose() 99 | .WriteTo.MicrosoftTeams(new MicrosoftTeamsSinkOptions(TestWebHook, "Integration Tests", buttons: buttons)) 100 | .CreateLogger(); 101 | 102 | return logger; 103 | } 104 | 105 | /// 106 | /// Creates the logger. 107 | /// 108 | /// An . 109 | public static ILogger CreateLoggerWithCodeTags() 110 | { 111 | var logger = new LoggerConfiguration() 112 | .MinimumLevel.Verbose() 113 | .WriteTo.MicrosoftTeams(new MicrosoftTeamsSinkOptions(TestWebHook, "Integration Tests", useCodeTagsForMessage: true)) 114 | .CreateLogger(); 115 | 116 | return logger; 117 | } 118 | 119 | /// 120 | /// Creates the logger with a channel handler. 121 | /// 122 | /// The channel handler. 123 | /// An . 124 | public static ILogger CreateLoggerWithChannels(MicrosoftTeamsSinkChannelHandlerOptions channelHandler) 125 | { 126 | var logger = new LoggerConfiguration() 127 | .MinimumLevel.Verbose() 128 | .WriteTo.MicrosoftTeams(new MicrosoftTeamsSinkOptions(TestWebHook, channelHandler: channelHandler)) 129 | .CreateLogger(); 130 | 131 | return logger; 132 | } 133 | 134 | /// 135 | /// Creates the logger from a appsettings file. 136 | /// 137 | /// The path for the appsettings file to use. 138 | /// An . 139 | public static ILogger CreateLoggerFromConfiguration(string appsettingsPath) 140 | { 141 | var configuration = new ConfigurationBuilder() 142 | .SetBasePath(Directory.GetCurrentDirectory()) 143 | .AddJsonFile(appsettingsPath) 144 | .Build(); 145 | 146 | var logger = new LoggerConfiguration() 147 | .ReadFrom.Configuration(configuration) 148 | .CreateLogger(); 149 | 150 | return logger; 151 | } 152 | 153 | /// 154 | /// Creates a mock HTTP server with the default channel. 155 | /// 156 | /// A mocked HTTP server. 157 | public static WireMockServer CreateMockServerWithDefaultChannel() 158 | { 159 | var server = WireMockServer.Start(MockServerPort); 160 | server.AddDefaultChannel(); 161 | return server; 162 | } 163 | 164 | /// 165 | /// Creates a clean mock HTTP server. 166 | /// 167 | /// A mocked HTTP server. 168 | public static WireMockServer CreateMockServer() 169 | { 170 | var server = WireMockServer.Start(MockServerPort); 171 | return server; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests/appsettings.TwoChannelsExample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.MicrosoftTeams.Alternative" ], 4 | "MinimumLevel": "Debug", 5 | "WriteTo": [ 6 | { 7 | "Name": "MicrosoftTeams", 8 | "Args": { 9 | "webHookUri": "http://localhost:63210/", 10 | "channelHandler": 11 | { 12 | "filterOnProperty": "MsTeams", 13 | "channelList": { 14 | "ITTeam": "http://localhost:63210/ITTeam/", 15 | "SupportTeam": "http://localhost:63210/SupportTeam/" 16 | } 17 | } 18 | } 19 | } 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34024.191 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.MicrosoftTeams.Alternative", "Serilog.Sinks.MicrosoftTeams.Alternative\Serilog.Sinks.MicrosoftTeams.Alternative.csproj", "{8CD41BF4-028E-41D4-8C19-F07F9D37080B}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.MicrosoftTeams.Alternative.Tests", "Serilog.Sinks.MicrosoftTeams.Alternative.Tests\Serilog.Sinks.MicrosoftTeams.Alternative.Tests.csproj", "{E3210545-0C3B-4AAA-99FD-7DA26C072C29}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {8CD41BF4-028E-41D4-8C19-F07F9D37080B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {8CD41BF4-028E-41D4-8C19-F07F9D37080B}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {8CD41BF4-028E-41D4-8C19-F07F9D37080B}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {8CD41BF4-028E-41D4-8C19-F07F9D37080B}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {E3210545-0C3B-4AAA-99FD-7DA26C072C29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {E3210545-0C3B-4AAA-99FD-7DA26C072C29}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {E3210545-0C3B-4AAA-99FD-7DA26C072C29}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {E3210545-0C3B-4AAA-99FD-7DA26C072C29}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {8E3D65A1-F7A2-42C6-9FCF-742F5AE18E50} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0065 // Die using-Anweisung wurde falsch platziert. 2 | global using System.Net; 3 | global using System.Net.Mime; 4 | global using System.Text; 5 | 6 | global using Newtonsoft.Json; 7 | 8 | global using Serilog.Configuration; 9 | global using Serilog.Debugging; 10 | global using Serilog.Events; 11 | global using Serilog.Formatting.Display; 12 | global using Serilog.Sinks.MicrosoftTeams.Alternative; 13 | global using Serilog.Sinks.MicrosoftTeams.Alternative.Constants; 14 | global using Serilog.Sinks.MicrosoftTeams.Alternative.Core; 15 | global using Serilog.Sinks.MicrosoftTeams.Alternative.Enumerations; 16 | global using Serilog.Sinks.MicrosoftTeams.Alternative.Exceptions; 17 | global using Serilog.Sinks.MicrosoftTeams.Alternative.Extensions; 18 | global using Serilog.Sinks.PeriodicBatching; 19 | #pragma warning restore IDE0065 // Die using-Anweisung wurde falsch platziert. -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/LoggerConfigurationMicrosoftTeamsExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // Provides extension methods on . 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog; 11 | 12 | /// 13 | /// Provides extension methods on . 14 | /// 15 | public static class LoggerConfigurationMicrosoftTeamsExtensions 16 | { 17 | /// 18 | /// extension that provides configuration chaining. 19 | /// 20 | /// new LoggerConfiguration() 21 | /// .MinimumLevel.Verbose() 22 | /// .WriteTo.MicrosoftTeams("webHookUri") 23 | /// .CreateLogger(); 24 | /// 25 | /// 26 | /// Instance of object. 27 | /// The incoming web hook URI to the Microsoft Teams channel. 28 | /// The title template of the messages. 29 | /// The maximum number of events to post in a single batch; defaults to 1 if 30 | /// not provided i.e. no batching by default. 31 | /// The time to wait between checking for event batches; defaults to 1 sec if not 32 | /// provided. 33 | /// The output template. 34 | /// The format provider used for formatting the message. 35 | /// value that specifies minimum logging 36 | /// level that will be allowed to be logged. 37 | /// The proxy address to use. 38 | /// Indicates whether the properties section should be omitted or not. 39 | /// A value indicating whether code tags are used for the message template or not. 40 | /// A value indicating whether Power Automate workflows are used or not. 41 | /// The buttons to add to a message. 42 | /// The maximum number of events that should be stored in the batching queue. 43 | /// The configuration for sending events to multiple channels. 44 | /// Instance of object. 45 | public static LoggerConfiguration MicrosoftTeams( 46 | this LoggerSinkConfiguration loggerSinkConfiguration, 47 | string webHookUri, 48 | string? titleTemplate = null, 49 | int? batchSizeLimit = null, 50 | TimeSpan? period = null, 51 | string? outputTemplate = null, 52 | IFormatProvider? formatProvider = null, 53 | LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, 54 | string? proxy = null, 55 | bool omitPropertiesSection = false, 56 | bool useCodeTagsForMessage = false, 57 | bool usePowerAutomateWorkflows = false, 58 | IEnumerable? buttons = null, 59 | int? queueLimit = null, 60 | MicrosoftTeamsSinkChannelHandlerOptions? channelHandler = null) 61 | { 62 | var microsoftTeamsSinkOptions = new MicrosoftTeamsSinkOptions( 63 | webHookUri, 64 | titleTemplate, 65 | batchSizeLimit, 66 | period, 67 | outputTemplate, 68 | formatProvider, 69 | restrictedToMinimumLevel, 70 | omitPropertiesSection, 71 | useCodeTagsForMessage, 72 | usePowerAutomateWorkflows, 73 | proxy, 74 | buttons, 75 | queueLimit, 76 | channelHandler); 77 | return loggerSinkConfiguration.MicrosoftTeams(microsoftTeamsSinkOptions, restrictedToMinimumLevel); 78 | } 79 | 80 | /// 81 | /// extension that provides configuration chaining. 82 | /// 83 | /// Instance of object. 84 | /// The microsoft teams sink options object. 85 | /// value that specifies minimum logging 86 | /// level that will be allowed to be logged. 87 | /// Instance of object. 88 | public static LoggerConfiguration MicrosoftTeams( 89 | this LoggerSinkConfiguration loggerSinkConfiguration, 90 | MicrosoftTeamsSinkOptions microsoftTeamsSinkOptions, 91 | LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) 92 | { 93 | if (loggerSinkConfiguration is null) 94 | { 95 | throw new ArgumentNullException(nameof(loggerSinkConfiguration)); 96 | } 97 | 98 | if (microsoftTeamsSinkOptions is null) 99 | { 100 | throw new ArgumentNullException(nameof(microsoftTeamsSinkOptions)); 101 | } 102 | 103 | if (string.IsNullOrWhiteSpace(microsoftTeamsSinkOptions.WebHookUri)) 104 | { 105 | throw new ArgumentNullException("The webhook URI is empty.", nameof(microsoftTeamsSinkOptions.WebHookUri)); 106 | } 107 | 108 | var batchingOptions = new PeriodicBatchingSinkOptions() 109 | { 110 | BatchSizeLimit = microsoftTeamsSinkOptions.BatchSizeLimit, 111 | Period = microsoftTeamsSinkOptions.Period, 112 | QueueLimit = microsoftTeamsSinkOptions.QueueLimit 113 | }; 114 | 115 | var batchingSink = new PeriodicBatchingSink(new MicrosoftTeamsSink(microsoftTeamsSinkOptions), batchingOptions); 116 | return loggerSinkConfiguration.Sink(batchingSink, restrictedToMinimumLevel); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Serilog.Sinks.MicrosoftTeams.Alternative.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | Serilog.Sinks.MicrosoftTeams.Alternative 6 | Serilog 7 | true 8 | true 9 | Serilog.Sinks.MicrosoftTeams.Alternative 10 | SeppPenner and the Serilog contributors 11 | SeppPenner and the Serilog contributors 12 | Copyright © SeppPenner and the Serilog contributors 13 | Serilog.Sinks.MicrosoftTeams.Alternative is a library to save logging information from https://github.com/serilog/serilog to https://products.office.com/en-us/microsoft-teams/group-chat-software. 14 | c# csharp serilog teams microsoft-teams microsoft sink logging log 15 | https://www.nuget.org/packages/Serilog.Sinks.MicrosoftTeams.Alternative/ 16 | https://github.com/serilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternative 17 | Icon.png 18 | Github 19 | Version 1.5.0.0 (2025-03-24): Fixes https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative/issues/83, updates NuGet packages, deprecates `failureCallback`. 20 | MIT 21 | latest 22 | enable 23 | true 24 | true 25 | true 26 | snupkg 27 | enable 28 | NU1803 29 | true 30 | README.md 31 | all 32 | true 33 | 34 | 35 | 36 | 37 | 38 | all 39 | runtime; build; native; contentfiles; analyzers; buildtransitive 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | True 51 | 52 | 53 | 54 | True 55 | 56 | 57 | 58 | True 59 | 60 | 61 | 62 | True 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Constants/SinkConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Constants; 2 | 3 | /// 4 | /// The sink constants. 5 | /// 6 | internal static class SinkConstants 7 | { 8 | /// 9 | /// The too many requests message constant. 10 | /// 11 | public const string TooManyRequestsMessage = "Too many requests"; 12 | 13 | /// 14 | /// The properties constant. 15 | /// 16 | public const string Properties = "Properties"; 17 | 18 | /// 19 | /// The open URI constant. 20 | /// 21 | public const string OpenUri = "OpenUri"; 22 | 23 | /// 24 | /// The level constant. 25 | /// 26 | public const string Level = "Level"; 27 | 28 | /// 29 | /// The message template constant. 30 | /// 31 | public const string MessageTemplate = "MessageTemplate"; 32 | 33 | /// 34 | /// The exception constant. 35 | /// 36 | public const string Exception = "Exception"; 37 | 38 | /// 39 | /// The first occurence constant. 40 | /// 41 | public const string FirstOccurence = "First occurrence"; 42 | 43 | /// 44 | /// The last occurence constant. 45 | /// 46 | public const string LastOccurence = "Last occurrence"; 47 | 48 | /// 49 | /// The occurred on constant. 50 | /// 51 | public const string OccuredOn = "Occurred on"; 52 | } 53 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Core/MicrosoftTeamsAttachment.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // The attachment. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Core; 11 | 12 | /// 13 | /// The attachment. 14 | /// 15 | internal class MicrosoftTeamsAttachment 16 | { 17 | /// 18 | /// Gets the content type of the card. 19 | /// 20 | [JsonProperty("contentType")] 21 | public string ContentType { get; } = "application/vnd.microsoft.card.adaptive"; 22 | 23 | /// 24 | /// Gets the content URL. 25 | /// 26 | [JsonProperty("contentUrl")] 27 | public string ContentUrl { get; } = string.Empty; 28 | 29 | /// 30 | /// Gets or sets the content. 31 | /// 32 | [JsonProperty("content")] 33 | [JsonConverter(typeof(AdaptiveCards.AdaptiveCardConverter))] 34 | public AdaptiveCards.AdaptiveCard? Content { get; set; } 35 | } 36 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Core/MicrosoftTeamsMessage.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // The teams message. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Core; 11 | 12 | /// 13 | /// The teams message. 14 | /// 15 | internal class MicrosoftTeamsMessage 16 | { 17 | /// 18 | /// Gets the type of the card. 19 | /// 20 | [JsonProperty("type")] 21 | public string Type { get; } = "message"; 22 | 23 | /// 24 | /// Gets or sets the attachments. 25 | /// 26 | [JsonProperty("attachments")] 27 | public List Attachments { get; set; } = []; 28 | } 29 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Core/MicrosoftTeamsMessageAction.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // The Microsoft Teams message action class. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Core; 11 | 12 | /// 13 | /// The Microsoft Teams message action class. 14 | /// 15 | public class MicrosoftTeamsMessageAction 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The type. 21 | /// The name. 22 | /// The target. 23 | public MicrosoftTeamsMessageAction(string type, string name, MicrosoftTeamsMessageActionTarget target) 24 | { 25 | this.Type = type; 26 | this.Name = name; 27 | this.Targets = [target]; 28 | } 29 | 30 | /// 31 | /// Gets or sets the type. 32 | /// 33 | [JsonProperty("@type")] 34 | public string Type { get; set; } 35 | 36 | /// 37 | /// Gets or sets the name. 38 | /// 39 | [JsonProperty("name")] 40 | public string Name { get; set; } 41 | 42 | /// 43 | /// Gets or sets the targets. 44 | /// 45 | [JsonProperty("targets")] 46 | public IList Targets { get; set; } 47 | } 48 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Core/MicrosoftTeamsMessageActionTarget.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // The Microsoft Teams message action target class. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Core; 11 | 12 | /// 13 | /// The Microsoft Teams message action target class. 14 | /// 15 | public class MicrosoftTeamsMessageActionTarget 16 | { 17 | /// 18 | /// Gets or sets the operating system. 19 | /// 20 | [JsonProperty("os")] 21 | public string OperatingSystem { get; set; } = string.Empty; 22 | } 23 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Core/MicrosoftTeamsMessageActionTargetUri.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // The Microsoft Teams message target URI class. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Core; 11 | 12 | /// 13 | /// The Microsoft Teams message target URI class. 14 | /// 15 | public class MicrosoftTeamsMessageActionTargetUri : MicrosoftTeamsMessageActionTarget 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The URI. 21 | /// The operating system. 22 | public MicrosoftTeamsMessageActionTargetUri(string uri, string operatingSystem = "default") 23 | { 24 | this.OperatingSystem = operatingSystem; 25 | this.Uri = uri; 26 | } 27 | 28 | /// 29 | /// Gets or sets the URI. 30 | /// 31 | [JsonProperty("uri")] 32 | public string Uri { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Core/MicrosoftTeamsMessageCard.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // The teams message card. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Core; 11 | 12 | /// 13 | /// The teams message card. 14 | /// 15 | internal class MicrosoftTeamsMessageCard 16 | { 17 | /// 18 | /// Gets the type of the card. 19 | /// 20 | [JsonProperty("@type")] 21 | public string Type { get; } = "MessageCard"; 22 | 23 | /// 24 | /// Gets the context of the card. 25 | /// 26 | [JsonProperty("@context")] 27 | public string Context { get; } = "http://schema.org/extensions"; 28 | 29 | /// 30 | /// Gets or sets the title of the card. 31 | /// 32 | [JsonProperty("title")] 33 | public string Title { get; set; } = string.Empty; 34 | 35 | /// 36 | /// Gets or sets the text of the card. 37 | /// 38 | [JsonProperty("text")] 39 | public string Text { get; set; } = string.Empty; 40 | 41 | /// 42 | /// Gets or sets the theme color of the card. 43 | /// 44 | [JsonProperty("themeColor")] 45 | public string Color { get; set; } = string.Empty; 46 | 47 | /// 48 | /// Gets or sets the sections of the card. 49 | /// 50 | [JsonProperty("sections")] 51 | public IList Sections { get; set; } = []; 52 | 53 | /// 54 | /// Gets or sets the potential action buttons. 55 | /// 56 | [JsonProperty("potentialAction")] 57 | public IList PotentialActions { get; set; } = []; 58 | } 59 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Core/MicrosoftTeamsMessageFact.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // The message card fact. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Core; 11 | 12 | /// 13 | /// The message card fact. 14 | /// 15 | internal class MicrosoftTeamsMessageFact 16 | { 17 | /// 18 | /// Gets or sets the name of the card fact. 19 | /// 20 | [JsonProperty("name")] 21 | public string Name { get; set; } = string.Empty; 22 | 23 | /// 24 | /// Gets or sets the value of the card fact. 25 | /// 26 | [JsonProperty("value")] 27 | public string Value { get; set; } = string.Empty; 28 | } 29 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Core/MicrosoftTeamsMessageSection.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // The message section. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Core; 11 | 12 | /// 13 | /// The message section. 14 | /// 15 | internal class MicrosoftTeamsMessageSection 16 | { 17 | /// 18 | /// Gets or sets message section title. 19 | /// 20 | [JsonProperty("title")] 21 | public string Title { get; set; } = string.Empty; 22 | 23 | /// 24 | /// Gets or sets message section facts. 25 | /// 26 | [JsonProperty("facts")] 27 | public IList Facts { get; set; } = []; 28 | } 29 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Enumerations/MicrosoftTeamsColors.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // A class to store all the Microsoft Teams colors. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Enumerations; 11 | 12 | /// 13 | /// A class to store all the Microsoft Teams colors. 14 | /// 15 | public static class MicrosoftTeamsColors 16 | { 17 | /// 18 | /// The default Microsoft Teams color. 19 | /// 20 | public static string Default => "777777"; 21 | 22 | /// 23 | /// The information Microsoft Teams color. 24 | /// 25 | public static string Information => "5bc0de"; 26 | 27 | /// 28 | /// The warning Microsoft Teams color. 29 | /// 30 | public static string Warning => "f0ad4e"; 31 | 32 | /// 33 | /// The error Microsoft Teams color. 34 | /// 35 | public static string Error => "d9534f"; 36 | } 37 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Exceptions/LoggingException.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // The logging exception. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Exceptions; 11 | 12 | /// 13 | /// 14 | /// The logging exception. 15 | /// 16 | public sealed class LoggingException : LoggingFailedException 17 | { 18 | /// 19 | /// Gets or sets the status code. 20 | /// 21 | public HttpStatusCode? StatusCode { get; set; } 22 | 23 | /// 24 | /// Construct a to communicate a logging failure. 25 | /// 26 | /// A message describing the logging failure. 27 | public LoggingException(string message) : base(message) 28 | { 29 | } 30 | 31 | /// 32 | /// Construct a to communicate a logging failure. 33 | /// 34 | /// A message describing the logging failure. 35 | /// The status code. 36 | public LoggingException(string message, HttpStatusCode statusCode) : base(message) 37 | { 38 | this.StatusCode = statusCode; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // This class contains extension methods. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative.Extensions; 11 | 12 | /// 13 | /// This class contains extension methods. 14 | /// 15 | public static class EnumerableExtensions 16 | { 17 | /// 18 | /// Checks whether the enumerable is null or empty. 19 | /// 20 | /// The type of object to use. 21 | /// The . 22 | /// A value indicating whether the enumerable is null or empty. 23 | public static bool IsNullOrEmpty(this IEnumerable? enumerable) 24 | { 25 | if (enumerable is null) 26 | { 27 | return true; 28 | } 29 | 30 | return !enumerable.Any(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/MicrosoftExtendedLogEvent.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // Added a new class to store the first and last occurrence timestamps. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative; 11 | 12 | /// 13 | /// Added a new class to store the first and last occurrence timestamps. 14 | /// 15 | public class MicrosoftExtendedLogEvent 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The first occurrence. 21 | /// The last occurrence. 22 | /// The log event. 23 | public MicrosoftExtendedLogEvent(DateTime firstOccurrence, DateTime lastOccurrence, LogEvent logEvent) 24 | { 25 | this.FirstOccurrence = firstOccurrence; 26 | this.LastOccurrence = lastOccurrence; 27 | this.LogEvent = logEvent; 28 | } 29 | 30 | /// 31 | /// Gets or sets the log event. 32 | /// 33 | public LogEvent LogEvent { get; set; } 34 | 35 | /// 36 | /// Gets or sets the first occurrence. 37 | /// 38 | public DateTimeOffset FirstOccurrence { get; set; } 39 | 40 | /// 41 | /// Gets or sets the last occurrence. 42 | /// 43 | public DateTimeOffset LastOccurrence { get; set; } 44 | } 45 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/MicrosoftTeamsSink.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // Implements and provides means needed for sending Serilog log events to Microsoft Teams. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative; 11 | 12 | /// 13 | /// Implements and provides means needed for sending Serilog log events to Microsoft Teams. 14 | /// 15 | public class MicrosoftTeamsSink : IBatchedLogEventSink 16 | { 17 | /// 18 | /// The json serializer settings. 19 | /// 20 | private static readonly JsonSerializerSettings JsonSerializerSettings = new() 21 | { 22 | NullValueHandling = NullValueHandling.Ignore 23 | }; 24 | 25 | /// 26 | /// The client. 27 | /// 28 | private readonly HttpClient client; 29 | 30 | /// 31 | /// The options. 32 | /// 33 | private readonly MicrosoftTeamsSinkOptions options; 34 | 35 | /// 36 | /// Initializes a new instance of the class. 37 | /// 38 | /// Microsoft teams sink options object. 39 | public MicrosoftTeamsSink(MicrosoftTeamsSinkOptions options) 40 | { 41 | this.options = options; 42 | 43 | if (string.IsNullOrWhiteSpace(options.Proxy) == false) 44 | { 45 | var httpClientHandler = new HttpClientHandler 46 | { 47 | Proxy = new WebProxy(options.Proxy, true), 48 | UseProxy = true 49 | }; 50 | this.client = new HttpClient(httpClientHandler); 51 | } 52 | else 53 | { 54 | this.client = new HttpClient(); 55 | } 56 | } 57 | 58 | /// 59 | /// 60 | /// Emit a batch of log events, running asynchronously. 61 | /// 62 | /// The events to emit. 63 | /// 64 | /// Received failed result {result.StatusCode} when posting events to Microsoft Teams 65 | /// 66 | /// Override either or , 67 | /// not both. Overriding EmitBatch() is preferred. 68 | /// 69 | public async Task EmitBatchAsync(IEnumerable events) 70 | { 71 | var messages = this.GetMessagesToSend(events); 72 | await this.PostMessages(messages); 73 | } 74 | 75 | /// 76 | /// 77 | /// Allows sinks to perform periodic work without requiring additional threads or 78 | /// timers (thus avoiding additional flush/shut-down complexity). 79 | /// 80 | public Task OnEmptyBatchAsync() 81 | { 82 | return Task.CompletedTask; 83 | } 84 | 85 | /// 86 | /// Gets the color of the attachment. 87 | /// 88 | /// The level. 89 | /// The attachment color as . 90 | private static string GetAttachmentColor(LogEventLevel level) 91 | { 92 | return level switch 93 | { 94 | LogEventLevel.Information => MicrosoftTeamsColors.Information, 95 | LogEventLevel.Warning => MicrosoftTeamsColors.Warning, 96 | LogEventLevel.Error or LogEventLevel.Fatal => MicrosoftTeamsColors.Error, 97 | _ => MicrosoftTeamsColors.Default, 98 | }; 99 | } 100 | 101 | /// 102 | /// Gets the adaptive text color of the attachment. 103 | /// 104 | /// The level. 105 | /// The attachment color as . 106 | private static AdaptiveCards.AdaptiveTextColor GetCardColor(LogEventLevel level) 107 | { 108 | return level switch 109 | { 110 | LogEventLevel.Information => AdaptiveCards.AdaptiveTextColor.Default, 111 | LogEventLevel.Warning => AdaptiveCards.AdaptiveTextColor.Warning, 112 | LogEventLevel.Error or LogEventLevel.Fatal => AdaptiveCards.AdaptiveTextColor.Attention, 113 | _ => AdaptiveCards.AdaptiveTextColor.Default, 114 | }; 115 | } 116 | 117 | /// 118 | /// Gets the messages to send and groups them. 119 | /// 120 | /// The log events. 121 | /// A of . 122 | private List GetMessagesToSend(IEnumerable events) 123 | { 124 | var isFilterEnabled = this.options.ChannelHandler.IsFilterOnPropertyEnabled; 125 | var messagesToSend = new List(); 126 | 127 | foreach (var logEvent in events) 128 | { 129 | if (logEvent.Level < this.options.MinimumLogEventLevel) 130 | { 131 | continue; 132 | } 133 | 134 | // Ignore messages that do not comply with the filter. 135 | if (isFilterEnabled && !logEvent.Properties.ContainsKey(this.options.ChannelHandler.FilterOnProperty)) 136 | { 137 | continue; 138 | } 139 | 140 | var foundSameLogEvent = messagesToSend.Where(m => m.LogEvent?.Exception?.Message is not null) 141 | .FirstOrDefault(m => m.LogEvent!.Exception!.Message == logEvent.Exception?.Message); 142 | 143 | if (foundSameLogEvent is null) 144 | { 145 | messagesToSend.Add(new MicrosoftExtendedLogEvent(logEvent.Timestamp.DateTime, logEvent.Timestamp.DateTime, logEvent)); 146 | } 147 | else 148 | { 149 | if (foundSameLogEvent.FirstOccurrence > logEvent.Timestamp) 150 | { 151 | foundSameLogEvent.FirstOccurrence = logEvent.Timestamp; 152 | } 153 | else if (foundSameLogEvent.LastOccurrence < logEvent.Timestamp) 154 | { 155 | foundSameLogEvent.LastOccurrence = logEvent.Timestamp; 156 | } 157 | } 158 | } 159 | 160 | return messagesToSend; 161 | } 162 | 163 | /// 164 | /// Posts the messages. 165 | /// 166 | /// The messages. 167 | /// A representing any asynchronous operation. 168 | private async Task PostMessages(IEnumerable messages) 169 | { 170 | var isFilterEnabled = this.options.ChannelHandler.IsFilterOnPropertyEnabled; 171 | 172 | foreach (var logEvent in messages) 173 | { 174 | var webHookUri = isFilterEnabled 175 | ? this.GetChannelUri(logEvent) 176 | : this.options.WebHookUri; 177 | 178 | var json = string.Empty; 179 | 180 | if (this.options.UsePowerAutomateWorkflows) 181 | { 182 | var message = this.CreateMessage(logEvent); 183 | json = JsonConvert.SerializeObject(message, JsonSerializerSettings); 184 | } 185 | else 186 | { 187 | var message = this.CreateMessageCard(logEvent); 188 | json = JsonConvert.SerializeObject(message, JsonSerializerSettings); 189 | } 190 | 191 | var result = await this.client 192 | .PostAsync(webHookUri, new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json)) 193 | .ConfigureAwait(false); 194 | 195 | if (result.StatusCode == HttpStatusCode.TooManyRequests) 196 | { 197 | SelfLog.WriteLine(SinkConstants.TooManyRequestsMessage); 198 | } 199 | 200 | if (!result.IsSuccessStatusCode) 201 | { 202 | throw new LoggingException($"Received failed result {result.StatusCode} when posting events to Microsoft Teams.", 203 | result.StatusCode); 204 | } 205 | 206 | SelfLog.WriteLine($"Status code: {result.StatusCode}"); 207 | } 208 | } 209 | 210 | /// 211 | /// If support for multiple channels is enabled this will get 212 | /// the corresponding Uri for the channel or the default Uri if 213 | /// there is no specific configuration for the event. 214 | /// 215 | /// Event to check for the target channel. 216 | /// Uri for the target channel. 217 | private string GetChannelUri(MicrosoftExtendedLogEvent logEvent) 218 | { 219 | if (!logEvent.LogEvent.Properties.ContainsKey(this.options.ChannelHandler.FilterOnProperty)) 220 | { 221 | return this.options.WebHookUri; 222 | } 223 | 224 | var expectedKey = logEvent.LogEvent 225 | .Properties[this.options.ChannelHandler.FilterOnProperty] 226 | .ToString() 227 | .Trim('"'); 228 | 229 | // Return the specific channel uri if available. 230 | if (this.options.ChannelHandler.ChannelList.TryGetValue(expectedKey, out string? value)) 231 | { 232 | return value; 233 | } 234 | 235 | return this.options.WebHookUri; 236 | } 237 | 238 | /// 239 | /// Creates the message. 240 | /// 241 | /// The log event. 242 | /// A message card. 243 | private MicrosoftTeamsMessageCard CreateMessageCard(MicrosoftExtendedLogEvent logEvent) 244 | { 245 | var renderedMessage = this.GetRenderedMessage(logEvent); 246 | 247 | var request = new MicrosoftTeamsMessageCard 248 | { 249 | Title = this.GetRenderedTitle(logEvent), 250 | Text = this.options.UseCodeTagsForMessage 251 | ? $"```{Environment.NewLine}{renderedMessage}{Environment.NewLine}```" 252 | : renderedMessage, 253 | Color = GetAttachmentColor(logEvent.LogEvent.Level), 254 | Sections = this.options.OmitPropertiesSection 255 | ? new List() 256 | : new[] 257 | { 258 | new MicrosoftTeamsMessageSection 259 | { 260 | Title = SinkConstants.Properties, 261 | Facts = this.GetFacts(logEvent).ToArray() 262 | } 263 | }, 264 | PotentialActions = [] 265 | }; 266 | 267 | // Add static URL buttons from the options. 268 | if (this.options.Buttons.IsNullOrEmpty()) 269 | { 270 | return request; 271 | } 272 | 273 | request.PotentialActions = []; 274 | 275 | foreach (var button in this.options.Buttons!) 276 | { 277 | var action = new MicrosoftTeamsMessageAction( 278 | SinkConstants.OpenUri, 279 | button.Name, 280 | new MicrosoftTeamsMessageActionTargetUri(button.Uri) 281 | ); 282 | request.PotentialActions.Add(action); 283 | } 284 | 285 | return request; 286 | } 287 | 288 | /// 289 | /// Creates the message. 290 | /// 291 | /// The log event. 292 | /// A message. 293 | private MicrosoftTeamsMessage CreateMessage(MicrosoftExtendedLogEvent logEvent) 294 | { 295 | var renderedMessage = this.GetRenderedMessage(logEvent); 296 | var message = new MicrosoftTeamsMessage(); 297 | 298 | var card = new AdaptiveCards.AdaptiveCard(new AdaptiveCards.AdaptiveSchemaVersion(1, 0)) 299 | { 300 | Body = 301 | [ 302 | new AdaptiveCards.AdaptiveTextBlock 303 | { 304 | Text = this.GetRenderedTitle(logEvent), 305 | Size = AdaptiveCards.AdaptiveTextSize.Medium, 306 | Weight = AdaptiveCards.AdaptiveTextWeight.Bolder, 307 | Style = AdaptiveCards.AdaptiveTextBlockStyle.Heading 308 | }, 309 | new AdaptiveCards.AdaptiveTextBlock 310 | { 311 | Text = this.options.UseCodeTagsForMessage 312 | ? $"```{Environment.NewLine}{renderedMessage}{Environment.NewLine}```" 313 | : renderedMessage, 314 | Wrap = true, 315 | Color = GetCardColor(logEvent.LogEvent.Level) 316 | } 317 | ], 318 | Actions = [] 319 | }; 320 | 321 | message.Attachments.Add(new MicrosoftTeamsAttachment() 322 | { 323 | Content = card 324 | }); 325 | 326 | if (!this.options.OmitPropertiesSection) 327 | { 328 | card.Body.Add(new AdaptiveCards.AdaptiveFactSet 329 | { 330 | Facts = this.GetFacts(logEvent).Select(f => new AdaptiveCards.AdaptiveFact(f.Name, f.Value)).ToList() 331 | }); 332 | } 333 | 334 | // Add static URL buttons from the options. 335 | if (this.options.Buttons.IsNullOrEmpty()) 336 | { 337 | return message; 338 | } 339 | 340 | card.Actions = []; 341 | 342 | foreach (var button in this.options.Buttons!) 343 | { 344 | var action = new AdaptiveCards.AdaptiveOpenUrlAction 345 | { 346 | Title = button.Name, 347 | Url = new Uri(button.Uri) 348 | }; 349 | card.Actions.Add(action); 350 | } 351 | 352 | return message; 353 | } 354 | 355 | /// 356 | /// Gets the rendered message from the . 357 | /// 358 | /// The log event. 359 | /// The rendered messages as . 360 | private string GetRenderedMessage(MicrosoftExtendedLogEvent logEvent) 361 | { 362 | if (string.IsNullOrWhiteSpace(this.options.OutputTemplate)) 363 | { 364 | return logEvent.LogEvent.RenderMessage(this.options.FormatProvider); 365 | } 366 | 367 | var formatter = new MessageTemplateTextFormatter(this.options.OutputTemplate!, this.options.FormatProvider); 368 | var stringWriter = new StringWriter(); 369 | formatter.Format(logEvent.LogEvent, stringWriter); 370 | return stringWriter.ToString(); 371 | } 372 | 373 | /// 374 | /// Gets the rendered title from the . 375 | /// 376 | /// The log event. 377 | /// The rendered messages as . 378 | private string GetRenderedTitle(MicrosoftExtendedLogEvent logEvent) 379 | { 380 | if (string.IsNullOrWhiteSpace(this.options.TitleTemplate)) 381 | { 382 | return logEvent.LogEvent.RenderMessage(this.options.FormatProvider); 383 | } 384 | 385 | var formatter = new MessageTemplateTextFormatter(this.options.TitleTemplate!, this.options.FormatProvider); 386 | var stringWriter = new StringWriter(); 387 | formatter.Format(logEvent.LogEvent, stringWriter); 388 | return stringWriter.ToString(); 389 | } 390 | 391 | /// 392 | /// Gets the facts. 393 | /// 394 | /// The log event. 395 | /// A list of facts. 396 | private IEnumerable GetFacts(MicrosoftExtendedLogEvent logEvent) 397 | { 398 | yield return new MicrosoftTeamsMessageFact 399 | { 400 | Name = SinkConstants.Level, 401 | Value = logEvent.LogEvent.Level.ToString() 402 | }; 403 | 404 | yield return new MicrosoftTeamsMessageFact 405 | { 406 | Name = SinkConstants.MessageTemplate, 407 | Value = this.options.UseCodeTagsForMessage ? $"```{Environment.NewLine}{logEvent.LogEvent.MessageTemplate.Text}{Environment.NewLine}```" : logEvent.LogEvent.MessageTemplate.Text 408 | }; 409 | 410 | if (logEvent.LogEvent.Exception is not null) 411 | { 412 | yield return new MicrosoftTeamsMessageFact 413 | { 414 | Name = SinkConstants.Exception, 415 | Value = $"```{Environment.NewLine}{logEvent.LogEvent.Exception}{Environment.NewLine}```" 416 | }; 417 | } 418 | 419 | foreach (var property in logEvent.LogEvent.Properties) 420 | { 421 | yield return new MicrosoftTeamsMessageFact 422 | { 423 | Name = property.Key, 424 | Value = property.Value.ToString(null, this.options.FormatProvider) 425 | }; 426 | } 427 | 428 | if (logEvent.FirstOccurrence != logEvent.LastOccurrence) 429 | { 430 | yield return new MicrosoftTeamsMessageFact 431 | { 432 | Name = SinkConstants.FirstOccurence, 433 | Value = logEvent.FirstOccurrence.ToString("dd.MM.yyyy HH:mm:sszzz", this.options.FormatProvider) 434 | }; 435 | 436 | yield return new MicrosoftTeamsMessageFact 437 | { 438 | Name = SinkConstants.LastOccurence, 439 | Value = logEvent.LastOccurrence.ToString("dd.MM.yyyy HH:mm:sszzz", this.options.FormatProvider) 440 | }; 441 | } 442 | else 443 | { 444 | yield return new MicrosoftTeamsMessageFact 445 | { 446 | Name = SinkConstants.OccuredOn, 447 | Value = logEvent.FirstOccurrence.ToString("dd.MM.yyyy HH:mm:sszzz", this.options.FormatProvider) 448 | }; 449 | } 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/MicrosoftTeamsSinkChannelHandlerOptions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // A container for the multiple channel configuration. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative; 11 | 12 | /// 13 | /// A container for the multiple channel configuration. 14 | /// 15 | public class MicrosoftTeamsSinkChannelHandlerOptions 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | public MicrosoftTeamsSinkChannelHandlerOptions() 21 | { 22 | } 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// Only emit events that have this property (Ignoring it's value). 28 | /// Mapping between a requested channel (set in ) and it's API endpoint. 29 | public MicrosoftTeamsSinkChannelHandlerOptions(string? filterOnProperty = null, Dictionary? channelList = null) 30 | { 31 | this.FilterOnProperty = filterOnProperty ?? string.Empty; 32 | this.ChannelList = channelList ?? []; 33 | } 34 | 35 | /// 36 | /// Gets or sets the filter to filter for, only emit events that have this property (Ignoring its value). 37 | /// 38 | public string FilterOnProperty { get; set; } = string.Empty; 39 | 40 | /// 41 | /// Gets or sets the mapping between a requested channel (set in ) and it's API endpoint. 42 | /// 43 | /// 44 | /// If is set then the value of that property is 45 | /// searched on this dictionary to find the matching channel endpoint, if no matching key is 46 | /// found here, it will send the event to the default endpoint for this sink. 47 | /// 48 | public Dictionary ChannelList { get; set; } = []; 49 | 50 | /// 51 | /// Gets a value indicating whether the sink is configured to only emit events that have the property set in or not. 52 | /// 53 | public bool IsFilterOnPropertyEnabled => !string.IsNullOrWhiteSpace(this.FilterOnProperty); 54 | } -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/MicrosoftTeamsSinkOptions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // Container for all Microsoft Teams sink configurations. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative; 11 | 12 | /// 13 | /// Container for all Microsoft Teams sink configurations. 14 | /// 15 | public class MicrosoftTeamsSinkOptions 16 | { 17 | /// 18 | /// The default batch size limit. 19 | /// 20 | private const int DefaultBatchSizeLimit = 1; 21 | 22 | /// 23 | /// The default queue limit. 24 | /// 25 | private const int DefaultQueueLimit = int.MaxValue; 26 | 27 | /// 28 | /// The default period. 29 | /// 30 | private static readonly TimeSpan DefaultPeriod = TimeSpan.FromSeconds(1); 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | /// The incoming web hook URI to the Microsoft Teams channel. 36 | /// The title template of the messages. 37 | /// The maximum number of events to post in a single batch; defaults to 1 if 38 | /// not provided i.e. no batching by default. 39 | /// The time to wait between checking for event batches; defaults to 1 sec if not 40 | /// provided. 41 | /// The output template. 42 | /// The format provider used for formatting the message. 43 | /// The minimum log event level to use. 44 | /// A value indicating whether the properties section should be omitted or not. 45 | /// A value indicating whether code tags are used for the message template or not. 46 | /// A value indicating whether Power Automate workflows are used or not. 47 | /// The proxy address to use. 48 | /// The buttons to add to a message. 49 | /// The maximum number of events that should be stored in the batching queue. 50 | /// The configuration for sending events to multiple channels. 51 | public MicrosoftTeamsSinkOptions( 52 | string webHookUri, 53 | string? titleTemplate = null, 54 | int? batchSizeLimit = null, 55 | TimeSpan? period = null, 56 | string? outputTemplate = null, 57 | IFormatProvider? formatProvider = null, 58 | LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose, 59 | bool omitPropertiesSection = false, 60 | bool useCodeTagsForMessage = false, 61 | bool usePowerAutomateWorkflows = false, 62 | string? proxy = null, 63 | IEnumerable? buttons = null, 64 | int? queueLimit = null, 65 | MicrosoftTeamsSinkChannelHandlerOptions? channelHandler = null) 66 | { 67 | if (string.IsNullOrWhiteSpace(webHookUri)) 68 | { 69 | throw new ArgumentException("The webhook URI is empty.", nameof(webHookUri)); 70 | } 71 | 72 | this.WebHookUri = webHookUri; 73 | this.TitleTemplate = titleTemplate; 74 | this.BatchSizeLimit = batchSizeLimit ?? DefaultBatchSizeLimit; 75 | this.Period = period ?? DefaultPeriod; 76 | this.OutputTemplate = outputTemplate; 77 | this.FormatProvider = formatProvider; 78 | this.MinimumLogEventLevel = minimumLogEventLevel; 79 | this.OmitPropertiesSection = omitPropertiesSection; 80 | this.UseCodeTagsForMessage = useCodeTagsForMessage; 81 | this.UsePowerAutomateWorkflows = usePowerAutomateWorkflows; 82 | this.Proxy = proxy; 83 | this.Buttons = buttons ?? []; 84 | this.QueueLimit = queueLimit ?? DefaultQueueLimit; 85 | this.ChannelHandler = channelHandler ?? new MicrosoftTeamsSinkChannelHandlerOptions(); 86 | } 87 | 88 | /// 89 | /// Gets the incoming web hook URI to the Microsoft Teams channel. 90 | /// 91 | public string WebHookUri { get; } = string.Empty; 92 | 93 | /// 94 | /// Gets the title template of messages. 95 | /// 96 | public string? TitleTemplate { get; } 97 | 98 | /// 99 | /// Gets the maximum number of events to post in a single batch. 100 | /// 101 | public int BatchSizeLimit { get; } 102 | 103 | /// 104 | /// Gets the time to wait between checking for event batches. 105 | /// 106 | public TimeSpan Period { get; } 107 | 108 | /// 109 | /// Gets the output template. 110 | /// 111 | public string? OutputTemplate { get; } 112 | 113 | /// 114 | /// Gets the format provider used for formatting the message. 115 | /// 116 | public IFormatProvider? FormatProvider { get; } 117 | 118 | /// 119 | /// Gets the minimum log event level. 120 | /// 121 | public LogEventLevel MinimumLogEventLevel { get; } 122 | 123 | /// 124 | /// Gets a value indicating whether the properties section should be omitted or not. 125 | /// 126 | public bool OmitPropertiesSection { get; } 127 | 128 | /// 129 | /// Gets a value indicating whether code tags are used for the message template or not. 130 | /// 131 | public bool UseCodeTagsForMessage { get; } 132 | 133 | /// 134 | /// A value indicating whether Power Automate workflows are used or not (since O365 Incoming Webhooks are being shutdown). 135 | /// 136 | public bool UsePowerAutomateWorkflows { get; } 137 | 138 | /// 139 | /// Gets the proxy URL. 140 | /// 141 | public string? Proxy { get; } 142 | 143 | /// 144 | /// Gets the buttons to add to a message. 145 | /// 146 | public IEnumerable? Buttons { get; } 147 | 148 | /// 149 | /// Gets the maximum number of events that should be stored in the batching queue. 150 | /// 151 | public int QueueLimit { get; } 152 | 153 | /// 154 | /// Gets the configuration for sending events to multiple channels. 155 | /// 156 | public MicrosoftTeamsSinkChannelHandlerOptions ChannelHandler { get; } 157 | } 158 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.MicrosoftTeams.Alternative/Sinks/MicrosoftTeams/Alternative/MicrosoftTeamsSinkOptionsButton.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // The project is licensed under the MIT license. 4 | // 5 | // 6 | // A class to handle the Microsoft Teams options buttons. 7 | // 8 | // -------------------------------------------------------------------------------------------------------------------- 9 | 10 | namespace Serilog.Sinks.MicrosoftTeams.Alternative; 11 | 12 | /// 13 | /// A class to handle the Microsoft Teams options buttons. 14 | /// 15 | public class MicrosoftTeamsSinkOptionsButton 16 | { 17 | /// 18 | /// Gets or sets the link display name. 19 | /// 20 | public string Name { get; set; } = string.Empty; 21 | 22 | /// 23 | /// Gets or sets the link URI. 24 | /// 25 | public string Uri { get; set; } = string.Empty; 26 | } 27 | --------------------------------------------------------------------------------