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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,26 @@ class SomeDomain {
) {}
}
```

## Using DataDog processor

This processor is meant to be used with the DataDog agent to send the stack trace to DataDog.

And it also supports excluding frames from the stack trace.
This is useful if you have a lot of duplicate frames in the stack trace, and you don't want to send them to DataDog.
It's also nice to remove framework code from the stack traces since in most cases it's not relevant.

### Setup

```php
<?php declare(strict_types=1);

use Stefna\Logger\Processor\DataDogProcessor;

$processor = new DataDogProcessor([
[ // stack frames you aren't interested in.
'file' => 'vendor/phpunit/phpunit/src/Framework/TestSuite.php',
'line' => 685, // optional, if not set all frames including the file will be excluded
],
]);
```
95 changes: 93 additions & 2 deletions src/Processor/DatadogProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,107 @@

final class DatadogProcessor implements ProcessorInterface
{
public function __construct(
/** @var list<array{file: string, line?: int}> */
private readonly array $framesToExclude = [],
) {}

public function __invoke(LogRecord $record): LogRecord
{
$context = $record->context;
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$context['error.message'] = $context['exception']->getMessage();
$context['error.stack'] = $context['exception']->getTraceAsString();
$context['error.stack'] = $this->buildStackTrace($context['exception']->getTrace());
$context['error.kind'] = $context['exception']::class;
unset($context['exception']);

return $record->with(context: $context);
}

return $record;
}

/**
* @param list<array{
* function: string,
* type?: "::"|"->",
* line?: int,
* file?: string,
* class?: string,
* args?: array<mixed>,
* }> $stackTrace
*/
private function buildStackTrace(array $stackTrace): string
{
$filteredFrames = [];

foreach ($stackTrace as $trace) {
$excludeFrame = false;
foreach ($this->framesToExclude as $exclude) {
if (!isset($trace['file'])) {
break;
}
if (!str_ends_with($trace['file'], $exclude['file'])) {
continue;
}
if (isset($trace['line'], $exclude['line']) && $trace['line'] !== $exclude['line']) {
continue;
}
$excludeFrame = true;
break;
}
if (!$excludeFrame) {
$filteredFrames[] = $trace;
}
}

return $record->with(context: $context);
return $this->renderStackTrace($filteredFrames);
}

/**
* @param list<array{
* function: string,
* type?: "::"|"->",
* line?: int,
* file?: string,
* class?: string,
* args?: array<mixed>,
* }> $frames
*/
private function renderStackTrace(array $frames): string
{
$rtn = '';
foreach ($frames as $index => $frame) {
$file = '[internal function]';
if (isset($frame['file'], $frame['line'])) {
$file = sprintf('%s(%d)', $frame['file'], $frame['line']);
}
$renderedArguments = '';
if (isset($frame['args'])) {
$args = [];
foreach ($frame['args'] as $arg) {
$args[] = match (true) {
is_string($arg) => "'" . $arg . "'",
is_array($arg) => 'Array',
is_null($arg) => 'NULL',
is_bool($arg) => $arg ? 'true' : 'false',
is_object($arg) => get_class($arg),
is_resource($arg) => get_resource_type($arg),
default => $arg,
};
}
$renderedArguments = implode(', ', $args);
}
$rtn .= sprintf(
"#%d %s: %s%s%s(%s)\n",
$index,
$file,
$frame['class'] ?? '',
$frame['type'] ?? '',
$frame['function'],
$renderedArguments,
);
}
return $rtn;
}
}
101 changes: 101 additions & 0 deletions tests/Processor/DataDogProcessorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php declare(strict_types=1);

namespace Stefna\Logger\Processor;

use Monolog\Level;
use Monolog\LogRecord;
use PHPUnit\Framework\TestCase;
use Stefna\Logger\Processor\DatadogProcessor;

final class DataDogProcessorTest extends TestCase
{
public function testSimpleException(): void
{
$processor = new DataDogProcessor();
$exception = new \RuntimeException('test exception');
$logRecord = new LogRecord(
new \DateTimeImmutable(),
'test',
Level::Info,
'test',
[
'exception' => $exception,
],
);
$newLogRecord = $processor($logRecord);

$this->assertNotSame($logRecord, $newLogRecord);

$this->assertArrayHasKey('error.message', $newLogRecord->context);
$this->assertArrayHasKey('error.stack', $newLogRecord->context);
$this->assertArrayHasKey('error.kind', $newLogRecord->context);

$this->assertSame('test exception', $newLogRecord->context['error.message']);
$this->assertSame('RuntimeException', $newLogRecord->context['error.kind']);;
}

public function testNoChangeIfContextDontHaveException(): void
{
$processor = new DataDogProcessor();
$logRecord = new LogRecord(
new \DateTimeImmutable(),
'test',
Level::Info,
'test',
[
'test' => 1,
],
);
$newLogRecord = $processor($logRecord);

$this->assertSame($logRecord, $newLogRecord);
}

public function testNoChangeIfContextHaveStrangeExceptionType(): void
{
$processor = new DataDogProcessor();
$logRecord = new LogRecord(
new \DateTimeImmutable(),
'test',
Level::Info,
'test',
[
'exception' => 1,
],
);
$newLogRecord = $processor($logRecord);

$this->assertSame($logRecord, $newLogRecord);
}

public function testExcludeFrames(): void
{
$processor = new DataDogProcessor([
[
'file' => 'vendor/phpunit/phpunit/src/Framework/TestSuite.php',
]
]);
$exception = new \RuntimeException('test exception');
$logRecord = new LogRecord(
new \DateTimeImmutable(),
'test',
Level::Info,
'test',
[
'exception' => $exception,
],
);
$newLogRecord = $processor($logRecord);

$this->assertNotSame($logRecord, $newLogRecord);

$this->assertArrayHasKey('error.message', $newLogRecord->context);
$this->assertArrayHasKey('error.stack', $newLogRecord->context);
$this->assertArrayHasKey('error.kind', $newLogRecord->context);

$this->assertSame('test exception', $newLogRecord->context['error.message']);
$this->assertSame('RuntimeException', $newLogRecord->context['error.kind']);;

$this->assertStringNotContainsString('TestSuite.php', $newLogRecord->context['error.stack']);
}
}