Skip to content

Commit

Permalink
Merge pull request #222 from clue-labs/logstream
Browse files Browse the repository at this point in the history
Refactor logging into new `LogStreamHandler`
  • Loading branch information
SimonFrings authored Apr 11, 2023
2 parents cdef98b + 5bf9c88 commit 4402433
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 91 deletions.
12 changes: 7 additions & 5 deletions src/AccessLogHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace FrameworkX;

use FrameworkX\Io\SapiHandler;
use FrameworkX\Io\LogStreamHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
Expand All @@ -14,15 +14,17 @@
*/
class AccessLogHandler
{
/** @var SapiHandler */
private $sapi;
/** @var LogStreamHandler */
private $logger;

/** @var bool */
private $hasHighResolution;

/** @throws void */
public function __construct()
{
$this->sapi = new SapiHandler();
/** @throws void because `fopen()` is known to always return a `resource` for built-in wrappers */
$this->logger = new LogStreamHandler(\PHP_SAPI === 'cli' ? 'php://output' : 'php://stderr');
$this->hasHighResolution = \function_exists('hrtime'); // PHP 7.3+
}

Expand Down Expand Up @@ -85,7 +87,7 @@ private function log(ServerRequestInterface $request, ResponseInterface $respons
$responseSize = 0;
}

$this->sapi->log(
$this->logger->log(
($request->getAttribute('remote_addr') ?? $request->getServerParams()['REMOTE_ADDR'] ?? '-') . ' ' .
'"' . $this->escape($method) . ' ' . $this->escape($request->getRequestTarget()) . ' HTTP/' . $request->getProtocolVersion() . '" ' .
$status . ' ' . $responseSize . ' ' . sprintf('%.3F', $time < 0 ? 0 : $time)
Expand Down
32 changes: 21 additions & 11 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace FrameworkX;

use FrameworkX\Io\FiberHandler;
use FrameworkX\Io\LogStreamHandler;
use FrameworkX\Io\MiddlewareHandler;
use FrameworkX\Io\RedirectHandler;
use FrameworkX\Io\RouteHandler;
Expand All @@ -24,9 +25,12 @@ class App
/** @var RouteHandler */
private $router;

/** @var SapiHandler */
/** @var ?SapiHandler */
private $sapi;

/** @var ?LogStreamHandler */
private $logger;

/** @var Container */
private $container;

Expand Down Expand Up @@ -115,7 +119,8 @@ public function __construct(...$middleware)
$this->router = new RouteHandler($this->container);
$handlers[] = $this->router;
$this->handler = new MiddlewareHandler($handlers);
$this->sapi = new SapiHandler();
$this->sapi = (\PHP_SAPI !== 'cli' ? new SapiHandler() : null);
$this->logger = (\PHP_SAPI === 'cli' ? new LogStreamHandler('php://output') : null);
}

/**
Expand Down Expand Up @@ -231,6 +236,9 @@ public function run(): void

private function runLoop(): void
{
$logger = $this->logger;
assert($logger instanceof LogStreamHandler);

$http = new HttpServer(function (ServerRequestInterface $request) {
return $this->handleRequest($request);
});
Expand All @@ -240,31 +248,31 @@ private function runLoop(): void
$socket = new SocketServer($listen);
$http->listen($socket);

$this->sapi->log('Listening on ' . \str_replace('tcp:', 'http:', (string) $socket->getAddress()));
$logger->log('Listening on ' . \str_replace('tcp:', 'http:', (string) $socket->getAddress()));

$http->on('error', function (\Exception $e): void {
$this->sapi->log('HTTP error: ' . $e->getMessage());
$http->on('error', static function (\Exception $e) use ($logger): void {
$logger->log('HTTP error: ' . $e->getMessage());
});

// @codeCoverageIgnoreStart
try {
Loop::addSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 = function () use ($socket) {
Loop::addSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 = static function () use ($socket, $logger) {
if (\PHP_VERSION_ID >= 70200 && \stream_isatty(\STDIN)) {
echo "\r";
}
$this->sapi->log('Received SIGINT, stopping loop');
$logger->log('Received SIGINT, stopping loop');

$socket->close();
Loop::stop();
});
Loop::addSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 = function () use ($socket) {
$this->sapi->log('Received SIGTERM, stopping loop');
Loop::addSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 = static function () use ($socket, $logger) {
$logger->log('Received SIGTERM, stopping loop');

$socket->close();
Loop::stop();
});
} catch (\BadMethodCallException $e) {
$this->sapi->log('Notice: No signal handler support, installing ext-ev or ext-pcntl recommended for production use.');
$logger->log('Notice: No signal handler support, installing ext-ev or ext-pcntl recommended for production use.');
}
// @codeCoverageIgnoreEnd

Expand All @@ -273,7 +281,7 @@ private function runLoop(): void

if ($socket->getAddress() !== null) {
// Fiber compatibility mode for PHP < 8.1: Restart loop as long as socket is available
$this->sapi->log('Warning: Loop restarted. Upgrade to react/async v4 recommended for production use.');
$logger->log('Warning: Loop restarted. Upgrade to react/async v4 recommended for production use.');
} else {
break;
}
Expand All @@ -286,6 +294,7 @@ private function runLoop(): void

private function runOnce(): void
{
assert($this->sapi instanceof SapiHandler);
$request = $this->sapi->requestFromGlobals();

$response = $this->handleRequest($request);
Expand All @@ -294,6 +303,7 @@ private function runOnce(): void
$this->sapi->sendResponse($response);
} elseif ($response instanceof PromiseInterface) {
$response->then(function (ResponseInterface $response) {
assert($this->sapi instanceof SapiHandler);
$this->sapi->sendResponse($response);
});
}
Expand Down
45 changes: 45 additions & 0 deletions src/Io/LogStreamHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace FrameworkX\Io;

/**
* @internal
*/
class LogStreamHandler
{
/** @var resource */
private $stream;

/** @throws \RuntimeException if given `$path` can not be opened in append mode */
public function __construct(string $path)
{
$errstr = '';
\set_error_handler(function (int $_, string $error) use (&$errstr): bool {
// Match errstr from PHP's warning message.
// fopen(/dev/not-a-valid-path): Failed to open stream: Permission denied
$errstr = \preg_replace('#.*: #', '', $error);

return true;
});

$stream = \fopen($path, 'ae');
\restore_error_handler();

if ($stream === false) {
throw new \RuntimeException(
'Unable to open log file "' . $path . '": ' . $errstr
);
}

$this->stream = $stream;
}

public function log(string $message): void
{
$time = \microtime(true);
$prefix = \date('Y-m-d H:i:s', (int) $time) . \sprintf('.%03d ', (int) (($time - (int) $time) * 1e3));

$ret = \fwrite($this->stream, $prefix . $message . \PHP_EOL);
assert(\is_int($ret));
}
}
17 changes: 0 additions & 17 deletions src/Io/SapiHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,6 @@
*/
class SapiHandler
{
/** @var resource */
private $logStream;

public function __construct()
{
// @phpstan-ignore-next-line because `fopen()` is known to always return a `resource` for built-in wrappers
$this->logStream = PHP_SAPI === 'cli' ? \fopen('php://output', 'a') : (\defined('STDERR') ? \STDERR : \fopen('php://stderr', 'a'));
}

public function requestFromGlobals(): ServerRequestInterface
{
$host = null;
Expand Down Expand Up @@ -140,12 +131,4 @@ public function sendResponse(ResponseInterface $response): void
echo $body;
}
}

public function log(string $message): void
{
$time = microtime(true);
$log = date('Y-m-d H:i:s', (int)$time) . sprintf('.%03d ', (int)(($time - (int)$time) * 1e3)) . $message . PHP_EOL;

fwrite($this->logStream, $log);
}
}
Loading

0 comments on commit 4402433

Please sign in to comment.