Today I continue playing with event dispatcher and Silex. Now I want to send a detailed log of our Kernel events to a remote server. We can do it something similar with Monolog, but I want to implement one working example hacking a little bit the event dispatcher. Basically we’re going to create one Logger class (implementing PSR-3 of course)
namespace G; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; class Logger implements LoggerInterface { private $socket; public function __construct($socket) { $this->socket = $socket; } function __destruct() { @fclose($this->socket); } public function emergency($message, array $context = array()) { $this->sendLog($message, $context, LogLevel::EMERGENCY); } public function alert($message, array $context = array()) { $this->sendLog($message, $context, LogLevel::ALERT); } public function critical($message, array $context = array()) { $this->sendLog($message, $context, LogLevel::CRITICAL); } public function error($message, array $context = array()) { $this->sendLog($message, $context, LogLevel::ERROR); } public function warning($message, array $context = array()) { $this->sendLog($message, $context, LogLevel::WARNING); } public function notice($message, array $context = array()) { $this->sendLog($message, $context, LogLevel::NOTICE); } public function info($message, array $context = array()) { $this->sendLog($message, $context, LogLevel::INFO); } public function debug($message, array $context = array()) { $this->sendLog($message, $context, LogLevel::DEBUG); } public function log($level, $message, array $context = array()) { $this->sendLog($message, $context, $level); } private function sendLog($message, array $context = array(), $level = LogLevel::INFO) { $data = serialize([$message, $context, $level]); @fwrite($this->socket, "{$data}\n"); } }
As you can see our Logger class send logs to a remote server, with a socket passed within the constructor.
We also need one Service Provider called LoggerServiceProvider to integrate our Logger instance into our Silex application.
namespace G; use Silex\Application; use Silex\ServiceProviderInterface; class LoggerServiceProvider implements ServiceProviderInterface { private $socket; public function __construct($socket) { $this->socket = $socket; } public function register(Application $app) { $app['remoteLogger'] = $app->share( function () use ($app) { return new Logger($this->socket); } ); } public function boot(Application $app) { } }
And now the last part is our Silex application:
use G\LoggerServiceProvider; use G\Silex\Application; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel; use Symfony\Component\HttpKernel\Event; $app = new Application(); $app->register(new LoggerServiceProvider(stream_socket_client('tcp://localhost:4000'))); $app->on(HttpKernel\KernelEvents::REQUEST, function (Event\GetResponseEvent $event) use ($app) { $app->getLogger()->info($event->getName()); } ); $app->on(HttpKernel\KernelEvents::CONTROLLER, function (Event\FilterControllerEvent $event) use ($app) { $app->getLogger()->info($event->getName()); } ); $app->on(HttpKernel\KernelEvents::TERMINATE, function (Event\PostResponseEvent $event) use ($app) { $app->getLogger()->info($event->getName()); } ); $app->on(HttpKernel\KernelEvents::EXCEPTION, function (Event\GetResponseForExceptionEvent $event) use ($app) { $app->getLogger()->critical($event->getException()->getMessage()); } ); $app->get('/', function () { return 'Hello'; }); $app->run();
As we can see the event dispacher send each event to a remote server (in this example: tcp://localhost:4000). Now we only need a tcp server to handle those sockets. We can use different tools and libraries to do that. In this example we’re going to use React.
use React\EventLoop\Factory; use React\Socket\Server; $loop = Factory::create(); $socket = new Server($loop); $socket->on('connection', function (\React\Socket\Connection $conn){ $unique = uniqid(); $conn->on('data', function ($data) use ($unique) { list($message, $context, $level) = \unserialize($data); echo date("d/m/Y H:i:s")."::{$level}::{$unique}::{$message}" . PHP_EOL; }); }); echo "Socket server listening on port 4000." .PHP_EOL; echo "You can connect to it by running: telnet localhost 4000" . PHP_EOL; $socket->listen(4000); $loop->run();
Now we only need to start our servers:
our silex one
php -S 0.0.0.0:8080 -t www
and the tcp server
php app/server.php
One screencast showing the prototype in action:
You can see the full code in my github account.
“class Logger implements LoggerInterface”
Extend abstract logger for much cleaner solution…
I try to avoid to extend abstract classes as a plague. I’ve used a lot of abstract classes in the past, but nowadays I realized that they normally turn into a hodgepodge, violating the single responsibility principle. As a general rule I prefer to work with interfaces instead of abstract classes (and simple inheritance). In fact I prefer to work with traits than extending abstract classes.