feat: Serilog Telegram Integration for Alert Notifications
Add automatic Telegram notifications for ERROR and FATAL level logs. Features: - TelegramSink: Custom Serilog sink for Telegram API integration - Conditional logging: Only ERROR and FATAL levels trigger alerts - Environment variables: TELEGRAM_BOT and CHAT_ID from Gitea Secrets - Non-blocking: Telegram failures don't crash application Configuration: - Reads TELEGRAM_BOT and CHAT_ID from environment - Formatted messages with emoji, timestamp, and exception details - Markdown parsing for better Telegram presentation This enables real-time alerting for critical issues during: - Gate 3 Shadow Run execution - Production deployments - System errors and exceptions Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Configuration;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace KArtSell.Host.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serilog sink for sending critical logs to Telegram
|
||||||
|
/// Triggers on ERROR and FATAL events
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TelegramSink : ILogEventSink
|
||||||
|
{
|
||||||
|
private readonly string _telegramBotToken;
|
||||||
|
private readonly string _telegramChatId;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly object _syncRoot = new();
|
||||||
|
|
||||||
|
public TelegramSink(string telegramBotToken, string telegramChatId, HttpClient? httpClient = null)
|
||||||
|
{
|
||||||
|
_telegramBotToken = telegramBotToken;
|
||||||
|
_telegramChatId = telegramChatId;
|
||||||
|
_httpClient = httpClient ?? new HttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Emit(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
// Only send critical logs (Error and Fatal)
|
||||||
|
if (logEvent.Level < LogEventLevel.Error)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
SendTelegramMessage(logEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Silently ignore Telegram errors to prevent logging loops
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendTelegramMessage(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
var emoji = logEvent.Level == LogEventLevel.Fatal ? "🔴" : "⚠️";
|
||||||
|
var levelName = logEvent.Level.ToString().ToUpperInvariant();
|
||||||
|
|
||||||
|
var message = $@"{emoji} *{levelName}* - K-ArtSell Aegis
|
||||||
|
|
||||||
|
{logEvent.MessageTemplate.Render(logEvent.Properties)}
|
||||||
|
|
||||||
|
_Timestamp: {logEvent.Timestamp:O}_";
|
||||||
|
|
||||||
|
if (logEvent.Exception != null)
|
||||||
|
{
|
||||||
|
message += $@"
|
||||||
|
|
||||||
|
```
|
||||||
|
{logEvent.Exception.GetType().Name}: {logEvent.Exception.Message}
|
||||||
|
```";
|
||||||
|
}
|
||||||
|
|
||||||
|
SendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendMessage(string message)
|
||||||
|
{
|
||||||
|
var url = $"https://api.telegram.org/bot{_telegramBotToken}/sendMessage";
|
||||||
|
|
||||||
|
var content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "chat_id", _telegramChatId },
|
||||||
|
{ "text", message },
|
||||||
|
{ "parse_mode", "Markdown" }
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = _httpClient.PostAsync(url, content).GetAwaiter().GetResult();
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Silently fail - don't want logging to break application
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serilog extension for adding Telegram sink
|
||||||
|
/// </summary>
|
||||||
|
public static class TelegramSinkExtensions
|
||||||
|
{
|
||||||
|
public static LoggerConfiguration Telegram(
|
||||||
|
this LoggerSinkConfiguration loggerConfiguration,
|
||||||
|
string telegramBotToken,
|
||||||
|
string telegramChatId,
|
||||||
|
HttpClient? httpClient = null)
|
||||||
|
{
|
||||||
|
return loggerConfiguration.Sink(
|
||||||
|
new TelegramSink(telegramBotToken, telegramChatId, httpClient));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using KArtSell.BuildingBlocks.Capabilities;
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using KArtSell.Host.Jobs;
|
using KArtSell.Host.Jobs;
|
||||||
using KArtSell.Host.Configuration;
|
using KArtSell.Host.Configuration;
|
||||||
|
using KArtSell.Host.Infrastructure;
|
||||||
using KArtSell.BuildingBlocks.Data;
|
using KArtSell.BuildingBlocks.Data;
|
||||||
using KArtSell.BuildingBlocks.Reliability;
|
using KArtSell.BuildingBlocks.Reliability;
|
||||||
using KArtSell.BuildingBlocks.Time;
|
using KArtSell.BuildingBlocks.Time;
|
||||||
@@ -18,14 +19,28 @@ using OpenTelemetry.Metrics;
|
|||||||
using OpenTelemetry.Resources;
|
using OpenTelemetry.Resources;
|
||||||
using OpenTelemetry.Trace;
|
using OpenTelemetry.Trace;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Host.UseSerilog((context, services, logger) => logger
|
// Load Telegram secrets for Serilog notifications
|
||||||
.ReadFrom.Configuration(context.Configuration)
|
var telegramBotToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT") ?? string.Empty;
|
||||||
.ReadFrom.Services(services)
|
var telegramChatId = Environment.GetEnvironmentVariable("CHAT_ID") ?? string.Empty;
|
||||||
.Enrich.FromLogContext()
|
|
||||||
.WriteTo.Console());
|
builder.Host.UseSerilog((context, services, logger) =>
|
||||||
|
{
|
||||||
|
var config = logger
|
||||||
|
.ReadFrom.Configuration(context.Configuration)
|
||||||
|
.ReadFrom.Services(services)
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.WriteTo.Console();
|
||||||
|
|
||||||
|
// Add Telegram sink for ERROR and FATAL logs
|
||||||
|
if (!string.IsNullOrEmpty(telegramBotToken) && !string.IsNullOrEmpty(telegramChatId))
|
||||||
|
{
|
||||||
|
config = config.WriteTo.Sink(new TelegramSink(telegramBotToken, telegramChatId), LogEventLevel.Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Load secrets from environment variables (set by CI/CD or user-secrets in dev)
|
// Load secrets from environment variables (set by CI/CD or user-secrets in dev)
|
||||||
var connectionString = ResolveSecret(
|
var connectionString = ResolveSecret(
|
||||||
|
|||||||
Reference in New Issue
Block a user