日志记录
日志系统用于把运行时发生的事件,按统一格式输出到控制台、文件或其他目标中。
在 soulsoft_extensions_logging 中,日志能力有 4 个最重要的特性:
- 用
LoggingBuilder统一配置日志系统 - 用
ILoggerFactory创建按分类命名的日志器 - 支持
Trace到Fatal的分级日志 - 支持全局级别、分类规则、提供者规则和自定义过滤函数
最小示例
如果你只是想“先把日志打出来”,可以从这个例子开始:
import soulsoft_extensions_logging.*
main(): Int64 {
// 创建日志工厂,注册控制台提供者并设置最低级别。
let factory = LoggingBuilder()
.addConsole()
.setMinimumLevel(LogLevel.Info)
.build()
// 按分类名创建 logger。
let logger = factory.createLogger("app.hosting.lifetime")
logger.debug("这条日志不会输出")
logger.info("应用已启动")
logger.warn("缓存命中率偏低")
logger.error("数据库连接失败")
// 使用完成后关闭日志工厂。
factory.close()
return 0
}这个例子里有三个关键点:
addConsole()注册控制台日志提供者setMinimumLevel(LogLevel.Info)指定最低输出级别createLogger("app.hosting.lifetime")创建一个带分类名的日志器
核心类型
| 类型 | 作用 |
|---|---|
LoggingBuilder | 配置日志系统,注册提供者和过滤规则 |
ILoggerFactory | 创建或缓存 ILogger |
ILogger | 记录日志 |
ILoggerProvider | 定义日志输出目标,例如控制台、文件 |
LogLevel | 表示日志级别 |
最常见的使用流程只有 4 步:
- 创建
LoggingBuilder - 注册提供者和过滤规则
- 调用
build()得到ILoggerFactory - 通过
ILoggerFactory创建ILogger
日志级别
日志级别定义了日志的重要程度。
public enum LogLevel <: Comparable<LogLevel> & ToString {
Trace | Debug | Info | Warn | Error | Fatal | Off
}| 级别 | 使用场景 |
|---|---|
Trace | 非常细的跟踪信息,例如函数进入/退出 |
Debug | 开发调试信息,例如中间变量、关键分支 |
Info | 正常运行信息,例如服务启动、任务完成 |
Warn | 非致命问题,例如重试、降级、潜在风险 |
Error | 当前操作失败,但进程未必终止 |
Fatal | 严重故障,通常意味着系统无法继续工作 |
Off | 完全关闭日志输出 |
级别从低到高依次递增,因此:
setMinimumLevel(LogLevel.Info)会放行Info、Warn、Error、FatalsetMinimumLevel(LogLevel.Error)只会放行Error和Fatal
记录日志
按分类创建日志器
分类名通常用来表示日志属于哪个模块。
// 创建日志工厂。
let factory = LoggingBuilder()
.addConsole()
.build()
// 为不同分类创建不同 logger。
let appLogger = factory.createLogger("app")
let dbLogger = factory.createLogger("app.database")
appLogger.info("应用启动")
dbLogger.warn("数据库连接池接近上限")分类名本身不会自动产生层级对象,它只是后续过滤规则匹配时使用的字符串。
在命名上,建议统一使用全小写的点分层级形式,例如:
appapp.serviceapp.database.connection
常用日志方法
ILogger 提供了一组按级别命名的方法:
// 按级别分别输出日志。
logger.trace("trace")
logger.debug("debug")
logger.info("info")
logger.warn("warn")
logger.error("error")
logger.fatal("fatal")如果你需要同时记录异常,也可以使用异常重载:
try {
throw Exception("连接超时")
} catch (ex: Exception) {
// 记录异常对象和日志消息。
logger.error(ex, "请求处理失败")
}控制台提供者
最常用的内置提供者是控制台提供者:
// 注册控制台日志提供者并构建工厂。
let factory = LoggingBuilder()
.addConsole()
.build()它有两个值得知道的行为:
- 提供者名称是
console,后面写过滤规则时会用到 Error和Fatal会写入标准错误流,其他级别写入标准输出流
如果你没有注册任何提供者,即使创建了 logger,也不会真正产生输出。
日志过滤
日志过滤用于决定“哪些日志应该被放行”。
设置全局最低级别
最简单的过滤方式是设置全局最低级别:
let factory = LoggingBuilder()
.addConsole()
// 全局最低级别设置为 Warn。
.setMinimumLevel(LogLevel.Warn)
.build()
let logger = factory.createLogger("app")
logger.info("不会输出")
logger.warn("会输出")
logger.error("会输出")按分类和提供者添加规则
如果你希望不同模块用不同级别,可以用 addFilter(providerName, categoryName, logLevel):
let factory = LoggingBuilder()
.addConsole()
.setMinimumLevel(LogLevel.Info)
// 对 console 提供者下的 app.hosting.* 分类单独提高门槛。
.addFilter("console", "app.hosting.*", LogLevel.Warn)
.build()
let logger1 = factory.createLogger("app.hosting.lifetime")
logger1.info("不会输出")
logger1.error("会输出")
let logger2 = factory.createLogger("app.web.router")
logger2.info("会输出")这里三个参数分别表示:
providerName:提供者名称,例如consolecategoryName:分类名匹配规则logLevel:该规则对应的最低级别
分类规则支持单个 *
分类规则支持一个 * 通配符,用来表示前缀/后缀匹配。
// 匹配 app 下的全部子分类。
.addFilter(None, "app.*", LogLevel.Warn)
// 匹配以 .controller 结尾的分类。
.addFilter(None, "*.controller", LogLevel.Error)
// 匹配 app 下某一层级中的 controller 分类。
.addFilter(None, "app.*.controller", LogLevel.Error)需要注意两点:
- 匹配时不区分大小写
- 一个规则里最多只能出现一个
*
使用自定义过滤函数
如果内置规则不够用,可以直接传入函数:
let factory = LoggingBuilder()
.addConsole()
// 用自定义函数决定哪些日志允许输出。
.addFilter { providerName, categoryName, logLevel =>
providerName == "console"
&& categoryName.startsWith("app.admin")
&& logLevel >= LogLevel.Error
}
.build()这个函数签名是:
public type LoggerFilter =
(providerName: String, categoryName: String, logLevel: LogLevel) -> Bool返回 true 表示允许输出,返回 false 表示过滤掉。
规则优先级
当多条规则同时命中时,可以按下面的顺序理解:
- 提供者特定规则优先于全局规则
- 分类更具体的规则优先于更宽泛的规则
- 如果没有命中显式规则,则回退到
setMinimumLevel()设置的全局最低级别
例如:
let factory = LoggingBuilder()
.addConsole()
.setMinimumLevel(LogLevel.Trace)
// 给 app.* 设置 Info 级别门槛。
.addFilter(None, "app.*", LogLevel.Info)
// 给更具体的 controllers 分类设置更高门槛。
.addFilter(None, "app.controllers.*", LogLevel.Error)
.build()那么:
app.services.user-service会使用Infoapp.controllers.home-controller会使用更具体的Error
自定义输出
一个日志系统可以同时注册多个提供者,同一条日志会分发到所有提供者。 如果控制台输出不够用,可以自己实现 ILoggerProvider 和 ILogger。
先看一个最简单的文件日志例子,日志按天写入 ./logs/yyyyMMdd.log:
import std.fs.*
import std.io.*
import std.time.*
import soulsoft_extensions_logging.*
class FileLogger <: ILogger {
// 记录当前 logger 对应的分类名。
public FileLogger(let categoryName: String) {}
public func log(logLevel: LogLevel, message: ?String, exception: ?Exception): Unit {
// 先检查当前级别是否启用。
if (!isEnabled(logLevel)) {
return
}
// 计算当天日志文件路径并追加写入。
let path = getFileName()
try (sw = StringWriter(getFileStream(path))) {
sw.write("${DateTime.now()}|${logLevel}|${categoryName}: ")
if (let Some(ex) <- exception) {
// 异常存在时优先写异常文本。
sw.writeln(ex.toString())
} else {
// 否则写普通日志消息。
sw.writeln(message ?? "")
}
}
}
private func getFileName() {
// 目录不存在时先创建 logs 目录。
if (!exists("logs")) {
Directory.create("logs", recursive: true)
}
let name = DateTime.now().format("yyyyMMdd")
return Path("./logs/${name}.log")
}
private func getFileStream(path: Path) {
// 文件不存在时新建,存在时追加。
if (!exists(path)) {
return File(path, OpenMode.Write)
}
return File(path, OpenMode.Append)
}
}然后补一个提供者:
class FileLoggerProvider <: ILoggerProvider {
public prop name: String {
get() {
// 声明提供者名称,供过滤规则匹配。
"file"
}
}
public func createLogger(categoryName: String): ILogger {
// 为每个分类创建对应的文件 logger。
return FileLogger(categoryName)
}
}再扩展 LoggingBuilder:
extend LoggingBuilder {
public func addFile() {
// 把自定义文件提供者加入日志系统。
addProvider(FileLoggerProvider())
return this
}
}使用时可以同时输出到控制台和文件:
let factory = LoggingBuilder()
.addConsole()
.addFile()
.build()
let logger = factory.createLogger("app")
// 同一条日志会同时分发给两个提供者。
logger.error("这条日志会同时输出到控制台和文件")如果你只想让文件日志记录更高等级的内容,可以再加一条规则:
let factory = LoggingBuilder()
.addConsole()
.addFile()
// 只让 file 提供者记录 Error 及以上级别。
.addFilter("file", None, LogLevel.Error)
.build()与依赖注入集成
如果你的应用已经使用 soulsoft_extensions_injection,通常做法是把 ILoggerFactory 注册到容器里,再在服务构造函数中注入它。
典型写法如下:
import soulsoft_extensions_injection.*
import soulsoft_extensions_logging.*
class AppService {
private let _logger: ILogger
public AppService(loggerFactory: ILoggerFactory) {
// 在服务构造函数中创建自己的分类 logger。
_logger = loggerFactory.createLogger("app.service")
}
public func run(): Unit {
_logger.info("service running")
}
}
main(): Int64 {
// 构建日志工厂。
let loggerFactory = LoggingBuilder()
.addConsole()
.setMinimumLevel(LogLevel.Info)
.build()
let services = ServiceCollection()
// 把 ILoggerFactory 注册进容器,并注册业务服务。
services.addSingleton<ILoggerFactory>(loggerFactory)
services.addSingleton<AppService, AppService>()
// 解析服务并执行业务逻辑。
let root = services.build()
let app = root.getOrThrow<AppService>()
app.run()
// 释放容器持有的资源。
root.close()
return 0
}这里有两个实践点:
- 容器里注册的是
ILoggerFactory,不是某个固定分类名的ILogger - 业务服务里自行创建 logger,并显式使用小写分类名,例如
app.service
如果你使用的是 HostBuilder 或 WebHostBuilder,ILoggerFactory 会在构建时自动注册到容器中,这时你的服务可以直接依赖注入 ILoggerFactory,不需要手动再注册一次。
与配置系统集成
上面这些例子都只依赖 soulsoft_extensions_logging。 如果你希望把日志规则放到 JSON、内存字典或配置文件中统一管理,再引入扩展包 soulsoft_extensions_logging_configuration。
要点只有两个:
- 核心日志能力来自
soulsoft_extensions_logging - 读取配置规则时,再额外引入
soulsoft_extensions_logging_configuration和soulsoft_extensions_configuration
典型写法如下:
import soulsoft_extensions_logging.*
import soulsoft_extensions_configuration.*
import soulsoft_extensions_logging_configuration.*
main(): Int64 {
// 构建一份包含 logging 节的配置对象。
let configuration = ConfigurationManager()
.addJsonString(###"
{
"logging": {
"logLevel": {
"default": "Info",
"app.hosting.*": "Warn"
},
"console": {
"logLevel": {
"default": "Error"
}
}
}
}
"###)
.build()
// 取出 logging 配置节。
let loggingSection = configuration.getSection("logging")
// 根据配置节构建日志工厂。
let factory = LoggingBuilder()
.addConsole()
.addConfiguration(loggingSection)
.build()
let logger1 = factory.createLogger("app.hosting.lifetime")
logger1.info("不会输出")
logger1.error("会输出")
let logger2 = factory.createLogger("app.web.router")
logger2.info("不会输出")
logger2.error("会输出")
// 使用完成后关闭工厂。
factory.close()
return 0
}这个例子里,logLevel.default = Info 是全局默认级别,console.logLevel.default = Error 是 console 提供者自己的规则,优先级更高。
配置结构保持下面这种形式即可:
当你把某个配置节传给 addConfiguration() 后,它会识别两类结构:
{
"logLevel": {
"default": "Info",
"app.service": "Warn"
},
"console": {
"logLevel": {
"default": "Error"
}
}
}其中 logLevel:<分类名> 表示全局规则,<提供者名>:logLevel:<分类名> 表示提供者特定规则。 default 表示默认规则。日志级别必须是 Trace、Debug、Info、Warn、Error、Fatal、Off,并且大小写敏感。
使用建议
- 先从
addConsole()+setMinimumLevel()开始,不要一开始就把规则设计得太复杂 - 分类名建议按模块命名,并统一使用小写,例如
app.web.router、app.data.sql - 扩展包应当后置引入:先把核心日志能力跑通,再接入配置系统
- 自定义文件提供者时,先采用简单目录,例如
./logs/yyyyMMdd.log,后续再演进到更复杂的落盘策略