Skip to content

Add Monolog Sentry Logs handler #1867

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
59 changes: 22 additions & 37 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@
<code>$parsedDsn['user']</code>
</PossiblyUndefinedArrayOffset>
</file>
<file src="src/HttpClient/HttpClientFactory.php">
<UndefinedClass occurrences="5">
<code>Guzzle6HttpClient</code>
<code>GuzzleHttpClientOptions</code>
<code>GuzzleHttpClientOptions</code>
<code>GuzzleHttpClientOptions</code>
<code>SymfonyHttpClient</code>
</UndefinedClass>
</file>
<file src="src/Integration/IntegrationRegistry.php">
<PossiblyInvalidArgument occurrences="2">
<code>$userIntegration</code>
Expand All @@ -38,6 +29,14 @@
<code>int|string|Level|LogLevel::*</code>
</UndefinedDocblockClass>
</file>
<file src="src/Monolog/CompatibilityLogLevelTrait.php">
<DuplicateClass occurrences="1">
<code>CompatibilityLogLevelTrait</code>
</DuplicateClass>
<UndefinedClass occurrences="1">
<code>Level</code>
</UndefinedClass>
</file>
<file src="src/Monolog/CompatibilityProcessingHandlerTrait.php">
<DuplicateClass occurrences="1">
<code>CompatibilityProcessingHandlerTrait</code>
Expand All @@ -60,6 +59,20 @@
<code>$record['context']</code>
</PossiblyUndefinedMethod>
</file>
<file src="src/Monolog/LogsHandler.php">
<PossiblyInvalidArgument occurrences="3">
<code>$record['level']</code>
<code>$record['level']</code>
<code>$record['message']</code>
</PossiblyInvalidArgument>
<PossiblyInvalidArrayOffset occurrences="1">
<code>$record['context']['exception']</code>
</PossiblyInvalidArrayOffset>
<PossiblyUndefinedMethod occurrences="2">
<code>$record['context']</code>
<code>$record['context']</code>
</PossiblyUndefinedMethod>
</file>
<file src="src/Profiling/Profile.php">
<LessSpecificReturnStatement occurrences="1"/>
<MoreSpecificReturnType occurrences="1">
Expand All @@ -79,37 +92,9 @@
<code>representationSerialize</code>
</InvalidReturnType>
</file>
<file src="src/State/Hub.php">
<TooManyArguments occurrences="3">
<code>captureException</code>
<code>captureLastError</code>
<code>captureMessage</code>
</TooManyArguments>
</file>
<file src="src/State/HubAdapter.php">
<TooManyArguments occurrences="4">
<code>captureException</code>
<code>captureLastError</code>
<code>captureMessage</code>
<code>startTransaction</code>
</TooManyArguments>
</file>
<file src="src/Tracing/SpanContext.php">
<UnsafeInstantiation occurrences="1">
<code>new static()</code>
</UnsafeInstantiation>
</file>
<file src="src/Tracing/Transaction.php">
<NonInvariantDocblockPropertyType occurrences="1">
<code>$transaction</code>
</NonInvariantDocblockPropertyType>
</file>
<file src="src/functions.php">
<TooManyArguments occurrences="4">
<code>captureException</code>
<code>captureLastError</code>
<code>captureMessage</code>
<code>startTransaction</code>
</TooManyArguments>
</file>
</files>
29 changes: 20 additions & 9 deletions src/Logs/LogLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,66 @@ class LogLevel
*/
private $value;

/**
* @var int The priority of the log level, used for sorting
*/
private $priority;

/**
* @var array<string, self> A list of cached enum instances
*/
private static $instances = [];

private function __construct(string $value)
private function __construct(string $value, int $priority)
{
$this->value = $value;
$this->priority = $priority;
}

public static function trace(): self
{
return self::getInstance('trace');
return self::getInstance('trace', 10);
}

public static function debug(): self
{
return self::getInstance('debug');
return self::getInstance('debug', 20);
}

public static function info(): self
{
return self::getInstance('info');
return self::getInstance('info', 30);
}

public static function warn(): self
{
return self::getInstance('warn');
return self::getInstance('warn', 40);
}

public static function error(): self
{
return self::getInstance('error');
return self::getInstance('error', 50);
}

public static function fatal(): self
{
return self::getInstance('fatal');
return self::getInstance('fatal', 60);
}

public function __toString(): string
{
return $this->value;
}

private static function getInstance(string $value): self
public function getPriority(): int
{
return $this->priority;
}

private static function getInstance(string $value, int $priority): self
{
if (!isset(self::$instances[$value])) {
self::$instances[$value] = new self($value);
self::$instances[$value] = new self($value, $priority);
}

return self::$instances[$value];
Expand Down
77 changes: 77 additions & 0 deletions src/Monolog/CompatibilityLogLevelTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Sentry\Monolog;

use Monolog\Level;
use Monolog\Logger;
use Sentry\Logs\LogLevel;

if (Logger::API >= 3) {
/**
* Logic which is used if monolog >= 3 is installed.
*
* @internal
*/
trait CompatibilityLogLevelTrait
{
/**
* Translates the Monolog level into the Sentry LogLevel.
*/
private static function getSentryLogLevelFromMonologLevel(int $level): LogLevel
{
$level = Level::from($level);

switch ($level) {
case Level::Debug:
return LogLevel::debug();
case Level::Warning:
return LogLevel::warn();
case Level::Error:
return LogLevel::error();
case Level::Critical:
case Level::Alert:
case Level::Emergency:
return LogLevel::fatal();
case Level::Info:
case Level::Notice:
default:
return LogLevel::info();
}
}
}
} else {
/**
* Logic which is used if monolog < 3 is installed.
*
* @internal
*/
trait CompatibilityLogLevelTrait
{
/**
* Translates the Monolog level into the Sentry LogLevel.
*
* @param Logger::DEBUG|Logger::INFO|Logger::NOTICE|Logger::WARNING|Logger::ERROR|Logger::CRITICAL|Logger::ALERT|Logger::EMERGENCY $level The Monolog log level
*/
private static function getSentryLogLevelFromMonologLevel(int $level): LogLevel
{
switch ($level) {
case Logger::DEBUG:
return LogLevel::debug();
case Logger::WARNING:
return LogLevel::warn();
case Logger::ERROR:
return LogLevel::error();
case Logger::CRITICAL:
case Logger::ALERT:
case Logger::EMERGENCY:
return LogLevel::fatal();
case Logger::INFO:
case Logger::NOTICE:
default:
return LogLevel::info();
}
}
}
}
114 changes: 114 additions & 0 deletions src/Monolog/LogsHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace Sentry\Monolog;

use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\HandlerInterface;
use Monolog\LogRecord;
use Sentry\Logs\LogLevel;
use Sentry\Logs\Logs;

class LogsHandler implements HandlerInterface
{
use CompatibilityLogLevelTrait;

/**
* The minimum logging level at which this handler will be triggered.
*
* @var LogLevel
*/
private $logLevel;

/**
* Whether the messages that are handled can bubble up the stack or not.
*
* @var bool
*/
private $bubble;

/**
* Creates a new Monolog handler that converts Monolog logs to Sentry logs.
*
* @param LogLevel|null $logLevel the minimum logging level at which this handler will be triggered and collects the logs
* @param bool $bubble whether the messages that are handled can bubble up the stack or not
*/
public function __construct(?LogLevel $logLevel = null, bool $bubble = true)
{
$this->logLevel = $logLevel ?? LogLevel::debug();
$this->bubble = $bubble;
}

/**
* @param array<string, mixed>|LogRecord $record
*/
public function isHandling($record): bool
{
return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority();
}

/**
* @param array<string, mixed>|LogRecord $record
*/
public function handle($record): bool
{
// Do not collect logs for exceptions, they should be handled seperately by the `Handler` or `captureException`
if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) {
return false;
}

Logs::getInstance()->aggregator()->add(
self::getSentryLogLevelFromMonologLevel($record['level']),
$record['message'],
[],
array_merge($record['context'], $record['extra'])
);

return $this->bubble === false;
}

/**
* @param array<array<string, mixed>|LogRecord> $records
*/
public function handleBatch(array $records): void
{
foreach ($records as $record) {
$this->handle($record);
}
}

public function close(): void
{
Logs::getInstance()->flush();
}

/**
* @param callable $callback
*/
public function pushProcessor($callback): void
{
// noop, this handler does not support processors
}

/**
* @return callable
*/
public function popProcessor()
{
// Since we do not support processors, we throw an exception if this method is called
throw new \LogicException('You tried to pop from an empty processor stack.');
}

public function setFormatter(FormatterInterface $formatter): void
{
// noop, this handler does not support formatters
}

public function getFormatter(): FormatterInterface
{
// To adhere to the interface we need to return a formatter so we return a default one
return new LineFormatter();
}
}
Loading