Files
EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/MicrosoftTeamsFlow.cs
2026-02-28 07:19:29 +01:00

179 lines
6.0 KiB
C#

using EonaCat.Json;
using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace EonaCat.LogStack.Flows
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
/// <summary>
/// logging flow that sends messages to Microsoft Teams via an incoming webhook.
/// </summary>
public sealed class MicrosoftTeamsFlow : FlowBase, IAsyncDisposable
{
private const int ChannelCapacity = 4096;
private const int DefaultBatchSize = 5; // Keep batches small to avoid throttling
private readonly Channel<LogEvent> _channel;
private readonly Task _workerTask;
private readonly CancellationTokenSource _cts;
private readonly HttpClient _httpClient;
private readonly string _webhookUrl;
public MicrosoftTeamsFlow(
string webhookUrl,
LogLevel minimumLevel = LogLevel.Information)
: base("MicrosoftTeams", minimumLevel)
{
_webhookUrl = webhookUrl ?? throw new ArgumentNullException(nameof(webhookUrl));
_httpClient = new HttpClient();
var channelOptions = new BoundedChannelOptions(ChannelCapacity)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
};
_channel = Channel.CreateBounded<LogEvent>(channelOptions);
_cts = new CancellationTokenSource();
_workerTask = Task.Run(() => ProcessQueueAsync(_cts.Token));
}
public override Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return Task.FromResult(WriteResult.LevelFiltered);
}
if (_channel.Writer.TryWrite(logEvent))
{
Interlocked.Increment(ref BlastedCount);
return Task.FromResult(WriteResult.Success);
}
Interlocked.Increment(ref DroppedCount);
return Task.FromResult(WriteResult.Dropped);
}
private async Task ProcessQueueAsync(CancellationToken cancellationToken)
{
var batch = new List<LogEvent>(DefaultBatchSize);
try
{
while (await _channel.Reader.WaitToReadAsync(cancellationToken))
{
while (_channel.Reader.TryRead(out var logEvent))
{
batch.Add(logEvent);
if (batch.Count >= DefaultBatchSize)
{
await SendBatchAsync(batch, cancellationToken);
batch.Clear();
}
}
if (batch.Count > 0)
{
await SendBatchAsync(batch, cancellationToken);
batch.Clear();
}
}
if (batch.Count > 0)
{
await SendBatchAsync(batch, cancellationToken);
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Console.Error.WriteLine($"MicrosoftTeamsFlow error: {ex.Message}");
}
}
private async Task SendBatchAsync(List<LogEvent> batch, CancellationToken cancellationToken)
{
foreach (var logEvent in batch)
{
var payload = new
{
// Teams expects a "text" field with Markdown or simple message
text = BuildMessage(logEvent)
};
var json = JsonHelper.ToJson(payload);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
await _httpClient.PostAsync(_webhookUrl, content, cancellationToken);
}
}
private static string BuildMessage(LogEvent logEvent)
{
var sb = new StringBuilder();
sb.AppendLine($"**Level:** {logEvent.Level}");
if (!string.IsNullOrWhiteSpace(logEvent.Category))
{
sb.AppendLine($"**Category:** {logEvent.Category}");
}
sb.AppendLine($"**Timestamp:** {LogEvent.GetDateTime(logEvent.Timestamp):yyyy-MM-dd HH:mm:ss.fff}");
sb.AppendLine($"**Message:** {logEvent.Message}");
if (logEvent.Exception != null)
{
sb.AppendLine("**Exception:**");
sb.AppendLine($"```\n{logEvent.Exception.GetType().FullName}: {logEvent.Exception.Message}\n{logEvent.Exception.StackTrace}\n```");
}
if (logEvent.Properties.Count > 0)
{
sb.AppendLine("**Properties:**");
foreach (var prop in logEvent.Properties)
{
sb.AppendLine($"`{prop.Key}` = `{prop.Value?.ToString() ?? "null"}`");
}
}
return sb.ToString();
}
public override async Task FlushAsync(CancellationToken cancellationToken = default)
{
_channel.Writer.Complete();
try
{
await _workerTask.ConfigureAwait(false);
}
catch { }
}
public override async ValueTask DisposeAsync()
{
IsEnabled = false;
_channel.Writer.Complete();
_cts.Cancel();
try
{
await _workerTask.ConfigureAwait(false);
}
catch { }
_httpClient.Dispose();
_cts.Dispose();
await base.DisposeAsync();
}
}
}