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
<PackageReference Include="YtSoft.Gmc.Core.Logging" Version="1.0.3" />
<PackageVersion Include="YtSoft.Gmc.Core.Logging" Version="1.0.3" />
<PackageReference Include="YtSoft.Gmc.Core.Logging" />
paket add YtSoft.Gmc.Core.Logging --version 1.0.3
#r "nuget: YtSoft.Gmc.Core.Logging, 1.0.3"
#:package YtSoft.Gmc.Core.Logging@1.0.3
#addin nuget:?package=YtSoft.Gmc.Core.Logging&version=1.0.3
#tool nuget:?package=YtSoft.Gmc.Core.Logging&version=1.0.3
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 | Versions 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. |
-
net6.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 6.0.0)
- Microsoft.Extensions.Options (>= 6.0.0)
- RabbitMQ.Client (>= 7.0.0)
- YtSoft.Gmc.Core.Config (>= 1.0.2)
- YtSoft.Gmc.Core.Logging.Contracts (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.