Skip to content
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
43 changes: 7 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,16 @@ Sends more info about the error than [Tracy\Logger](https://github.com/nette/tra
Installation
------------

````
````bash
composer require adt/error-logger
````

Place this to your bootstrap.php after calling `$configurator->enableDebugger()` and before calling `$configurator->createContainer()`:
````
Place this to your bootstrap.php after calling `$configurator->enableDebugger()`:

````php
$logger = \ADT\ErrorLogger::install($email = 'errors@example.com', $maxEmailsPerDay = 10, $maxEmailsPerRequest = 10);
````
and this after calling `$configurator->createContainer()`:
```
if ($logger) {
$logger->setup($container);
if (!\Tracy\Debugger::$productionMode) {
// Do not send emails
$logger->mailer = null;
}
```

**Sensitive fields:**

You can specify keys of array, which will be hidden in POST dump.

Example:

````
$logger = \ADT\ErrorLogger::install(
'errors@example.com',
10,
10,
$sensitiveFields = [
'password',
]
);
````

POST dump:

```
POST:array (4)
username => "my_username" (11)
password => "*****" (5)
login => "Sign in" (7)
_do => "signForm-submit" (15)
```

3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
],
"require": {
"php": ">=7.4",
"tracy/tracy": ">=2.6.0",
"nette/di": "^2.4|^3.0"
"tracy/tracy": ">=2.6.0"
},
"suggest": {
"adt/tracy-git": "Useful for displaying information about currently deployed application version."
Expand Down
178 changes: 50 additions & 128 deletions src/ErrorLogger.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,168 +3,103 @@
namespace ADT;

use DateTime;
use Exception;
use Nette\DI\Container;
use RuntimeException;
use Tracy\Debugger;
use Tracy\Dumper;
use Tracy\Helpers;
use Tracy\Logger;

class ErrorLogger extends Logger
{
/**
* Maximální počet odeslaných emailů denně
* Maximum number of emails per day
* @var int
*/
protected int $maxEmailsPerDay;

/**
* Maximální počet odeslaných emailů v rámci jednoho requestu
* Maximum number of emails per request
* @var int
*/
protected int $maxEmailsPerRequest;

/**
* Počet odeslaných emailů v rámci aktuálního requestu
* Number of emails in current request
* @var int
*/
protected int $sentEmailsPerRequest = 0;

/**
* Pole s citlivými údaji, jejižch hodnoty se nemají zobrazovat ve výpisu.
* @var array
*/
protected array $sensitiveFields = [];

/**
* Ma se vlozit error message do emailu
* Include exception file as an attachment?
*/
protected bool $includeErrorMessage = true;

protected ?Container $container = null;

public static function install($email, $maxEmailsPerDay = NULL, $maxEmailsPerRequest = NULL, $sensitiveFields = [], $includeErrorMessage = true): ?self
public static function install($email, $maxEmailsPerDay = NULL, $maxEmailsPerRequest = NULL, $includeErrorMessage = true): ?self
{
if (!Debugger::$productionMode) {
return null;
}

Debugger::$email = $email;

$logger = new static(Debugger::$logDirectory, Debugger::$email, Debugger::getBlueScreen());

$logger->maxEmailsPerDay = $maxEmailsPerDay ?: 10;
$logger->maxEmailsPerRequest = $maxEmailsPerRequest ?: 10;
$logger->sensitiveFields = $sensitiveFields;
$logger->includeErrorMessage = $includeErrorMessage;

Debugger::setLogger($logger);

return $logger;
}

public function setup(Container $container)
{
$this->container = $container;
}

protected function sendEmail($message): void
{
if (
$this->email
!$this->email
||
!$this->mailer
) {
return;
}

$exceptionFile = $message instanceof \Throwable
? $this->getExceptionFile($message)
: null;
$line = static::formatLogLine($message, $exceptionFile);
$logFile = $this->directory . '/email-sent';

// Delete email-sent file from yesterday
if (date('Y-m-d', @filemtime($logFile)) < (new DateTime('midnight'))->format('Y-m-d')) {
@unlink($logFile);
}

$messageHash = preg_replace('~(Resource id #)\d+~', '$1', $message);
$messageHash = preg_replace('~(PID: )\d+~', '$1', $messageHash);
$messageHash = md5($messageHash);

if ($this->sentEmailsPerRequest >= $this->maxEmailsPerRequest) {
// Limit per request exceeded
return;
}

if (
($logContent = @file_get_contents($logFile))
&&
$this->mailer
(strstr($logContent, $messageHash) !== false)
) {
$exceptionFile = $message instanceof \Throwable
? $this->getExceptionFile($message)
: null;
$line = static::formatLogLine($message, $exceptionFile);
$logFile = $this->directory . '/email-sent';

// we delete email-sent file from yesterday
if (date('Y-m-d', @filemtime($logFile)) < (new DateTime('midnight'))->format('Y-m-d')) {
@unlink($logFile);
}
// Duplicate error
return;
}

$messageHash = preg_replace('~(Resource id #)\d+~', '$1', $message);
$messageHash = preg_replace('~(PID: )\d+~', '$1', $messageHash);
$messageHash = md5($messageHash);

if (
// ještě se vejdeme do limitu v rámci aktuálního requestu
$this->sentEmailsPerRequest < $this->maxEmailsPerRequest
&&
// tento hash jsme ještě neposlali
(
!($logContent = @file_get_contents($logFile))
||
(strstr($logContent, $messageHash) === false)
)
&&
// ještě se vejdeme do limitu v rámci aktuálního dne
substr_count($logContent, date('Y-m-d')) < $this->maxEmailsPerDay
) {
if (!@file_put_contents($logFile, $line . ' ' . $messageHash . PHP_EOL, FILE_APPEND | LOCK_EX)) {
throw new RuntimeException("Unable to write to log file '" . $logFile . "'. Is directory writable?");
}

// sestavíme zprávu
if (is_array($message)) {
$stringMessage = implode(' ', $message);
} else {
$stringMessage = $message;
}

$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if (count($backtrace) > 3) { //pokud jsou 3 tak jde pouze o exception a je ulozena nette chybova stranka
$backtraceString = "";

for ($i = 0; $i < count($backtrace); $i++) {
$backtraceData = $backtrace[$i] + [
'file' => '_unknown_',
'line' => '_unknown_',
'function' => '_unknown_',
];

$backtraceString = "#$i {$backtraceData['file']}({$backtraceData['line']}): "
. (isset($backtraceData['class']) ? $backtraceData['class'] . '::' : '')
. "{$backtraceData['function']}()\n";
}

$stringMessage .= "\n\n" . $backtraceString;
}


// přidáme doplnující info - referer, browser...
$stringMessage .= "\n\n" .
(isset($_SERVER['HTTP_HOST']) ? 'LINK:' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . "\n" : '') .
'SERVER:' . Dumper::toText($_SERVER) . "\n\n" .
'GET:' . Dumper::toText($_GET, [Dumper::DEPTH => 10]) . "\n\n" .
'POST:' . Dumper::toText($this->hideSensitiveFieldValue($_POST), [Dumper::DEPTH => 10]);

if ($this->container && ($securityUser = $this->container->getByType('\Nette\Security\User', FALSE))) {
// obalujeme do try protoze SecurityUser je zavisly na databazi a pokud je chyba v db, tak nam error nedojde
try {
$stringMessage .= "\n\n" .
'securityUser:' . Dumper::toText($securityUser->identity, [Dumper::DEPTH => 1]);
} catch (Exception $e) {}
}

if ($this->container && ($git = $this->container->getByType('\ADT\TracyGit\Git', FALSE)) !== NULL && ($gitInfo = $git->getInfo())) {
$stringMessage .= "\n\n";

foreach ($gitInfo as $key => $value) {
$stringMessage .= $key . ": " . $value . "\n";
}
}

// odešleme chybu emailem
call_user_func($this->mailer, $stringMessage, implode(', ', (array)$this->email), $exceptionFile);

$this->sentEmailsPerRequest++;
}
if (substr_count($logContent, date('Y-m-d')) >= $this->maxEmailsPerDay) {
// Limit per day exceeded
return;
}

if (!@file_put_contents($logFile, $line . ' ' . $messageHash . PHP_EOL, FILE_APPEND | LOCK_EX)) {
throw new RuntimeException("Unable to write to log file '" . $logFile . "'. Is directory writable?");
}


call_user_func($this->mailer, $message, implode(', ', (array)$this->email), $exceptionFile);

$this->sentEmailsPerRequest++;
}

/**
Expand Down Expand Up @@ -217,17 +152,4 @@ public function defaultMailer($message, string $email, ?string $attachment = nul
mail($email, $parts['subject'], $parts['body'], $parts['headers']);
}

protected function hideSensitiveFieldValue($array): array
{
$sensitiveFields = $this->sensitiveFields;
$replacement = '*****';

array_walk_recursive($array, function (&$value, $key) use ($sensitiveFields, $replacement) {
if (in_array($key, $sensitiveFields, TRUE)) {
$value = $replacement;
}
});

return $array;
}
}