Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate logging code to winter from storm to prevent excess file logs being written #1319

Merged
merged 4 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions modules/system/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,23 @@ protected function registerErrorHandler()
protected function registerLogging()
{
Event::listen(\Illuminate\Log\Events\MessageLogged::class, function ($event) {
if (!EventLog::useLogging()) {
return;
}

$details = $event->context ?? null;

// This allows for preventing db logging in cases where we don't want all log messages logged to the DB.
if (isset($details['skipDatabaseLog']) && $details['skipDatabaseLog']) {
return;
}

EventLog::add($event->message, $event->level, $details);
});

Event::listen('exception.report', function (\Throwable $throwable) {
if (EventLog::useLogging()) {
$details = $event->context ?? null;
EventLog::add($event->message, $event->level, $details);
EventLog::addException($throwable);
}
});
}
Expand Down
185 changes: 181 additions & 4 deletions modules/system/models/EventLog.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<?php namespace System\Models;

use App;
use Exception;
use Model;
use Str;
use Illuminate\Support\Facades\App;
use Throwable;
use ReflectionClass;
use Winter\Storm\Database\Model;
use Winter\Storm\Support\Str;

/**
* Model for logging system errors and debug trace messages
Expand All @@ -13,6 +15,9 @@
*/
class EventLog extends Model
{
protected const EXCEPTION_LOG_VERSION = 2;
protected const EXCEPTION_SNIPPET_LINES = 12;

/**
* @var string The database table used by the model.
*/
Expand Down Expand Up @@ -40,7 +45,7 @@ class_exists('Model') &&
/**
* Creates a log record
*/
public static function add(string $message, string $level = 'info', ?array $details = null): self
public static function add(string $message, string $level = 'info', ?array $details = null): static
{
$record = new static;
$record->message = $message;
Expand All @@ -59,6 +64,25 @@ public static function add(string $message, string $level = 'info', ?array $deta
return $record;
}

/**
* Creates an exception log record
*/
public static function addException(Throwable $throwable, string $level = 'error'): static
{
$record = new static;
$record->message = $throwable->getMessage();
$record->level = $level;
$record->details = $record->getDetails($throwable);

try {
$record->save();
}
catch (Exception $ex) {
}

return $record;
}

/**
* Beautify level value.
*/
Expand All @@ -82,4 +106,157 @@ public function getSummaryAttribute(): string

return Str::limit($matches[1] ?? '', 500);
}

/**
* Constructs the details array for logging
*/
public function getDetails(Throwable $throwable): array
{
return [
'logVersion' => static::EXCEPTION_LOG_VERSION,
'exception' => $this->exceptionToArray($throwable),
'environment' => $this->getEnviromentInfo(),
];
}

/**
* Convert a throwable into an array of data for logging
*/
protected function exceptionToArray(Throwable $throwable): array
{
return [
'type' => $throwable::class,
'message' => $throwable->getMessage(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'snippet' => $this->getSnippet($throwable->getFile(), $throwable->getLine()),
'trace' => $this->exceptionTraceToArray($throwable->getTrace()),
'stringTrace' => $throwable->getTraceAsString(),
'code' => $throwable->getCode(),
'previous' => $throwable->getPrevious()
? $this->exceptionToArray($throwable->getPrevious())
: null,
];
}

/**
* Generate an array trace with extra data not provided by the default trace
*
* @throws \ReflectionException
*/
protected function exceptionTraceToArray(array $trace): array
{
foreach ($trace as $index => $frame) {
if (!isset($frame['file']) && isset($frame['class'])) {
$ref = new ReflectionClass($frame['class']);
$frame['file'] = $ref->getFileName();

if (!isset($frame['line']) && isset($frame['function']) && !str_contains($frame['function'], '{')) {
foreach (file($frame['file']) as $line => $text) {
if (preg_match(sprintf('/function\s.*%s/', $frame['function']), $text)) {
$frame['line'] = $line + 1;
break;
}
}
}
}

$trace[$index] = [
'file' => $frame['file'] ?? null,
'line' => $frame['line'] ?? null,
'function' => $frame['function'] ?? null,
'class' => $frame['class'] ?? null,
'type' => $frame['type'] ?? null,
'snippet' => !empty($frame['file']) && !empty($frame['line'])
? $this->getSnippet($frame['file'], $frame['line'])
: '',
'in_app' => ($frame['file'] ?? null) ? $this->isInAppError($frame['file']) : false,
'arguments' => array_map(function ($arg) {
if (is_numeric($arg)) {
return $arg;
}
if (is_string($arg)) {
return "'$arg'";
}
if (is_null($arg)) {
return 'null';
}
if (is_bool($arg)) {
return $arg ? 'true' : 'false';
}
if (is_array($arg)) {
return 'Array';
}
if (is_object($arg)) {
return get_class($arg);
}
if (is_resource($arg)) {
return 'Resource';
}
}, $frame['args'] ?? []),
];
}

return $trace;
}

/**
* Get the code snippet referenced in a trace
*/
protected function getSnippet(string $file, int $line): array
{
if (str_contains($file, ': eval()\'d code')) {
return [];
}

$lines = file($file);

if (count($lines) < static::EXCEPTION_SNIPPET_LINES) {
return $lines;
}

return array_slice(
$lines,
$line - (static::EXCEPTION_SNIPPET_LINES / 2),
static::EXCEPTION_SNIPPET_LINES,
true
);
}

/**
* Get environment details to record with the exception
*/
protected function getEnviromentInfo(): array
{
if (app()->runningInConsole()) {
return [
'context' => 'CLI',
'testing' => app()->runningUnitTests(),
'env' => app()->environment(),
];
}

return [
'context' => 'Web',
'backend' => method_exists(app(), 'runningInBackend') ? app()->runningInBackend() : false,
'testing' => app()->runningUnitTests(),
'url' => app('url')->current(),
'method' => app('request')->method(),
'env' => app()->environment(),
'ip' => app('request')->ip(),
'userAgent' => app('request')->header('User-Agent'),
];
}

/**
* Helper to work out if a file should be considered "In App" or not
*/
protected function isInAppError(string $file): bool
{
if (basename($file) === 'index.php' || basename($file) === 'artisan') {
return false;
}

return !Str::startsWith($file, base_path('vendor')) && !Str::startsWith($file, base_path('modules'));
}
}
Loading