From e35f744e4c6b1db4c2a8e3401514f948bd5d07fc Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 2 Aug 2026 14:56:16 +0900 Subject: [PATCH] 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 --- .../Infrastructure/TelegramSink.cs | 106 ++++++++++++++++++ src/KArtSell.Host/Program.cs | 25 ++++- 2 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 src/KArtSell.Host/Infrastructure/TelegramSink.cs diff --git a/src/KArtSell.Host/Infrastructure/TelegramSink.cs b/src/KArtSell.Host/Infrastructure/TelegramSink.cs new file mode 100644 index 00000000..d6476e62 --- /dev/null +++ b/src/KArtSell.Host/Infrastructure/TelegramSink.cs @@ -0,0 +1,106 @@ +using System.Net.Http; +using Serilog; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; + +namespace KArtSell.Host.Infrastructure; + +/// +/// Serilog sink for sending critical logs to Telegram +/// Triggers on ERROR and FATAL events +/// +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 + { + { "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 + } + } +} + +/// +/// Serilog extension for adding Telegram sink +/// +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)); + } +} diff --git a/src/KArtSell.Host/Program.cs b/src/KArtSell.Host/Program.cs index 991559fb..07419836 100644 --- a/src/KArtSell.Host/Program.cs +++ b/src/KArtSell.Host/Program.cs @@ -5,6 +5,7 @@ using KArtSell.BuildingBlocks.Capabilities; using Microsoft.Extensions.Caching.Memory; using KArtSell.Host.Jobs; using KArtSell.Host.Configuration; +using KArtSell.Host.Infrastructure; using KArtSell.BuildingBlocks.Data; using KArtSell.BuildingBlocks.Reliability; using KArtSell.BuildingBlocks.Time; @@ -18,14 +19,28 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; +using Serilog.Events; var builder = WebApplication.CreateBuilder(args); -builder.Host.UseSerilog((context, services, logger) => logger - .ReadFrom.Configuration(context.Configuration) - .ReadFrom.Services(services) - .Enrich.FromLogContext() - .WriteTo.Console()); +// Load Telegram secrets for Serilog notifications +var telegramBotToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT") ?? string.Empty; +var telegramChatId = Environment.GetEnvironmentVariable("CHAT_ID") ?? string.Empty; + +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) var connectionString = ResolveSecret(