几乎每个大型应用程序都包含自己的日志记录或跟踪 API。根据这一规则,E.U. SEMPER 🌹项目决定编写自己的跟踪 API。那是在 1996 年初。经过无数次的增强、几个化身和大量的工作,API 已经发展成为 log4j —— 一个流行的 Java 日志包。该软件包是在 Apache 软件许可证下发布的,这是一个由开源 🌹倡议组织认证的成熟的开源许可证。最新的 log4j 版本,包括完整的源代码、类文件和文档,可以在 https://logging.apache.org/log4j/2.x/index.html 🌹上找到。
在代码中插入日志语句是一种低技术含量的调试方法。这也可能是唯一的方法,因为调试器并不总是可用或适用的。这通常是多线程应用程序和分布式应用程序所要面临的情况。
经验表明,日志记录是开发周期的一个重要组成部分。它有几个优点。它提供了关于应用程序运行的精确上下文。一旦插入到代码中,日志输出的生成就不需要人工干预。此外,日志输出可以保存在持久介质中,以供以后研究。除了在开发周期中使用之外,一个功能足够丰富的日志包还可以被视为一个审计工具。
正如 Brian W. Kernighan 和 Rob Pike 在他们真正优秀的书《The Practice of Programming》中所说的那样:
作为个人选择,除了获取堆栈跟踪或一两个变量的值之外,我们倾向于不使用调试器。一个原因是它很容易迷失在复杂的数据结构和控制流的细节中;我们发现,与努力思考并在关键位置添加输出语句和自检代码相比,逐步执行程序的效率更低。点击语句比扫描精心放置的显示的输出要花更长的时间。决定在哪里放置
print
语句比单步到达代码的关键部分花费更少的时间,即便假设我们知道关键部分在哪里。更重要的是,调试语句留在程序中;调试会话是短暂的。
日志记录确实有其缺点。它会降低应用程序的运行速度。如果过于冗长,可能会导致滚动失明[1]。为了减轻这些担忧,log4j 被设计成可靠、快速和可扩展的。由于日志记录很少是应用程序的主要关注点,因此 log4j API 力求易于理解和使用。
Log4j 1.x 已被广泛采用并在许多应用程序中使用。然而,经过多年发展它的速度已经慢下来了。由于需要兼容非常旧的 Java 版本,它变得更加难以维护,并最终在 2015 年 8 月到达了其生命周期尽头 🌹。它的替代方案,SLF4J/Logback 对框架进行了许多必要的改进。
Apache Log4j 2 是 Log4j 的升级版,相对于其前身 Log4j 1.x 提供了重大改进。它提供了 Logback 中可用的许多改进,同时修复了 Logback 架构中的一些固有问题。
4
?
有些同学可能会对名称中出现的数字 “4
” 感到疑惑,但如果你尝试使用英文念出 Log4j 或者 Log4Net 的名称后,相信这个问题就会迎刃而解了。
没错,名称中的 “4
” 代表英文单词 "for",因为 “4
” 的英文单词 "four" 与 "for" 发音类似。所以,"Log4j" 的含义就是 "Log for Java", 意思是这是一个为 Java 提供日志服务的工具。
这是一种常见的命名方式,通过数字 “4
” 来代替 "for", 使得名称更加简洁,也增加了一些趣味性。此外,还有其他的类似命名方式,比如使用 “2
” 来代替 "to"。它们都在许多场合内大量使用着。
Log4Net 可用于多个框架。对于每个支持的框架,都构建了一个针对该框架的程序集:
并不是所有的框架都是平等的,一些特性被排除在一些构建(的程序集)之外。有关更多信息,请参阅框架支持文档。
Log4Net 有三个主要的组件:loggers
, appenders
与 layouts
。这三种类型的组件协同工作使得开发者能够根据消息类型和级别记录日志消息,并在运行时控制这些消息的格式和报告位置。这些组件由控制着 appender 和(将对象转换为字符串的)object renderers
的动作的 filters
辅助。
与普通的 System.Console.WriteLine
相比,任何日志 API 的首要优势在于它能够禁用某些日志语句,同时允许其他日志语句不受阻碍地打印。此功能假设日志空间,即所有可能的日志语句的空间,是根据开发人员选择的一些标准进行分类的。
Loggers 是命名实体。Loggers 名称区分大小写,并遵循以下分层命名规则:
如果一个 logger 名称后跟一个点是后代 logger 名称的前缀,则该 logger 被称为另一个 logger 的祖先。如果在其自身和后代 logger 之间没有祖先,则称其为子 logger 的父 logger。
层次结构的工作方式与 .NET 中的命名空间和类层次结构非常相似。这是非常方便的,我们很快就会看到。
例如:名为 “Foo.Bar” 的 logger 是名为 “Foo.Bar.Baz” 的 logger 的父级。
类似的,“System” 是 “System.Text” 的父级以及 “System.Text.StringBuilder” 的祖先。大多数开发人员应该熟悉这套命名方案。
root
logger 位于 logger 层次结构的顶层。它主要有三点不同:
使用 log4net.LogManager
类的静态方法以检索 loggers。GetLogger
方法将所需 logger 的名称作为参数。如下列所示:
namespace log4net
{
public class LogManager
{
public static ILog GetLogger(string name);
public static ILog GetLogger(Type type);
}
}
接收 Type
参数的 GetLogger
方法使用完全限定类型名作为要检索的 logger 名称。
这些 GetLogger
方法返回一个 ILog
接口。这是传递给开发人员的 Logger
的表现形式。ILog
接口定义如下:
namespace log4net
{
public interface ILog
{
/* Test if a level is enabled for logging */
bool IsDebugEnabled { get; }
bool IsInfoEnabled { get; }
bool IsWarnEnabled { get; }
bool IsErrorEnabled { get; }
bool IsFatalEnabled { get; }
/* Log a message object */
void Debug(object message);
void Info(object message);
void Warn(object message);
void Error(object message);
void Fatal(object message);
/* Log a message object and exception */
void Debug(object message, Exception t);
void Info(object message, Exception t);
void Warn(object message, Exception t);
void Error(object message, Exception t);
void Fatal(object message, Exception t);
/* Log a message string using the System.String.Format syntax */
void DebugFormat(string format, params object[] args);
void InfoFormat(string format, params object[] args);
void WarnFormat(string format, params object[] args);
void ErrorFormat(string format, params object[] args);
void FatalFormat(string format, params object[] args);
/* Log a message string using the System.String.Format syntax */
void DebugFormat(IFormatProvider provider, string format, params object[] args);
void InfoFormat(IFormatProvider provider, string format, params object[] args);
void WarnFormat(IFormatProvider provider, string format, params object[] args);
void ErrorFormat(IFormatProvider provider, string format, params object[] args);
void FatalFormat(IFormatProvider provider, string format, params object[] args);
}
}
loggers 也许 被分配了级别。级别是 log4net.Core.Level
类的实例。以下级别按优先级的先后顺序定义:
如果给定的 logger 没有被分配级别,那么它将从具有指定级别的最接近的祖先处继承一个级别。更正式地:
root
logger。
为了确保所有的 logger 最终都可以继承一个级别,root
logger 总是有一个分配的级别。其默认值是 DEBUG。
下面是四个表,其中包含各种分配的级别值和根据上述规则继承级别结果。
Logger 名称 | 分配的级别 | 继承的级别 |
---|---|---|
root | Proot | Proot |
X | none | Proot |
X.Y | none | Proot |
X.Y.Z | none | Proot |
上例一中,只有 root
logger 被分配了级别。该级别值,即 Proot,由其他 logger X、X.Y、X.Y.Z 继承。
Logger 名称 | 分配的级别 | 继承的级别 |
---|---|---|
root | Proot | Proot |
X | Px | Px |
X.Y | Pxy | Pxy |
X.Y.Z | Pxyz | Pxyz |
上例二中,所有的 logger 都有分配的级别值。所以不需要级别继承
Logger 名称 | 分配的级别 | 继承的级别 |
---|---|---|
root | Proot | Proot |
X | Px | Px |
X.Y | none | Px |
X.Y.Z | Pxyz | Pxyz |
上例三中,root
、X 以及 X.Y.Z logger 分别被分配为级别 Proot、Px 以及 Pxyz。logger X.Y 从其父级 X 处继承其级别值。
Logger 名称 | 分配的级别 | 继承的级别 |
---|---|---|
root | Proot | Proot |
X | Px | Px |
X.Y | none | Px |
X.Y.Z | none | Px |
上例四中,root
以及 X logger 分别被分配为级别 Proot 以及 Px。logger X.Y 以及 X.Y.Z 从最近的具有指定级别的父级 X 处继承其级别值。
日志记录请求是(通过 log4net.ILog
)调用 logger 实例的一个打印方法发出的。这些打印方法是 Debug
、Info
、Warn
、Error
和 Fatal
。
根据定义,打印方法确定日志记录请求的级别。例如:如果 log 是一个 logger 实例,则语句 log.Info("..")
是一个级别为 INFO 的日志请求。
如果日志记录请求的级别高于或等于其 logger 的级别,则说该日志记录请求已启用。否则,请求将被禁用。没有指定级别的 logger 将从层次结构中继承一个级别。这条规则总结如下:
这条规则是 log4net 的核心。它假设级别是有序的。对于标准级别,我们有 DEBUG < INFO < WARN < ERROR < FATAL。
使用相同的名称调用 log4net.LogManager.GetLogger
方法将始终返回对完全相同的 logger 对象的引用。
例如,在
ILog x = LogManager.GetLogger("wombat");
ILog y = LogManager.GetLogger("wombat");
中,x
和 y
引用完全相同的 logger 对象。
因此,这使得配置一个 logger 并随后在不传递引用的前提下在代码中的另外一个地方检索相同的实例成为可能。log4net logger 可以按任何顺序进行创建和配置,这与生物学上的亲子关系(父母总是先于孩子)存在根本矛盾。特别是,“parent” logger 将找到并链接到它的后代,即使它是在它们之后实例化的。
log4net 环境的配置通常在应用程序初始化时完成。首选的方法是读取配置文件。稍后将讨论这种方法。
通过软件组件,log4net 使得 loggers 命名变得非常容易。这可以通过在每个类中静态实例化一个 logger 来实现,logger 的名称等于类的完全限定名称。这是定义 logger 的一种有用且直接的方法。由于日志输出带有生成 logger 的名称,因此这种命名策略可以很容易地识别日志消息的来源。然而,这只是命名 loggers 的一种可能的策略,尽管很常见。log4net 不限制可能的 loggers 集合。开发人员可根据需要自由地命名 loggers。
尽管如此,以 logger 所在的类命名 logger 似乎是迄今为止已知的最佳策略。对于开发人员来说,要明确每个日志消息的来源很简单。最重要的是,它利用应用程序的设计来生成 logger 层次结构的设计。希望应用程序的设计中已经考虑了一些问题。
基于 logger 选择性地启用或禁用日志记录请求的能力只是其中的一部分。Log4Net 允许日志请求打印到多个目的地。在 Log4Net 中,输出目的地被称为 appender。Appenders 必须实现 log4net.Appenders.IAppender
接口。
在 Log4Net 包中定义了以下 appenders:
一个 logger 可以附加多个 appender。
给定 logger 的每个启用的日志记录请求将被转发到该 logger 中的所有 appender 以及层次结构中更高的 appender。 换句话说,appenders 是从 logger 层次结构中累加继承的。例如,如果将控制台 appender 添加到 root
logger,那么所有启用的日志请求将至少在控制台上打印。如果另外一个文件 appender 被添加到 logger X 中,那么对 X 和 X 的子级启用的日志记录请求将在文件和控制台上打印。通过将 logger 上的 additivity
设置为 false
,可以覆盖此默认行为,以便不再添加 appender 累积。
支配 appender 累加的规则总结如下:
logger X 的日志语句的输出将转到 X 及其祖先中的所有 appenders。这就是术语 "appender additivity" 的含义。
但是,如果 logger X 的祖先,比如 Y,将 additivity
标志设置为 false
,则 X 的输出将定向到 X 及其上至 Y(包括 Y)的祖先中的所有 appenders;但不定向到 Y 的任何祖先中的 appenders。
默认情况下,loggers 将其 additivity
标志设置为 true
。
下表展示了一个示例:
Logger 名称 | 添加的 Appenders | Additivity 标志 | 输出目标 | 说明 |
---|---|---|---|---|
root | A1 | 不适用 | A1 | 没有默认的 appender 附加到 root (即 root 默认是没有附加任何 appender 的,这里我们显式地为其配置了一个 A1 appender) |
x | A-x1, A-x2 | true | A1, A-x1, A-x2 | “x” 和 root 的 appenders |
x.y | none | true | A1, A-x1, A-x2 | “x” 和 root 的 appenders |
x.y.z | A-xyz1 | true | A1, A-x1, A-x2, A-xyz1 | “x.y.z”、“x” 和 root 中的 appenders |
security | A-sec | false | A-sec | 因为 additivity 标志设置为 false ,因此没有 appender 累积 |
security.access | none | true | A-sec | 只有 “security” 的 appenders,因为 “security” 中的 additivity 标志设置为了 false |
appenders 可以过滤传递给它们的事件。可以在配置中指定 filters,以便对通过不同 appenders 记录的事件进行精细控制。
最简单的控制形式是在 appender 上指定一个 Threshold(阈值)。其工作原理是只记录级别大于或等于阈值的事件。
更复杂和自定义的事件过滤可以使用在每个 appender 上定义的过滤器链来完成。filters 必须实现 log4net.Filter.IFilter
接口。
在 Log4Net 包中定义了以下 filters:
类型 | 描述 |
---|---|
log4net.Filter.DenyAllFilter | 丢弃所有日志事件。 |
log4net.Filter.LevelMatchFilter | 和事件的级别完全匹配。 |
log4net.Filter.LevelRangeFilter | 对一系列级别进行匹配。 |
log4net.Filter.LoggerMatchFilter | 与记录器名称的开头匹配。 |
log4net.Filter.PropertyFilter | 匹配特定属性值的子字符串。 |
log4net.Filter.StringMatchFilter | 匹配事件消息中的子字符串。 |
filters 可以配置为根据匹配接受或拒绝事件。
通常情况下,用户不仅希望自定义输出目的地,还希望自定义输出格式。这是通过将 layout 与 appender 关联来实现的。layout 负责根据用户的意愿格式化日志记录请求;而 appender 负责将格式化后的输出发送到目的地。PatternLayout 是标准 log4net 分发的一部分,它允许用户根据类似于 C 语言 printf
函数的转换模式指定输出格式。
例如,带有转换模式 “%timestamp [%thread] %-5level %logger - %message%newline” 的 PatternLayout 将输出类似于以下内容:
176 [main] INFO Com.Foo.Bar - Located nearest gas station.
第一个字段是程序开始后经过的毫秒数。第二个字段是发出日志请求的线程。第三个字段是日志语句的级别。第四个字段是与日志请求关联的 logger 的名称。“-”后面的文本是语句的消息。
log4net 包中包含以下 layouts:
类型 | 描述 |
---|---|
log4net.Layout.ExceptionLayout | 呈现来自日志事件的异常文本。 |
log4net.Layout.PatternLayout | 根据一组灵活的格式化标志来格式化日志事件。 |
log4net.Layout.RawTimeStampLayout | 从日志事件中提取时间戳。 |
log4net.Layout.RawUtcTimeStampLayout | 以通用时间(UTC)从日志事件中提取时间戳。 |
log4net.Layout.SimpleLayout | 非常简单地格式化日志事件:[level] - [message] |
log4net.Layout.XmlLayout | 将日志事件格式化为 XML 元素。 |
log4net.Layout.XmlLayoutSchemaLog4j | 将日志事件格式化为符合 log4j 事件文件类型定义的 XML 元素。 |
同样重要的是,log4net 将根据用户指定的标准呈现日志消息的内容。例如,如果您经常需要记录橙子(当前项目中使用的对象类型),那么您可以注册一个 OrangeRenderer,每当需要记录橙子时就会调用它。
对象呈现遵循类层次结构。例如,假设橙子是水果,如果你注册了一个 FruitRenderer,那么包括橙子在内的所有水果都将由 FruitRenderer 渲染,除非你注册了一个特定于橙子的 OrangeRenderer。
object renderers 必须实现 log4net.ObjectRenderer.IObjectRenderer
接口。
请注意,ObjectRenderers 不会被 DebugFormat、InfoFormat、WarnFormat、ErrorFormat 和 FatalFormat 方法使用。
[1]:所谓“滚动失明”,是指当日志信息过于冗长时,程序员查找日志需要滚动查看大量的日志。而相信大部分小伙伴应该体会过,在大量冗长的信息内快速滚动时,我们很难从中找到有用的信息,就像是“失明”了一样。这是由于人的注意力和视觉搜索能力是有限的,当面对大量快速变化的信息时,可能会错过重要的细节。这就是所谓的"滚动失明”。