YtSoft.Gmc.Core.Logging 1.0.3

dotnet add package YtSoft.Gmc.Core.Logging --version 1.0.3
                    
NuGet\Install-Package YtSoft.Gmc.Core.Logging -Version 1.0.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="YtSoft.Gmc.Core.Logging" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="YtSoft.Gmc.Core.Logging" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="YtSoft.Gmc.Core.Logging" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add YtSoft.Gmc.Core.Logging --version 1.0.3
                    
#r "nuget: YtSoft.Gmc.Core.Logging, 1.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package YtSoft.Gmc.Core.Logging@1.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=YtSoft.Gmc.Core.Logging&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=YtSoft.Gmc.Core.Logging&version=1.0.3
                    
Install as a Cake Tool

YtSoft.Gmc.Core.Logging 1.0.3

YtSoft.Gmc.Core.Logging 是统一日志生产端。业务系统通过唯一入口 ILogWriter.Write 写入请求、响应、SQL、异常和其他日志事件,组件将事件投递到 RabbitMQ。组件只负责生产消息,不负责消费消息、写入 MongoDB,也不初始化数据库、网关或其他业务运行时实例。

安装和包关系

业务系统只需要引用 Logging 包这一条 PackageReference:

<PackageReference Include="YtSoft.Gmc.Core.Logging" Version="1.0.3" />

Logging 的包依赖会自动传递引入 YtSoft.Gmc.Core.Config 1.0.2 和 YtSoft.Gmc.Core.Logging.Contracts 1.0.0,业务项目不需要重复添加这两个包。准备私有或本地 NuGet 源时,先确保源中可以还原 Config 1.0.2,再准备 Contracts 1.0.0,最后准备 Logging 1.0.3;Contracts 和 Logging 的公开发布状态以实际使用的 NuGet 源为准,本文不作预设。

Config 1.0.2 已发布,可直接从公网源还原。Config 负责 Nacos 加载、环境筛选、本地回退和配置绑定,详细说明见 YtSoft.Gmc.Core.Config README。

业务系统最小接入示例

下面是一个可直接放入 ASP.NET Core Web 项目的完整 Program.cs 示例。它使用同一个 ConfigurationManager,先等待 Config 完成远端首载或本地回退,再注册 Logging;await using 覆盖配置运行时和整个 Host 生命周期。示例按 .NET 6 Web SDK 默认的 ImplicitUsings=enable 编写,同时保留关键命名空间的显式 using,关闭隐式 using 时请保留项目模板生成的基础 using。

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using YtSoft.Gmc.Core.Config;
using YtSoft.Gmc.Core.Logging;
using YtSoft.Gmc.Core.Logging.Contracts;

var builder = WebApplication.CreateBuilder(args);

// Config 返回前已经完成 Nacos 首载,Nacos 不可用时会读取 appsettings.json。
await using var configRuntime = await builder.Services.AddYtSoftConfigAsync(
    builder.Configuration,
    config => config.UseLocalFallback(
        Path.Combine(builder.Environment.ContentRootPath, "appsettings.json")));

// MQ 地址、账号、密码和交换机来自 Config 的 Middleware.MqConnections。
builder.Services.AddYtSoftLogging(options =>
{
    options.ServiceName = "orders-api";
    options.Module = "orders";
});
builder.Services.AddSingleton<SqlAuditService>();

var app = builder.Build();

app.MapPost("/orders", (
    OrderRequest order,
    HttpContext httpContext,
    ILogWriter writer,
    SqlAuditService sqlAudit) =>
{
    var stopwatch = Stopwatch.StartNew();
    var requestedTradeId = httpContext.Request.Headers["X-Trade-Id"].FirstOrDefault();

    // 传入 X-Trade-Id 时沿用调用方标识;没有传入时由 Logging 生成。
    using var scope = LogContext.BeginScope(new LogRequestContext
    {
        TradeId = requestedTradeId,
        Route = "/orders",
        Method = httpContext.Request.Method,
        Url = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}{httpContext.Request.Path}{httpContext.Request.QueryString}",
        HostName = Environment.MachineName,
        Headers = httpContext.Request.Headers.ToDictionary(
            pair => pair.Key,
            pair => pair.Value.ToArray(),
            StringComparer.OrdinalIgnoreCase)
    });

    var requestResult = writer.Write(new LogWriteRequest
    {
        Kind = LogKind.Request,
        Level = LogSeverity.Information,
        ContentFormat = LogContentFormat.Json,
        Content = JsonSerializer.Serialize(order)
    });

    // 服务层沿用当前异步日志作用域,不要在服务内部再次 BeginScope。
    var sqlResult = sqlAudit.WriteSql(
        writer,
        "select OrderNo, Amount from Orders where OrderNo = @OrderNo");

    var response = new { order.OrderNo, order.Amount, Saved = true };
    var responseResult = writer.Write(new LogWriteRequest
    {
        Kind = LogKind.Response,
        Level = LogSeverity.Information,
        ContentFormat = LogContentFormat.Json,
        Content = JsonSerializer.Serialize(response),
        StatusCode = StatusCodes.Status200OK,
        DurationMs = stopwatch.Elapsed.TotalMilliseconds,
        IsTerminal = true
    });

    // 没有 X-Trade-Id 时,scope.TradeId 就是自动生成的值;将它返回或传给下游。
    return Results.Ok(new
    {
        tradeId = scope.TradeId,
        requestAccepted = requestResult.Accepted,
        sqlAccepted = sqlResult.Accepted,
        responseAccepted = responseResult.Accepted
    });
});

await app.RunAsync();

public sealed record OrderRequest(string OrderNo, decimal Amount);

public sealed class SqlAuditService
{
    public LogWriteResult WriteSql(ILogWriter writer, string sql)
    {
        // 这里只记录 SQL 字符串,不执行数据库操作,也不创建新的日志作用域。
        return writer.Write(new LogWriteRequest
        {
            Kind = LogKind.Sql,
            Level = LogSeverity.Debug,
            ContentFormat = LogContentFormat.Sql,
            Content = sql
        });
    }
}

请求没有 X-Trade-Id 时,LogContext.BeginScope 会生成一个 tradeId,示例把它放进响应;跨 HTTP、消息或后台任务时,应把这个值显式传给下游并作为下游作用域的 LogRequestContext.TradeId。同一个作用域内的 Request、Sql 和 Response 会使用同一个 tradeId,每次 Write 仍有独立的 eventId。

UserCode 按本条 LogWriteRequest 的非空白值、当前作用域的非空白值、Gateway.UserCode 配置依次取值;三处都没有值时保持为空。空白的本条值不会覆盖作用域值,也不会修改调用方请求对象或作用域上下文。UserName 只从本条请求或当前作用域继承,不从 Gateway 或其他配置回退;未传入时事件中的 UserName 为空。

作用域第一次成功入队时会冻结当时的 Config revision、MQ 连接和有效默认目标;后续未显式指定目标的事件继续沿用这份冻结值,不会因为配置刷新而把同一次请求拆到新连接。创建新的 LogContext 作用域后,才会读取新的快照。没有作用域且没有传入 TradeId 时,每次独立 Write 都会生成不同的 tradeId;作用域内传入与当前作用域不一致的显式 TradeId 会被拒绝,避免拆散请求。

生产业务代码只创建 LogWriteRequest 并调用 ILogWriter.Write,不要手工创建 LogEvent。LogEvent 是生产端补齐 schemaVersion、eventId、occurredAt、配置版本和完整目标后发送给消费者的消息模型。

环境变量和本地回退

部署时只需要提供下面五个环境变量;文档不写入任何真实地址、账号或密码:

环境变量 说明
NACOS_SERVERS 一个 Nacos host:port,或使用英文逗号分隔的多个地址;也支持 JSON 字符串数组
NACOS_NAMESPACE Nacos namespace ID
NACOS_USERNAME 可选用户名;设置时必须同时设置密码
NACOS_PASSWORD 可选密码;设置时必须同时设置用户名
ASPNETCORE_ENVIRONMENT Production 选择 net-*-prod.json,Development 选择 net-*-dev.json;缺失时按 Production 处理

本地回退文件只需要保留标准配置节。下面的空 MQ 字段表示示例没有放入连接凭据,部署时应由 Nacos 或受控的本地回退文件提供:

{
  "Database": {},
  "Gateway": {},
  "Middleware": {
    "MqConnections": {
      "HostName": "",
      "UserName": "",
      "Password": "",
      "ExchangeName": "",
      "QueueNames": "gateway-logs,ythis_logs"
    }
  }
}

Config 首载失败时会使用 UseLocalFallback 指定的 appsettings.json;远端和本地都无效时才阻止宿主启动。缺少 MQ 主机、账号、密码,或配置未提供交换机且本次请求也未显式提供交换机时,Write 返回 Accepted=false 和中文 FailureReason,不会使用匿名账号,也不会偷偷连接 localhost。

写入结果和可靠性边界

LogWriteResult.Accepted=true 只表示事件已进入 Logging 的本地有界发送队列,不表示 RabbitMQ 已确认,也不表示 Mqlog 已写入 MongoDB。队列已满、组件已停止、配置缺失、脱敏失败、正文超限或目标无效时返回 Accepted=false;业务代码应按需要记录失败原因或采取降级措施。

后台分发器启动时会在独立后台任务中进行一次 MQ 连接检查;连接失败、启动失败或配置不完整只写独立中文诊断,业务宿主和 Write 继续运行。每次初始化和发送尝试受 YtSoftLoggingOptions.SendTimeout 限制,默认值为 5 秒;发送超时按临时失败执行有限重试,最终失败的完整脱敏事件会按 eventId 写入 FailureDirectory。宿主停止时仍只在 ShutdownFlushTimeout 的有限预算内尽力冲刷。

内存队列和失败目录都不是可靠 outbox;失败文件只是本地降级记录,进程崩溃、磁盘不可用或停止预算耗尽时仍可能丢失日志。需要强一致审计时应另行建设持久化 outbox。

日志类型、正文格式和异常

LogKind 至少包括 Request、Response、Error、Sql、Other;LogContentFormat 包括 Text、Json、Xml、Query、Sql。正文始终作为字符串保存,组件不会把 JSON 或 XML 自动变成业务对象。

writer.Write(new LogWriteRequest
{
    Kind = LogKind.Error,
    Level = LogSeverity.Error,
    ContentFormat = LogContentFormat.Text,
    Content = exception.Message,
    Exception = new LogExceptionInfo
    {
        Type = exception.GetType().FullName,
        Message = exception.Message,
        StackTrace = exception.StackTrace
    }
});

writer.Write(new LogWriteRequest
{
    Kind = LogKind.Other,
    Level = LogSeverity.Information,
    ContentFormat = LogContentFormat.Xml,
    Content = "<audit><action>refresh</action></audit>"
});

XML 不能依赖业务对象反序列化;包含 DOCTYPE 时 Sanitizer 会将 XML 整体替换为脱敏文本,这属于脱敏结果,不等同于写入失败。正文和异常会按 Logging 的限制脱敏,超限时返回失败,不静默截断。Query 适合记录 GET 查询参数,Sql 只表示日志正文的格式,不会执行 SQL。

MQ 默认目标、拓扑参数和自定义队列

当 LogWriteRequest.Destination 为空时,交换机使用 Middleware.MqConnections.ExchangeName,队列和路由键默认使用 gateway-logs。可以逐条覆盖目标:

var result = writer.Write(new LogWriteRequest
{
    Kind = LogKind.Other,
    ContentFormat = LogContentFormat.Text,
    Content = "发送到业务自有队列",
    Destination = new LogDestination
    {
        QueueName = "orders-private-logs"
        // ExchangeName、RoutingKey 为 null 时继承默认值;队列被覆盖时路由键默认取该队列名。
    }
});

显式空白目标会被拒绝,省略字段才表示继承。Middleware.MqConnections.QueueNames 只是统一消费端的订阅名单,不是生产端的发送白名单。自定义队列即使不在名单中,Logging 仍会发送;统一的 YTSoft.GMC.Mqlog 不订阅该队列,由业务系统自行消费。默认 gateway-logs 和 Nacos 名单中的 gateway-logs 是否被统一消费,取决于该队列是否在 QueueNames 中。

注册 Logging 时,DeclareTopology 默认保持为 true,首条事件发送前会按下表声明交换机、队列并绑定精确的 routing key。下表的默认值按当前共享 MQ 的既有资源核实,不能推断任意未知 MQ 环境的资源属性:

选项 默认值 说明
ExchangeType topic 交换机类型
ExchangeDurable false 交换机不持久化
ExchangeAutoDelete false 交换机不自动删除
ExchangeArguments null 不附加交换机声明参数
QueueDurable true 队列持久化
QueueExclusive false 队列不排他
QueueAutoDelete false 队列不自动删除
QueueArguments x-max-length = 1000000(int) 默认限制队列长度

ExchangeArguments、QueueArguments 会在选项注册完成后复制一层,避免注册用字典之后继续变更;字典值不会被转换。需要为不同环境注册不同属性时,在各自宿主的 AddYtSoftLogging 中显式覆盖:

builder.Services.AddYtSoftLogging(options =>
{
    options.ExchangeDurable = true;
    options.ExchangeArguments = new Dictionary<string, object?>
    {
        ["alternate-exchange"] = "orders-unrouted"
    };
    options.QueueArguments = new Dictionary<string, object?>
    {
        ["x-max-length"] = 500_000
    };
});

若既有队列没有默认长度参数,可在注册时明确取消默认值:

builder.Services.AddYtSoftLogging(options =>
{
    options.QueueArguments!.Clear(); // 传递空参数字典
    // 或:options.QueueArguments = null; // 不传队列参数
});

RabbitMQ 对已存在资源的类型、持久化、自动删除、排他和 arguments 要求一致;属性不一致会在对应声明阶段返回 406。Logging 不会删除、重建、探测后静默改写或自动适配既有资源,业务应先核对运维创建的资源,再选择与其一致的注册参数。声明参数属于宿主注册配置,变更后重启应用生效;运行中的选项刷新不会触发复杂的拓扑重建。

默认交换机是非持久化的,Broker 重启后可能被删除;默认队列是持久化的,但队列长度上限仍可能丢弃旧消息。客户端启用连接和拓扑自动恢复,只会在连接恢复条件满足时尽力恢复当前连接上的声明,不能绕过权限、策略、资源被外部删除或属性冲突,也不保证任意未知 MQ 的拓扑成功。请在 Broker 重启或运维调整后核对交换机、队列和 binding。

消息会携带 MessageId=eventId、CorrelationId=tradeId 和日志类型 Type,并使用持久化消息和 mandatory=true。生产端默认启用 RabbitMQ 发布确认,对不可路由、网络断开或确认未知的事件按 eventId 有限重试;权限或拓扑 403/406 等永久错误停止重试并写中文阶段诊断。设置 YtSoftLoggingOptions.DeclareTopology=false 可关闭声明,但此时应用只负责发布,必须由运维提前准备精确 binding。

组件边界

Logging 从 Config 已加载的 IGmcConfigStore 原子快照获取 MQ 连接信息;快照不可用时使用已注册的 IOptionsMonitor<MiddlewareOptions> 当前值。不读取 Nacos SDK、DataId 或连接环境变量,也不创建 MongoDB 客户端,不负责消费和聚合;统一消费端或业务自有消费者应引用 Logging.Contracts 处理消息。

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.3 0 10/20/2026
1.0.1 46 10/16/2026
1.0.0 52 10/15/2026