C#/.NET 异常捕获与邮件通知:从基础实现到生产级全局处理

1. 项目概述:为什么我们需要带邮件通知的异常捕获?

在软件开发里,处理异常就像给程序买保险。一个简单的 try/catch 块能防止程序因为一个未预料的错误而彻底崩溃,但它通常只是默默地把错误“吞掉”,记录在某个开发者才能看到的日志文件里。想象一下,你负责维护一个线上电商系统,半夜两点,支付接口突然因为第三方服务升级而报错。如果没有一个主动的警报机制,你可能要等到第二天早上用户投诉如潮水般涌来时,才发现问题。这时,损失已经造成了。

“Adding a try/catch With Email Notification”这个项目,解决的正是这个痛点。它的核心目标不仅仅是捕获错误,更是要 主动、及时地将关键异常信息推送到负责人的收件箱 。这不再是简单的防御性编程,而是构建了一个从错误发生到人工介入的快速响应通道。对于任何涉及线上服务、定时任务、数据处理流水线的应用,这都是一项提升系统可观测性和运维效率的基础设施。

它适合所有层级的开发者:新手可以通过实现它来理解异常处理与外部服务的集成;资深工程师则可以借此设计更健壮、更易维护的全局异常处理策略。接下来,我将拆解如何从零开始,构建一个既可靠又实用的带邮件通知的异常捕获机制,并分享我在多个生产环境中趟过的坑和总结的心得。

2. 核心设计思路与方案选型

实现“捕获异常并发送邮件”听起来简单,但设计不当很容易变成“垃圾邮件制造器”或“通知风暴源”。我们需要在可靠性、及时性、可管理性之间找到平衡。

2.1 核心架构拆解

一个健壮的带邮件通知的异常处理机制,通常包含以下几个核心组件:

  1. 异常捕获层 :这是起点,即 try/catch 块本身。关键在于决定在代码的哪个粒度进行捕获。是每个方法都包一个?还是只在最外层的入口(如Controller的顶层、定时任务的Run方法)进行捕获?
  2. 异常信息封装层 :捕获到异常后,需要提取哪些信息?一个简单的 e.Message 远远不够。我们需要上下文,比如发生时间、机器名、线程ID、堆栈跟踪、引发异常的方法参数、当前用户信息等。
  3. 邮件内容构造层 :如何将封装好的异常信息,组织成人类可读、且包含必要技术细节的邮件正文和标题。
  4. 邮件发送服务层 :负责与SMTP服务器或其他邮件API交互,将构造好的邮件发送出去。这里需要考虑异步、重试、失败降级等问题。
  5. 配置与策略管理层 :并非所有异常都需要发邮件。如何根据异常类型、严重级别、发生频率进行过滤?邮件发给谁?这些都需要可配置。

2.2 方案选型:内置SMTP vs 第三方邮件服务API

这是第一个关键决策点,两种主流方案对比如下:

特性 使用内置System.Net.Mail (SMTP) 使用第三方API (如SendGrid, Mailgun)
复杂度 低,.NET框架原生支持 中,需要集成第三方SDK和API Key
可靠性 依赖自身或公司的SMTP服务器稳定性 高,服务商提供专业运维和送达率保障
可送达性 容易被收件方邮件服务器标记为垃圾邮件 通常有更好的发信信誉和收件箱抵达率
功能扩展 基础,需要自行实现统计、退订等功能 丰富,自带分析、模板、事件Webhook等
成本 通常免费(服务器成本除外) 通常有免费额度,超出后按量计费

选型建议

  • 对于内部系统、监控报警 :如果公司有稳定可靠的企业内部SMTP服务器(如Exchange),优先使用SMTP方案,简单直接。
  • 对于面向公众的互联网应用、需要高送达率的场景 :强烈建议使用SendGrid、Mailgun等专业服务。它们处理了DKIM/SPF认证、IP信誉、退信处理等繁琐问题,能极大提升邮件进入收件箱的概率,避免报警邮件被扔进垃圾箱的尴尬。

注意:无论选择哪种方案, 绝对不要将邮箱密码、API密钥等敏感信息硬编码在代码中 。必须使用如 appsettings.json 、环境变量或密钥管理服务(如Azure Key Vault, AWS Secrets Manager)来安全地存储和读取这些配置。

2.3 异步化与防风暴设计

这是设计中最容易忽略,也最容易引发生产事故的两个点。

异步化 :发送邮件是一个网络I/O操作,耗时可能从几百毫秒到几秒不等。如果在捕获异常的同步上下文中直接同步发送邮件,会阻塞当前请求或任务线程,导致性能下降,甚至在高并发下引发线程池耗尽。 必须采用异步发送 ,例如使用 SendMailAsync 方法,或者将发送任务丢入一个后台队列(如Channel、RabbitMQ)中由独立工作者处理。

防通知风暴 :假设一段有问题的代码在循环中执行,每次循环都可能抛出异常。如果每次异常都触发一封邮件,运维人员的邮箱会在几秒钟内被塞爆。我们必须引入“熔断”或“限流”机制。一个简单有效的策略是: 对同一异常(可通过异常类型和堆栈跟踪的哈希值来标识)进行频率限制 ,例如“相同异常在10分钟内最多发送一次报警邮件”。这需要在内存或分布式缓存(如Redis)中记录最近发送的异常指纹和时间戳。

3. 分步实现与核心代码解析

我们将以C#/.NET环境为例,结合使用内置SMTP和异步编程,实现一个基础但实用的版本。我会先给出一个“快速实现”版本,再逐步优化到“生产可用”版本。

3.1 第一步:基础实现 - 最简单的内联版本

这个版本帮助理解流程,但 不推荐用于生产环境

using System.Net.Mail;
using System.Net;

public void ProcessOrder(Order order)
{
    try
    {
        // 核心业务逻辑
        ValidateOrder(order);
        ChargePayment(order);
        UpdateInventory(order);
        SendConfirmationEmail(order);
    }
    catch (Exception ex)
    {
        // 1. 记录日志(必须做)
        _logger.LogError(ex, "处理订单 {OrderId} 时发生异常", order.Id);

        // 2. 构造邮件内容
        string subject = $"【系统异常报警】订单处理失败 - {order.Id}";
        string body = $@"
            发生时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}
            订单ID: {order.Id}
            异常类型: {ex.GetType().Name}
            异常信息: {ex.Message}
            堆栈跟踪: 
            {ex.StackTrace}
            ";

        // 3. 配置并发送邮件(同步,会阻塞!)
        using (var smtpClient = new SmtpClient("smtp.your-company.com", 587))
        {
            smtpClient.Credentials = new NetworkCredential("alerts@your-company.com", "YourPassword");
            smtpClient.EnableSsl = true;

            var mailMessage = new MailMessage
            {
                From = new MailAddress("alerts@your-company.com"),
                Subject = subject,
                Body = body,
                IsBodyHtml = false // 报警邮件建议用纯文本,兼容性更好
            };
            mailMessage.To.Add("dev-team@your-company.com");

            smtpClient.Send(mailMessage); // 同步发送,问题所在!
        }

        // 4. 可选择重新抛出或进行其他错误处理
        throw new ApplicationException($"订单处理失败,已通知管理员。原始错误: {ex.Message}", ex);
    }
}

这个版本的问题

  1. 配置硬编码 :SMTP服务器、密码、收件人全都写死在代码里。
  2. 同步阻塞 smtpClient.Send 是同步调用,会阻塞当前线程。
  3. 无错误处理 :如果发邮件本身失败(如网络问题),这个错误会被忽略,且可能掩盖原始业务异常。
  4. 代码重复 :每个需要异常处理的地方都要复制粘贴这段冗长的邮件发送代码。
  5. 无过滤限流 :任何异常都会发邮件。

3.2 第二步:优化版本 - 封装服务与异步化

我们来解决上述的大部分问题。首先,将邮件发送功能抽象成一个独立的服务。

3.2.1 创建配置模型

appsettings.json 中配置:

{
  "EmailNotificationSettings": {
    "SmtpServer": "smtp.office365.com",
    "SmtpPort": 587,
    "SenderEmail": "alerts@yourdomain.com",
    "SenderPassword": "", // 应从环境变量或密钥库读取
    "AdminEmail": "oncall-engineer@yourdomain.com",
    "EnableSsl": true
  }
}

3.2.2 创建邮件通知服务接口与实现

public interface IEmailNotificationService
{
    Task SendErrorNotificationAsync(Exception exception, string contextMessage = null, IDictionary<string, object> additionalData = null);
}

public class SmtpEmailNotificationService : IEmailNotificationService
{
    private readonly ILogger<SmtpEmailNotificationService> _logger;
    private readonly EmailNotificationSettings _settings;

    public SmtpEmailNotificationService(IOptions<EmailNotificationSettings> settings, ILogger<SmtpEmailNotificationService> logger)
    {
        _settings = settings.Value;
        _logger = logger;
    }

    public async Task SendErrorNotificationAsync(Exception exception, string contextMessage = null, IDictionary<string, object> additionalData = null)
    {
        if (exception == null) throw new ArgumentNullException(nameof(exception));

        try
        {
            var subject = $"🚨 应用异常报警: {exception.GetType().Name}";
            var body = BuildEmailBody(exception, contextMessage, additionalData);

            using (var smtpClient = new SmtpClient(_settings.SmtpServer, _settings.SmtpPort))
            {
                smtpClient.Credentials = new NetworkCredential(_settings.SenderEmail, _settings.SenderPassword);
                smtpClient.EnableSsl = _settings.EnableSsl;

                using (var mailMessage = new MailMessage())
                {
                    mailMessage.From = new MailAddress(_settings.SenderEmail);
                    mailMessage.To.Add(_settings.AdminEmail);
                    mailMessage.Subject = subject;
                    mailMessage.Body = body;
                    mailMessage.IsBodyHtml = false;

                    // 关键:使用异步发送,避免阻塞调用线程
                    await smtpClient.SendMailAsync(mailMessage).ConfigureAwait(false);
                }
            }
            _logger.LogInformation("异常报警邮件已发送。异常: {ExceptionType}", exception.GetType().Name);
        }
        catch (Exception emailEx)
        {
            // 如果发邮件本身失败,记录严重日志,但不要掩盖原始异常
            _logger.LogCritical(emailEx, "发送异常报警邮件时失败!原始异常信息可能丢失。原始异常: {OriginalException}", exception.ToString());
            // 这里可以选择将邮件发送失败的信息写入一个高优先级的本地日志文件,或推送到其他监控系统(如Sentry)
        }
    }

    private string BuildEmailBody(Exception ex, string context, IDictionary<string, object> data)
    {
        var sb = new StringBuilder();
        sb.AppendLine($"发生时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
        sb.AppendLine($"机器名称: {Environment.MachineName}");
        sb.AppendLine($"应用程序: {AppDomain.CurrentDomain.FriendlyName}");
        sb.AppendLine();
        if (!string.IsNullOrEmpty(context))
        {
            sb.AppendLine($"上下文信息: {context}");
            sb.AppendLine();
        }
        sb.AppendLine($"异常类型: {ex.GetType().FullName}");
        sb.AppendLine($"异常消息: {ex.Message}");
        sb.AppendLine();
        sb.AppendLine("=== 堆栈跟踪 ===");
        sb.AppendLine(ex.StackTrace);
        sb.AppendLine();

        if (ex.InnerException != null)
        {
            sb.AppendLine("=== 内部异常 ===");
            sb.AppendLine($"类型: {ex.InnerException.GetType().FullName}");
            sb.AppendLine($"消息: {ex.InnerException.Message}");
            sb.AppendLine($"堆栈: {ex.InnerException.StackTrace}");
            sb.AppendLine();
        }

        if (additionalData != null && additionalData.Any())
        {
            sb.AppendLine("=== 附加数据 ===");
            foreach (var kvp in additionalData)
            {
                sb.AppendLine($"{kvp.Key}: {kvp.Value}");
            }
        }
        return sb.ToString();
    }
}

3.2.3 在业务代码中使用

现在,业务代码变得非常简洁:

public class OrderProcessor
{
    private readonly IEmailNotificationService _emailService;
    private readonly ILogger<OrderProcessor> _logger;

    public OrderProcessor(IEmailNotificationService emailService, ILogger<OrderProcessor> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }

    public async Task ProcessOrderAsync(Order order)
    {
        try
        {
            // 核心业务逻辑
            await ValidateOrderAsync(order);
            await ChargePaymentAsync(order);
            // ... 其他操作
        }
        catch (Exception ex)
        {
            // 1. 记录日志
            _logger.LogError(ex, "处理订单 {OrderId} 时发生异常", order.Id);

            // 2. 准备附加数据,为邮件提供更多上下文
            var additionalData = new Dictionary<string, object>
            {
                { "OrderId", order.Id },
                { "CustomerId", order.CustomerId },
                { "OrderAmount", order.TotalAmount }
            };

            // 3. 发送邮件通知(异步,不阻塞)
            // 使用 `_ =` 或 `Task.Run` 使其“即发即忘”,不等待结果,避免影响当前请求的响应。
            // 但需注意:如果发送失败,错误只会在邮件服务内部记录。
            _ = _emailService.SendErrorNotificationAsync(ex, $"订单处理失败 (ID: {order.Id})", additionalData);

            // 4. 根据业务需求,决定是向上抛出异常,还是返回一个错误结果
            throw; // 重新抛出,让上层(如Controller的全局过滤器)处理HTTP错误响应
        }
    }
}

实操心得:使用 _ = Task.Run 来触发异步邮件发送是一个常见模式,它实现了“即发即忘”(Fire-and-Forget)。但这里有一个 重要的陷阱 :如果应用突然重启(如IIS回收工作进程),这个后台任务可能会被强行终止,导致邮件发送失败且无迹可寻。对于关键报警,更可靠的做法是使用 BackgroundService 、Hangfire或先将通知任务持久化到数据库/队列中,再由一个稳定的后台进程处理。

4. 进阶:构建生产级全局异常处理中间件

在Web API(如ASP.NET Core)中,更优雅的做法是使用 异常处理中间件 异常过滤器 。这样可以集中处理所有未处理的异常,避免在每个Action或Service中重复 try/catch

4.1 创建自定义异常处理中间件

public class GlobalExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionHandlingMiddleware> _logger;
    private readonly IEmailNotificationService _emailService;

    public GlobalExceptionHandlingMiddleware(RequestDelegate next, ILogger<GlobalExceptionHandlingMiddleware> logger, IEmailNotificationService emailService)
    {
        _next = next;
        _logger = logger;
        _emailService = emailService;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context); // 执行管道中的下一个组件(如MVC)
        }
        catch (Exception ex)
        {
            // 记录日志
            _logger.LogError(ex, "全局捕获到未处理异常。请求路径: {Path}", context.Request.Path);

            // 准备附加数据
            var additionalData = new Dictionary<string, object>
            {
                { "RequestPath", context.Request.Path },
                { "RequestMethod", context.Request.Method },
                { "User", context.User?.Identity?.Name ?? "Anonymous" },
                { "TraceIdentifier", context.TraceIdentifier }
            };

            // 发送邮件通知(异步)
            _ = _emailService.SendErrorNotificationAsync(ex, "Web API 全局异常", additionalData);

            // 向客户端返回一个友好的错误响应
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = "application/json";
            var errorResponse = new { error = "服务器内部错误,已通知管理员。", requestId = context.TraceIdentifier };
            await context.Response.WriteAsJsonAsync(errorResponse);
        }
    }
}

Program.cs Startup.cs 中注册这个中间件, 确保它被添加在管道的最开始 (或至少在其他可能抛出异常的中间件之前)。

app.UseMiddleware<GlobalExceptionHandlingMiddleware>();
// ... 其他中间件,如 UseRouting, UseAuthentication, UseAuthorization, MapControllers

4.2 实现异常过滤与频率限制

现在我们来解决“通知风暴”问题。我们需要一个能记住近期已发送异常的服务。

4.2.1 创建带限流的邮件通知服务

public class RateLimitedEmailNotificationService : IEmailNotificationService
{
    private readonly IEmailNotificationService _innerService;
    private readonly IMemoryCache _cache; // 使用内存缓存,对于分布式应用需改用IDistributedCache(如Redis)
    private readonly ILogger<RateLimitedEmailNotificationService> _logger;
    private readonly TimeSpan _suppressionWindow = TimeSpan.FromMinutes(10); // 10分钟内相同异常只发一次

    public RateLimitedEmailNotificationService(IEmailNotificationService innerService, IMemoryCache cache, ILogger<RateLimitedEmailNotificationService> logger)
    {
        _innerService = innerService;
        _cache = cache;
        _logger = logger;
    }

    public async Task SendErrorNotificationAsync(Exception exception, string contextMessage = null, IDictionary<string, object> additionalData = null)
    {
        // 生成异常的唯一指纹:类型 + 堆栈跟踪的前几行(忽略行号等可变信息)
        string exceptionFingerprint = GenerateExceptionFingerprint(exception);

        // 检查缓存中是否存在该指纹
        if (_cache.TryGetValue(exceptionFingerprint, out _))
        {
            _logger.LogDebug("异常指纹 {Fingerprint} 在抑制窗口内,跳过邮件通知。", exceptionFingerprint);
            return; // 在抑制期内,直接跳过
        }

        // 发送邮件
        await _innerService.SendErrorNotificationAsync(exception, contextMessage, additionalData);

        // 将指纹存入缓存,并设置过期时间(即抑制窗口)
        var cacheEntryOptions = new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = _suppressionWindow
        };
        _cache.Set(exceptionFingerprint, DateTime.UtcNow, cacheEntryOptions);

        _logger.LogInformation("已发送异常报警邮件并设置抑制窗口。指纹: {Fingerprint}", exceptionFingerprint);
    }

    private string GenerateExceptionFingerprint(Exception ex)
    {
        // 一个简单的指纹生成逻辑:类型名 + 堆栈中第一个非系统库的方法名
        var stackTrace = new StackTrace(ex, true); // 获取堆栈
        var firstFrame = stackTrace.GetFrames()?.FirstOrDefault(f => !IsSystemFrame(f));
        string methodInfo = firstFrame != null ? $"{firstFrame.GetMethod()?.DeclaringType?.FullName}.{firstFrame.GetMethod()?.Name}" : "UnknownMethod";

        // 可以加上异常消息的前N个字符,但要小心消息里包含可变数据(如ID)
        string messagePrefix = ex.Message.Length > 50 ? ex.Message.Substring(0, 50) : ex.Message;
        // 清理可变数据(这是一个简化示例,实际可能需要更复杂的正则匹配)
        messagePrefix = System.Text.RegularExpressions.Regex.Replace(messagePrefix, @"\d+", "#"); // 替换数字

        return $"{ex.GetType().FullName}:{methodInfo}:{messagePrefix}".GetHashCode().ToString(); // 取哈希值作为键
    }

    private bool IsSystemFrame(StackFrame frame)
    {
        var assemblyName = frame.GetMethod()?.DeclaringType?.Assembly.GetName().Name;
        return assemblyName != null && (assemblyName.StartsWith("System") || assemblyName.StartsWith("Microsoft"));
    }
}

然后在依赖注入容器中注册这个装饰器:

// 注册基础服务
builder.Services.AddSingleton<IEmailNotificationService, SmtpEmailNotificationService>();
// 用限流装饰器包装基础服务
builder.Services.Decorate<IEmailNotificationService, RateLimitedEmailNotificationService>();
// 需要 `Scrutor` 库来使用 `Decorate` 方法,或者手动注册。

5. 常见问题、排查技巧与实操心得

即使代码写好了,在实际部署和运行中,你一定会遇到各种问题。下面是我总结的“避坑指南”。

5.1 邮件发送失败问题排查

当你发现邮件没发出去时,可以按照以下流程排查:

问题现象 可能原因 排查步骤
无任何日志,邮件石沉大海 1. 异常未被捕获。
2. 邮件发送代码在异常发生前已出错。
3. 日志级别设置过高,忽略了Information/Debug日志。
1. 检查 try/catch 范围是否正确。
2. 在邮件发送代码前后加详细日志。
3. 将日志级别暂时调整为 Debug Trace
日志显示“发送异常报警邮件时失败” 1. SMTP服务器地址/端口错误。
2. 用户名/密码(或API Key)错误。
3. 发送方邮箱未启用SMTP或需要应用专用密码。
4. 网络防火墙/安全组阻止了出站连接。
5. 收件人地址被拒绝。
1. 使用 telnet smtp.server.com 587 测试网络连通性。
2. 用代码外的工具(如Outlook)测试同一组凭据。
3. 检查邮箱提供商的安全设置(如Gmail需开启“安全性较低的应用”或使用OAuth2)。
4. 查看邮件服务返回的详细SMTP错误码。
邮件进入垃圾箱 1. 发件域名SPF/DKIM/DMARC记录未设置或错误。
2. 邮件内容触发垃圾邮件过滤器(如过多链接、敏感词汇)。
3. 发信IP信誉差。
1. 使用第三方工具(如MXToolbox)检查发件域名的DNS记录。
2. 优化邮件标题和正文,避免像广告。
3. 考虑使用专业邮件发送服务(SendGrid等)。
异步发送“即发即忘”导致邮件丢失 应用重启或进程回收中断了未完成的 Task 1. 对于关键报警,改用 BackgroundService 或队列。
2. 实现一个简单的内存队列,由托管服务消费。

5.2 配置管理安全要点

绝对不要提交敏感信息到代码仓库 。这是安全红线。

  1. 开发环境 :使用 appsettings.Development.json 或用户机密(User Secrets)来存储本地测试用的凭据。
    dotnet user-secrets set "EmailNotificationSettings:SenderPassword" "your-password"
    
  2. 生产环境 :使用环境变量或云平台的密钥管理服务。
    • 环境变量 :在服务器或容器中设置 EmailNotificationSettings__SenderPassword
    • Azure :使用 Azure Key Vault。
    • AWS :使用 AWS Secrets Manager。
    • 在代码中通过 Configuration["Key"] 读取,框架会自动处理来源优先级。

5.3 性能与可靠性考量

  1. 连接池与SmtpClient生命周期 SmtpClient 实现了连接池。最佳实践是为每个需要发送的邮件 创建新的 SmtpClient 实例 (在 using 语句中),而不是使用单例。.NET Core 2.0以后, SmtpClient 的静态 Send 方法已被标记过时,就是为了鼓励实例化使用。
  2. 超时设置 :网络不稳定时,默认超时可能过长。可以设置 SmtpClient.Timeout 属性(默认100秒),避免线程长时间阻塞。
    smtpClient.Timeout = 30000; // 30秒
    
  3. 失败重试 :网络瞬时故障可能导致发送失败。可以实现简单的指数退避重试逻辑。
    public async Task SendWithRetryAsync(MailMessage message, int maxRetries = 3)
    {
        for (int i = 0; i < maxRetries; i++)
        {
            try
            {
                await smtpClient.SendMailAsync(message);
                return; // 成功则退出
            }
            catch (SmtpException) when (i < maxRetries - 1) // 捕获SMTP异常,且不是最后一次重试
            {
                var delay = TimeSpan.FromSeconds(Math.Pow(2, i)); // 指数退避:2, 4, 8秒...
                _logger.LogWarning("发送邮件失败,第 {RetryCount} 次重试将在 {Delay} 秒后执行。", i + 1, delay.TotalSeconds);
                await Task.Delay(delay);
            }
        }
        // 所有重试都失败,抛出最后一次的异常
        throw;
    }
    

5.4 扩展方向:超越邮件

邮件通知是经典方式,但现代运维监控体系中有更多选择。你可以轻松扩展 IEmailNotificationService 接口,实现多通道报警:

  1. 即时通讯工具 :集成 Slack、Microsoft Teams、钉钉、飞书的Webhook,将异常信息发送到群聊。
  2. 监控平台 :将异常信息推送到专业的APM或监控系统,如 Sentry、Application Insights、DataDog、Prometheus + AlertManager。这些平台提供了更强大的聚合、分组、降噪和升级策略。
  3. 短信/电话 :对于P0级(最高优先级)的致命错误,可以通过 Twilio、阿里云等服务的API触发短信或语音电话呼叫。

一个简单的多通道发送器设计如下:

public class MultiChannelNotificationService : IEmailNotificationService
{
    private readonly IEnumerable<INotificationSender> _senders;
    public MultiChannelNotificationService(IEnumerable<INotificationSender> senders) { _senders = senders; }

    public async Task SendErrorNotificationAsync(Exception exception, string contextMessage = null, IDictionary<string, object> additionalData = null)
    {
        var tasks = _senders.Select(sender =>
            sender.SendAsync(exception, contextMessage, additionalData).ContinueWith(t =>
            {
                if (t.IsFaulted) { /* 记录单个通道发送失败,但不影响其他通道 */ }
            })
        );
        await Task.WhenAll(tasks); // 并行发送到所有通道
    }
}

public interface INotificationSender
{
    Task SendAsync(Exception ex, string context, IDictionary<string, object> data);
}
// 然后实现 EmailSender, SlackSender, TeamsSender 等

我个人在实际项目中的体会是, 邮件适合做每日摘要或非紧急报警 ,而 即时通讯工具(如Slack)更适合需要快速响应的实时警报 。将两者结合,并设置合理的路由规则(例如,数据库连接失败发Slack+邮件,某个非核心功能失败只发邮件),能极大提升团队的响应效率,又不会造成信息过载。最后,记住监控系统本身也需要被监控,定期给自己发一封“测试邮件”,确保这个重要的警报通道始终畅通。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值