179 lines
6.0 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|