swf/slf4php

PHP的SWF日志门面 - 受slf4j / logback / log4j Java项目启发

1.0.0 2016-02-23 06:28 UTC

This package is not auto-updated.

Last update: 2024-09-13 16:16:02 UTC


README

CI master: Codeship Status for ironhawk/swf-slf4php

这个项目是什么?

slf4php 类似于 slf4j(Java的简单日志门面)项目。

slf4php 不是一个日志实现!这是一个在现有日志框架(如 Monolog)之上的 抽象层,就像 slf4j 一样。

如果你没有添加(配置)任何日志实现,没问题!slf4php 默认使用 "无操作" 机制。在代码中使用门面时,没有任何变化。你可以在以后随时配置日志!

支持的日志实现

这个1.0版本通过提供 MonologProxyAppender 类,默认支持 Monolog 日志实现。

未来版本可能会支持更多。

概念 - 简而言之

我们有以下关键实体

  • 追加器(Appenders)
    您可以将 Appender 视为一个输出通道。将某些内容以某种方式写入某处...(文件、数据库、电子邮件等)
    这就是现有日志框架发挥作用的地方!本项目没有自己的 Appender 实现。相反,它提供了一些 "连接器"(通常是代理/适配器设计模式),让您可以使用您喜欢的日志框架。

  • 记录器(Loggers)
    在您的代码中,您需要一个 Logger 实例来记录消息。您可以在其上调用适当的日志方法(info()、debug()、warning() 等) - 如 PSR/log 中的 LoggerInterface 定义。
    记录器有 日志级别。并且...它们后面有 一个或多个追加器!当您使用记录器实例记录消息时,该消息将路由到记录器后面的所有追加器。因此,您的日志消息将被写入文件、数据库或通过电子邮件发送。或者全部!

  • 日志工厂(LoggerFactory)
    我们有一个 LoggerFactory。您通过提供要记录的类的完全限定名称从工厂请求 Logger 实例。您将得到一个 Logger 实例 - 匹配您事先提供给 LoggerFactory 的日志配置。

还有:配置

我们希望能够轻松地配置所有这些!为了实现这一点,最好的方式是拥有一个简单的(尽可能简单)配置文件

快速看一下以下JSON配置,您将立即理解其概念

{
	"appenders" : [
		{
			"name" : "logFile",
			"builderClass" : "swf\\slf4php\\config\\builder\\monolog\\MonologProxyAppenderBuilder",
			"handlers" : [
				{
					"builderClass" : "swf\\slf4php\\config\\builder\\monolog\\MonologStreamHandlerBuilder",
					"stream" : "${LOG_DIR}/application.log",
					"formatter": {
						"builderClass" : "swf\\slf4php\\config\\builder\\monolog\\MonologLineFormatterBuilder",
						"format" : "[%datetime%] %extra.loggerName%.%level_name%: %message% %context% %extra%\n\n"
					}
				}
			]
		},
		{
			"name" : "console",
			"builderClass" : "swf\\slf4php\\config\\builder\\monolog\\MonologProxyAppenderBuilder",
			"handlers" : [
				{
					"builderClass" : "swf\\slf4php\\config\\builder\\monolog\\MonologStreamHandlerBuilder",
					"stream" : "php://stdout",
					"formatter": {
						"builderClass" : "swf\\slf4php\\config\\builder\\monolog\\MonologLineFormatterBuilder",
						"format" : "[%datetime%] %extra.loggerName%.%level_name%: %message% %context% %extra%\n\n"
					}
				}
			]
		}
	],

	"loggers" : [
		{
			"name" : "your.namespace.A",
			"level" : "DEBUG",
			"appenders" : ["logFile", "console"]
		},
		{
			"name" : "your.namespace.B",
			"level" : "INFO",
			"appenders" : "logFile"
		},
		{
			"level" : "WARNING",
			"appenders" : "logFile"
		}
	]
}

使用上述简单配置,我们有

  • 创建了两个追加器:一个日志文件和一个控制台。使用 MonologStreamHandler
  • 为 "your.namespace.A" 创建了一个 DEBUG 级别的记录器,它使用日志文件追加器和控制台追加器
  • 为 "your.namespace.B" 创建了一个 INFO 级别的记录器,它仅使用日志文件追加器
  • 并且还使用日志文件追加器创建了一个警告级别的记录器。AND... 你可以注意到这个没有命名空间定义... 这意味着这是一个默认的记录器设置

访问我们的维基百科!有关配置日志的更多信息,请访问维基百科页面

基本用法

假设你已经将上述配置保存在名为 log.config.json 的文件中,你可以这样配置LoggerFactory

<?php
use swf\slf4php\LoggerFactory;
use swf\slf4php\config\builder\LogConfigBuilder;
use swf\util\JsonUtil;

....

// you might use variables in the json file (unix like style) and we can pass an array to the json parsing mechanism
// to let it resolve the vars
$envVars = [
	'LOG_DIR' => '/var/log'
];
$loggerConfig = LogConfigBuilder::create()->initFromJson(JsonUtil::getJsonObjects("log.config.json"), $envVars)->build();
// and now we can init the factory
LoggerFactory::init($loggerConfig);

一旦配置了LoggerFactory,你就可以这样从工厂中获取Logger实例

$myLogger = LoggerFactory::getLogger(YourClass::class);   // the parameter is the fully qualified class name of YourClass
$myLogger->info("This is a log message", []);

正如你可能已经猜到的,当你从工厂中获取记录器时,你会得到匹配的记录器!

在这种情况下

$logger = LoggerFactory::getLogger('\your\namespace\A\MyClass');

你会得到一个DEBUG级别的记录器实例,它后面有logFile和控制台追加器。

而在这个情况下

$logger = LoggerFactory::getLogger('JustAClass');

将是一个默认的记录器,匹配所以你会得到一个WARNING级别的记录器实例,后面跟着logFile追加器。

在日志消息中输出变量

我最喜欢的slf4j Java框架中的想法之一。

想象一下我们有一些复杂对象。在DEBUG级别,我们通常想要将这些对象输出到日志流中。但是当我们把日志级别改为更严格的级别时,如果我们仍然将这些对象输出到字符串(日志消息)中,然后因为日志级别而不扔掉它们... 好吧,这是一种浪费CPU功率和额外工作给垃圾收集器... 所以我们不应该这样做!

这里是一个不好的例子

// this might be a huge object:
$jsonObj = json_decode($jsonString);
...
// and this is a bad idea...:
$myLogger->debug("Parsed json object is: " . print_r($jsonObj, true), []);

为什么这样做是个坏主意?因为无论如何都会构造一个String参数 - 使用内存和CPU功率。如果日志级别不是DEBUG,那么就毫无意义了...

slf4j为这个问题提供了一个优雅的解决方案 - 它使用Java中的"varargs"。这意味着一个方法可以获取任何数量的额外参数。实际上PHP也支持这个,所以我们也可以在这里使用它!

上面的例子中,这是推荐的做法

// this might be a huge object:
$jsonObj = json_decode($jsonString);
...
// do it this way instead:
$myLogger->debug("Parsed json object is: {}", [], $jsonObj);

你可以在日志消息中添加{}来定义占位符。然后你只需简单地将变量传递给日志方法调用。

你可以使用你想要的任何数量的占位符。传递给方法变量的数量应该与消息中使用的占位符数量相匹配!(当然,如果你传递的变量比占位符多或少,那不是什么大问题,但你的日志消息可能会看起来“奇怪”)

重要!你不应该在传递的对象上做任何转换!你应该以原样传递变量,并让slf4php在需要时进行字符串转换!

不要这样做

$jsonObj = json_decode($jsonString);
...
// you shouldn't do string transformations like this
$myLogger->debug("Parsed json object is: {}", [], print_r($jsonObj, true));

如果你不仅传递了简单类型(如布尔值、整数、字符串等),还传递了对象和/或数组,slf4php会以这种方式(在需要时)将它们转换为字符串

  • NULL变量将作为字符串“NULL”
  • 如果变量是数组,则将使用print_r()方法将其转换为字符串
  • 如果变量是对象
    • 如果它有一个__toString()方法,则将使用该方法
    • 如果没有,则使用print_r()方法将对象转换为字符串

这里还有一个例子

在类定义中推荐的使用方法

当你调用LoggerFactory::getLogger(<fully qualified class name>)时,在幕后发生的事情是LoggerFactory正在遍历配置的记录器,并尝试找到最佳匹配的记录器。如果你调用这个方法100次,它就会执行100次!所以你不应该这样做...

在Java中,典型的用法是以下代码(你可能可以读懂)

public class MyClass {
	// get the Logger instance only once / class
	private static final Logger LOG = LoggerFactory.getLogger(MyClass.class);
	
	...
	
	public void aMethod() {
		LOG.info("a message...");
	}
}

很遗憾,我们无法在PHP中这样做,因为

  • PHP不支持这种静态类变量的初始化方式.. :-(
  • 而且,除此之外,在PHP中静态变量可能会在继承时让你感到困惑...

目前找到的最佳替代方案(到目前为止)如下
在你的类定义中

  1. 你声明一个受保护的静态变量 - 用于持有Logger实例的引用
  2. 你创建一个受保护的静态方法,例如命名为logger(),它从LoggerFactory获取并存储Logger实例 - 如果之前没有这样做。但请注意!你需要使用static::关键字来引用受保护的静态变量,而不是self::关键字!你可能需要PHP的“后期静态绑定”特性...
  3. 当你想要记录一些内容时,通过调用(你第2步定义的)静态方法来获取Logger实例。当你扩展这个类时
  4. 确保在子类中重新定义受保护的静态变量 - 使用相同的名称。就像你在第1步中所做的那样。

以下代码示例展示了这一点

ClassA的定义

<?php

namespace your\namespace;

use swf\slf4php\LoggerFactory;

class ClassA {

	protected static $_LOG;

	/**
	 * @return \swf\slf4php\Logger
	 */
	protected static function logger() {
		if (is_null(static::$_LOG))
			static::$_LOG = LoggerFactory::getLogger(static::class);
		return static::$_LOG;
	}

	public function logSomething() {
		self::logger()->info("a simple message...", []);
	}
}

ClassA的子类定义

<?php

// this can be on different namespace of course.. but this doesn't matter
namespace different\namespace;

use swf\slf4php\LoggerFactory;
use your\namespace\ClassA;

class ClassASubclass extends ClassA {

	// we need to override this! if we would not do this then we would inherit OR hijack the Logger instance
	// of our superclass...
	protected static $_LOG;

	public function anotherLog() {
		self::logger()->info("another message...", []);
	}
}

说明
这将按预期工作!ClassASubclass继承了名为logger()的受保护的静态方法。但由于我们正在重写protected static $_LOG字段,并且继承的logger()方法使用了“后期静态绑定”,因此$_LOG变量将在第一次在ClassASubclass中使用logger()方法时初始化。

访问我们的Wiki页面

有关更多信息/高级主题,请访问Wiki页面