HJL WebAPI 项目日志入库实战:从建表到自动清理
一、背景
HJL 项目是一个 .NET 8 WebAPI 程序,原来使用 NLog 写文本日志。随着业务量增长,服务器磁盘 IO 和日志文件管理成为痛点。本文记录将日志全部写入 Oracle 数据库 、并配置自动清理策略的完整过程。
二、技术栈
| 组件 | 版本/说明 |
|---|---|
| 框架 | .NET 8 WebAPI |
| 日志框架 | NLog + NLog.Web.AspNetCore |
| 数据库 | Oracle |
| ORM | SQLSugar(业务库) |
| 驱动 | Oracle.ManagedDataAccess.Core |
三、第一步:数据库建表
在 Oracle 中创建专用日志表 HJL_API_APPLOG,表名含义:HJL 项目 - WebAPI - 应用日志。
3.1 建表 SQL
sql
-- ============================================
-- HJL WebAPI 应用日志表
-- 说明:替代服务器文本日志,所有日志只写入数据库
-- 保留策略:180 天(由 Oracle Job 自动清理)
-- ============================================
CREATE TABLE MESFLXRPT.HJL_API_APPLOG (
ID VARCHAR2(32) PRIMARY KEY, -- 主键,程序生成 GUID
LOG_LEVEL VARCHAR2(20) NOT NULL, -- 日志级别:Info/Warning/Error/Critical
CATEGORY VARCHAR2(255), -- 日志来源(Logger 名称,如 Controller 全名)
MESSAGE CLOB, -- 日志正文
EXCEPTION CLOB, -- 异常堆栈(Error 级别时写入)
CLASS_NAME VARCHAR2(500), -- 产生日志的完整类名
METHOD_NAME VARCHAR2(255), -- 产生日志的方法名(Action 名称)
THREAD_ID VARCHAR2(50), -- 线程 ID(排查并发问题)
TRACE_ID VARCHAR2(100), -- HTTP 请求追踪 ID(同一次请求的所有日志共享)
IP_ADDRESS VARCHAR2(50), -- 客户端请求 IP
CREATE_TIME DATE DEFAULT SYSDATE NOT NULL -- 日志产生时间
);
-- 表注释
COMMENT ON TABLE MESFLXRPT.HJL_API_APPLOG IS 'HJL项目WebAPI应用日志表:替代服务器文本日志,保留180天后自动清理';
-- 字段注释
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.ID IS '主键,程序生成的32位大写GUID';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.LOG_LEVEL IS '日志级别:Trace/Debug/Info/Warning/Error/Critical。生产环境建议只存Info及以上';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.CATEGORY IS '日志来源(Logger名称),通常是类的全限定名';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.MESSAGE IS '日志正文内容,CLOB大字段支持长文本';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.EXCEPTION IS '异常堆栈详情,仅Error/Critical级别时可能有值';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.CLASS_NAME IS '产生日志的完整类名(含命名空间)';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.METHOD_NAME IS '产生日志的方法名,WebAPI项目使用aspnet-mvc-action获取Action名称';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.THREAD_ID IS '当前托管线程ID,用于分析并发问题';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.TRACE_ID IS 'HTTP请求追踪标识(HttpContext.TraceIdentifier),实现全链路追踪';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.IP_ADDRESS IS '客户端请求IP地址,用于安全审计';
COMMENT ON COLUMN MESFLXRPT.HJL_API_APPLOG.CREATE_TIME IS '日志产生时间,默认SYSDATE,作为数据清理和查询的时间依据';
-- 索引:按时间查询和清理(最常用)
CREATE INDEX IDX_HJL_LOG_TIME ON MESFLXRPT.HJL_API_APPLOG(CREATE_TIME);
-- 索引:按日志级别筛选(查Error时提速)
CREATE INDEX IDX_HJL_LOG_LEVEL ON MESFLXRPT.HJL_API_APPLOG(LOG_LEVEL);
-- 索引:按业务模块筛选(查特定Controller日志时提速)
CREATE INDEX IDX_HJL_LOG_CATEGORY ON MESFLXRPT.HJL_API_APPLOG(CATEGORY);
3.2 设计说明
- 为什么用 CLOB? 异常堆栈可能很长,VARCHAR2 存不下。
- 为什么必须建时间索引? 日志查询和定时清理都按
CREATE_TIME过滤,没有索引全表扫描性能极差。 - 为什么保留 180 天? 生产环境半年前的日志参考价值低,定期清理防止表膨胀。
四、第二步:安装 NLog 相关 NuGet 包
在 WebAPI 项目上安装以下包:
bash
# NLog 核心包
dotnet add package NLog
# ASP.NET Core 扩展(提供 TraceId、IP 等变量)
dotnet add package NLog.Web.AspNetCore
# 数据库支持(DatabaseTarget)
dotnet add package NLog.Database
# Oracle 驱动(.NET 8 用 Core 版本)
dotnet add package Oracle.ManagedDataAccess.Core
五、第三步:配置 appsettings.json
在 appsettings.json 中配置 Oracle 连接串,不要写死在 NLog 配置文件里。
json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"strConnFlexMesInterface": "Data Source=你的TNS;User Id=用户名;Password=密码;"
}
}
安全提示 :生产环境使用
appsettings.Production.json,并加入.gitignore,避免密码提交到版本库。
六、第四步:配置 nlog.config
在项目根目录创建 nlog.config,属性设置为"复制到输出目录 → 始终复制"。
xml
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
autoReload="true"
throwExceptions="false"
internalLogLevel="Warn"
internalLogFile="${basedir}/NLog/internal.log">
<!-- 启用 ASP.NET Core 扩展,提供 TraceId 和 IP 变量 -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<targets>
<!--
数据库日志目标(三层包装)
第1层:AsyncWrapper(异步缓冲)
- 日志先存入内存队列,攒够 50 条或每 1 秒批量写入数据库
- 避免每条日志都等待数据库 IO,提升 API 响应速度
第2层:RetryingWrapper(失败重试)
- 数据库断线或锁表时自动重试 3 次
第3层:DatabaseTarget(数据库写入)
- 执行 Oracle INSERT 语句
-->
<target xsi:type="AsyncWrapper"
name="db_async"
batchSize="50"
timeToSleepBetweenBatches="1000"
queueLimit="10000"
overflowAction="Grow">
<target xsi:type="RetryingWrapper"
retryCount="3"
retryDelayMilliseconds="500">
<target xsi:type="Database"
name="db_log"
<!--
【重要】程序集名必须是 Oracle.ManagedDataAccess
NuGet 包名是 Oracle.ManagedDataAccess.Core,但程序集名没有 .Core 后缀!
这是最常见的坑,写错会导致 Could not load file or assembly 错误。
-->
dbProvider="Oracle.ManagedDataAccess.Client.OracleConnection, Oracle.ManagedDataAccess"
connectionString="${configsetting:item=ConnectionStrings.strConnFlexMesInterface}"
commandText="INSERT INTO MESFLXRPT.HJL_API_APPLOG (
ID, LOG_LEVEL, CATEGORY, MESSAGE, EXCEPTION,
CLASS_NAME, METHOD_NAME, THREAD_ID, TRACE_ID, IP_ADDRESS, CREATE_TIME
) VALUES (
:Id, :LogLevel, :Category, :Message, :Exception,
:ClassName, :MethodName, :ThreadId, :TraceId, :IpAddress, TO_DATE(:CreateTime, 'YYYY-MM-DD HH24:MI:SS')
)">
<!-- 32位大写GUID -->
<parameter name="Id" layout="${guid:format=N:uppercase=true}" />
<!-- 日志级别大写:INFO/ERROR/WARNING -->
<parameter name="LogLevel" layout="${level:uppercase=true}" />
<!-- 日志来源(Logger名称) -->
<parameter name="Category" layout="${logger}" />
<!-- 日志正文 -->
<parameter name="Message" layout="${message}" />
<!-- 异常堆栈。Oracle CLOB 足够大,不需要截断 -->
<parameter name="Exception" layout="${exception:format=toString,Data}" />
<!-- 完整类名 -->
<parameter name="ClassName" layout="${logger}" />
<!--
【重要】WebAPI 项目获取 Action 方法名
使用 ${aspnet-mvc-action} 直接从 ASP.NET Core 路由上下文读取,
比 ${callsite} 更适合 async Controller,避免抓到编译器生成的状态机方法名。
-->
<parameter name="MethodName" layout="${aspnet-mvc-action}" />
<!-- 线程ID -->
<parameter name="ThreadId" layout="${threadid}" />
<!--
HTTP 请求追踪 ID。
whenEmpty 处理非 HTTP 场景(如后台线程),从 MDLC 上下文读取。
-->
<parameter name="TraceId" layout="${aspnet-TraceIdentifier:whenEmpty=${mdlc:item=TraceId}}" />
<!-- 客户端IP -->
<parameter name="IpAddress" layout="${aspnet-request-ip:whenEmpty=${mdlc:item=ClientIp}}" />
<!-- 日志时间 -->
<parameter name="CreateTime" layout="${date:format=yyyy-MM-dd HHmmss}" />
</target>
</target>
</target>
</targets>
<rules>
<!-- 过滤 Microsoft 框架的噪音日志(如 Kestrel 启动信息) -->
<logger name="Microsoft.*" minlevel="Trace" final="true" />
<!-- 业务日志 Info 及以上写入数据库 -->
<logger name="*" minlevel="Info" writeTo="db_async" />
</rules>
</nlog>
6.1 关键踩坑点
| 坑点 | 错误写法 | 正确写法 |
|---|---|---|
| Oracle 程序集名 | Oracle.ManagedDataAccess.Core |
Oracle.ManagedDataAccess(没有 .Core) |
| async 方法名获取 | ${callsite:methodName=true} |
${aspnet-mvc-action}(WebAPI 专用) |
| 时间格式冒号 | HH:mm:ss |
HHmmss(避免 NLog 解析冲突) |
七、第五步:配置 Program.cs
csharp
using NLog.Web;
var builder = WebApplication.CreateBuilder(args);
// 注册 HttpContext 访问器(NLog 获取 TraceId 和 IP 必须)
builder.Services.AddHttpContextAccessor();
// 清除默认日志提供器,启用 NLog
builder.Logging.ClearProviders();
builder.Host.UseNLog();
var app = builder.Build();
app.Run();
八、第六步:Oracle 定时清理 Job
8.1 创建 Job
sql
-- ============================================
-- 创建 HJL 应用日志定时清理 Job
-- 每天凌晨 2 点,删除 180 天前的日志
-- ============================================
-- 安全删除旧 Job(如果存在)
BEGIN
DBMS_SCHEDULER.drop_job('JOB_CLEAN_HJL_APPLOG');
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -27475 THEN
RAISE;
END IF;
END;
/
-- 创建新 Job
BEGIN
DBMS_SCHEDULER.create_job (
job_name => 'JOB_CLEAN_HJL_APPLOG',
job_type => 'PLSQL_BLOCK',
-- 清理逻辑:删除 180 天前的数据
job_action => 'BEGIN
DELETE FROM MESFLXRPT.HJL_API_APPLOG
WHERE CREATE_TIME < SYSDATE - 180;
COMMIT;
END;',
-- 首次执行:今天凌晨 2 点
start_date => TRUNC(SYSDATE) + INTERVAL '2' HOUR,
-- 重复规则:每天凌晨 2 点
repeat_interval => 'FREQ=DAILY; BYHOUR=2; BYMINUTE=0; BYSECOND=0',
enabled => TRUE,
comments => 'HJL WebAPI 日志自动清理:每天凌晨2点删除180天前的应用日志'
);
END;
/
8.2 验证 Job 状态
sql
-- 查看 Job 是否创建成功
SELECT job_name, state, next_run_date, comments
FROM user_scheduler_jobs
WHERE job_name = 'JOB_CLEAN_HJL_APPLOG';
预期结果:
state=SCHEDULEDnext_run_date= 明天凌晨 2 点
九、第七步:测试清理 Job
9.1 插入测试数据
sql
-- 今天的数据(应该保留)
INSERT INTO MESFLXRPT.HJL_API_APPLOG (
ID, LOG_LEVEL, CATEGORY, MESSAGE, CLASS_NAME,
METHOD_NAME, THREAD_ID, TRACE_ID, IP_ADDRESS, CREATE_TIME
) VALUES (
'TEST_KEEP', 'INFO', 'Test', '今天的数据,应该保留', 'Test',
'Test', '1', 'TRACE001', '127.0.0.1', SYSDATE
);
-- 5 天前的数据(应该被删除)
INSERT INTO MESFLXRPT.HJL_API_APPLOG (
ID, LOG_LEVEL, CATEGORY, MESSAGE, CLASS_NAME,
METHOD_NAME, THREAD_ID, TRACE_ID, IP_ADDRESS, CREATE_TIME
) VALUES (
'TEST_DEL1', 'INFO', 'Test', '5天前的数据,应该被删除', 'Test',
'Test', '1', 'TRACE002', '127.0.0.1', SYSDATE - 5
);
INSERT INTO MESFLXRPT.HJL_API_APPLOG (
ID, LOG_LEVEL, CATEGORY, MESSAGE, CLASS_NAME,
METHOD_NAME, THREAD_ID, TRACE_ID, IP_ADDRESS, CREATE_TIME
) VALUES (
'TEST_DEL2', 'INFO', 'Test', '也是5天前的数据,应该被删除', 'Test',
'Test', '1', 'TRACE003', '127.0.0.1', SYSDATE - 5
);
COMMIT;
9.2 查看测试数据
sql
SELECT ID, MESSAGE, CREATE_TIME,
CASE WHEN CREATE_TIME < SYSDATE - 1 THEN '会被删除' ELSE '保留' END AS 预期
FROM MESFLXRPT.HJL_API_APPLOG
WHERE ID LIKE 'TEST%'
ORDER BY CREATE_TIME;
9.3 手动执行清理 SQL(模拟 Job 逻辑)
sql
-- 模拟 Job 的清理逻辑(把时间改成 1 天,方便验证)
DELETE FROM MESFLXRPT.HJL_API_APPLOG
WHERE CREATE_TIME < SYSDATE - 1;
COMMIT;
9.4 验证结果
sql
SELECT ID, MESSAGE, CREATE_TIME
FROM MESFLXRPT.HJL_API_APPLOG
WHERE ID LIKE 'TEST%'
ORDER BY CREATE_TIME;
预期结果:
TEST_KEEP(今天)保留 ✅TEST_DEL1、TEST_DEL2(5天前)已删除 ✅
9.5 清理测试数据
sql
DELETE FROM MESFLXRPT.HJL_API_APPLOG WHERE ID LIKE 'TEST%';
COMMIT;
十、第八步:验证日志写入
启动 WebAPI 项目,调用任意接口(如登录接口),然后查询数据库:
sql
SELECT ID, LOG_LEVEL, MESSAGE, METHOD_NAME, TRACE_ID, IP_ADDRESS, CREATE_TIME
FROM MESFLXRPT.HJL_API_APPLOG
ORDER BY CREATE_TIME DESC
FETCH FIRST 5 ROWS ONLY;
预期结果:
METHOD_NAME显示AdminLogin(或你的 Action 名称)TRACE_ID有值(如0HNM4V...)IP_ADDRESS有值(本机调试显示::1)
十一、总结
| 步骤 | 操作 | 关键注意点 |
|---|---|---|
| 1 | 数据库建表 | 表名 HJL_API_APPLOG,字段用 CLOB 存长文本 |
| 2 | 安装 NuGet 包 | 4 个包:NLog、NLog.Web.AspNetCore、NLog.Database、Oracle 驱动 |
| 3 | 配置 appsettings.json | 连接串集中管理,不暴露密码 |
| 4 | 配置 nlog.config | 程序集名没有 .Core 、用 aspnet-mvc-action 取方法名 |
| 5 | 配置 Program.cs | 必须加 AddHttpContextAccessor() |
| 6 | 创建 Oracle Job | 每天凌晨 2 点清理 180 天前数据 |
| 7 | 测试 Job | 造测试数据 → 执行 DELETE → 验证 → 清理 |
| 8 | 验证日志写入 | 调接口后查数据库,确认字段完整 |
十二、后续维护
-
查看 Job 执行历史:
sqlSELECT job_name, status, actual_start_date, run_duration FROM user_scheduler_job_run_details WHERE job_name = 'JOB_CLEAN_HJL_APPLOG' ORDER BY actual_start_date DESC; -
手动触发 Job(排查问题时用):
sqlBEGIN DBMS_SCHEDULER.run_job('JOB_CLEAN_HJL_APPLOG'); END; -
删除 Job(如需停用):
sqlBEGIN DBMS_SCHEDULER.drop_job('JOB_CLEAN_HJL_APPLOG'); END;
以上即为 HJL WebAPI 项目日志入库的完整操作流程。