通过审计服务 LinAudit,可以记录运行日志、操作审计和带问题代码的异常报告:
LinAudit audit = lin.linAudit();
LinAudit 有三个成员,承担不同职责:
logger() 日志服务,记录普通日志信息。record() 审计服务,记录谁在何时对什么对象执行了什么操作。problem() 记录稳定的问题代码,您可以通过代码查询 Linlang 遇到的异常问题。通过 logger() 取得 LinLogger:
LinLogger logger = lin.linAudit().logger();
logger.info("Loaded {} entries", count);
logger.warn(
"Failed to load {file}",
exception,
"file", file
);
普通日志支持 debug、info、warn 和 error 四个等级。warn 与 error 可以直接接收 Throwable,运行时会保留异常类型、原因链和堆栈。
{} 会按顺序使用参数,{name} 会使用后续的名称和值进行替换。没有出现在模板中的键值对会作为结构化字段保留:
logger.info(
"Player {player} saved item",
"player", player.getName(),
"slot", slot
);
init、startup 和 op 是特殊日志通道。startup 可以在服务器启动阶段暂存消息,op 会发送给在线管理员,没有合适接收者时则进入待发送队列。
简单事件可以直接传入事件名称和字段:
lin.linAudit().record(
"config.reload",
"actor", sender.getName(),
"file", "config.yml"
);
需要指定操作者、操作对象和结果等信息时,可以创建 AuditEvent:
AuditEvent event = AuditEvent.builder("item.rename")
.actor(player.getUniqueId().toString())
.action("rename")
.resource(itemId)
.outcome(AuditOutcome.SUCCESS)
.correlationId(requestId)
.field("name", nextName)
.build();
lin.linAudit().record(event);
事件名称应使用稳定、可检索的机器标识,不应放入会随语言变化的展示文本。actor、action、resource、outcome 和 correlationId 分别记录操作者、动作、操作对象、结果和关联 ID;其他信息放入 field。
当错误需要被记录并继续运行时,可直接报告代码、原始异常和定位上下文:
lin.linAudit().problem().report(
"PLUGIN-FILE-LOAD-FAIL",
exception,
"file", file,
"operation", "reload"
);
问题记录只包含稳定代码、上下文和 Throwable。它不会读取 LangText 或语言对象,也不会在建立记录时查询文件,因此即使语言服务没有完成加载,原始错误仍然可以被保留下来。
需要向调用方传播失败时,应继续抛出标准 Java 异常,并把问题代码放在异常消息中:
throw new IllegalStateException("PLUGIN-FILE-LOAD-FAIL", exception);
同一个错误通常只需记录一次。底层方法抛出异常,由上层启动器、命令执行器或任务入口决定是否恢复,并调用 problem().report(...) 记录,避免每层都打印同一异常。
如果需要逐步收集上下文,可以使用 LinProblem 构建器:
LinProblem problem = LinProblem.builder("PLUGIN-FILE-LOAD-FAIL")
.context("file", file)
.context("operation", "reload")
.cause(exception)
.build();
lin.linAudit().problem().report(problem);
构建器不是异常类型,也不代替 Java 异常。它只用于组织需要写入问题日志的诊断信息。
如果 Linlang 仍然能够正常启动,或 LinlangRuntimeBukkit 可用,则 Linlang 内建问题代码可以通过同一个入口查询:
Optional<ProblemDefinition> result =
lin.linAudit().problem().lookup("LIN-AUDIT-FILE-WRITE-FAIL");
result.ifPresent(definition -> {
logger.info("Component: {}", definition.component());
logger.info("Description: {}", definition.description());
logger.info("Resolution: {}", definition.resolution());
});
问题代码、组件名称和默认说明直接编译在 Runtime 中,不依赖配置文件、语言文件或数据库。语言服务就绪后,问题含义和处理建议会从运行时的问题目录语言包读取,并跟随语言切换与软重载更新;语言包不可用时自动回退到内建说明。只要审计 Provider 已经安装,即使其他服务初始化失败,仍可以查询代码。若 Linlang 在 Provider 安装前就无法启动,异常和控制台输出中的代码仍可用于查阅本文档。
使用 list() 可以取得按代码名称排序的全部内建定义:
List<ProblemDefinition> definitions =
lin.linAudit().problem().list();
在安装了 Bukkit Runtime 的服务器中,也可以直接使用内建命令:
/linlang problems
/linlang problem LIN-AUDIT-FILE-WRITE-FAIL
第一条命令列出全部内建代码及简要含义,第二条命令显示指定代码的所属组件、完整说明、处理建议和文档位置。两条命令都需要 linlangruntimebukkit.admin 权限。
lookup() 和 list() 当前只查询 Linlang 内建代码。插件可以定义自己的代码,例如 RAINBOW-FILE-LOAD-FAIL,但应在插件文档中维护对应说明,不应假定它会自动进入 Linlang 的内建目录。
问题代码由 LIN、组件名和故障描述组成,例如 LIN-FILE-CONFIG-SAVE-FAIL。要查询错误代码列表,请见 异常代码 ⇱
每个插件拥有独立的审计配置和输出目录。配置中的相对路径以当前插件的数据目录为根目录,绝对路径和包含 .. 的越界路径会被拒绝。
level: INFO
json: true
console:
enabled: true
json: false
file:
path: linlang/audit/log.log
enabled: false
json: true
size-mb: 10
retained: 5
audit:
path: linlang/audit/audit.log
enabled: false
json: true
size-mb: 10
retained: 5
problem:
path: linlang/audit/problem.log
enabled: false
json: true
size-mb: 10
retained: 5
queue-capacity: 4096
file、audit 和 problem 分别控制普通日志、行为审计和问题记录,互不共用开关。文件写入通过有界队列异步完成;队列满时会使用同步写入保护当前记录。达到 size-mb 后执行轮转,并最多保留 retained 个历史文件。
上下文字段中的密码、令牌和密钥等敏感名称会在输出前遮蔽。仍不应把完整凭据或无法安全转换的大型业务对象交给日志服务。
插件关闭或必须确保记录已经落盘时调用:
lin.linAudit().flush();
正常的 Runtime 关闭流程会自动刷新文件队列。
flowchart LR
Facade["linAudit"] --> Logger["LinLogger"]
Facade --> Event["AuditEvent"]
Facade --> Problem["LinProblem"]
Logger --> Provider["Audit Provider"]
Event --> Provider
Problem --> Provider
Provider --> Tenant["按插件选择配置与输出"]
Tenant --> Console["控制台或特殊通道"]
Tenant --> Queue["异步文件队列"]
Problem --> Catalog["内建问题代码目录"]
讨论