Skip to content

Commit

Permalink
Add OTEL logging
Browse files Browse the repository at this point in the history
  • Loading branch information
GDXbsv committed Sep 18, 2024
1 parent 9bcf458 commit 10be21f
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 32 deletions.
57 changes: 37 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,30 +95,47 @@ symfony_logging:
```
make monolog configuration looks like this
```yaml
monolog:
handlers:
handler1:
type: stream
path: "php://stderr"
formatter: 'Gotphoto\Logging\Formatter'
main:
type: stream
path: "php://stderr"
formatter: 'Gotphoto\Logging\Formatter'
level: info
channels: [ "!something"]
handler2:
formatter: 'Gotphoto\Logging\Formatter'
```php
<?php declare(strict_types=1);

use Gotphoto\Logging\Formatter;
use Gotphoto\Logging\OtelFormatter;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Config\MonologConfig;

return static function (MonologConfig $monolog, ContainerConfigurator $containerConfigurator): void {

$monolog->handler('newrelic')
->type('stream')
->path('php://stderr')
->formatter(Formatter::class)
// log start from info messages (debug is lowest level)
->level('info');
$monolog->handler('otel')
->type('service')
->id(Handler::class)
->formatter(OtelFormatter::class)
// log start from info messages (debug is lowest level)
->level('info');

};

```
Where the most important things are:

NewRelic:
```
->type('stream')
->path('php://stderr')
->formatter(Formatter::class)
```
Where the most important things are
Otel:
```
type: stream
path: "php://stderr"
formatter: 'Gotphoto\Logging\Formatter'
->type('service')
->id(Handler::class)
->formatter(OtelFormatter::class)
```
And `main` is a fully working example

### Exception context
Works in Symfony automatically. Just create implementation for the interface `Gotphoto\Logging\ExceptionContext\ExceptionContext` and add it as a
Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
"description": "Integrate a common way for log handling.",
"type": "library",
"require": {
"php": "^8.1",
"monolog/monolog": "^3.3"
"php": "^8.2",
"monolog/monolog": "^3.7.0",
"open-telemetry/opentelemetry-logger-monolog": "^1.0"
},
"require-dev": {
"vimeo/psalm": "^4.24",
"vimeo/psalm": "^5.26.1",
"illuminate/support": "^9.19",
"symfony/http-kernel": "^6.1",
"symfony/dependency-injection": "^6.1",
Expand Down
2 changes: 1 addition & 1 deletion src/ExceptionContext/GuzzleRequestExceptionContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function __invoke(RequestException $exception): array
{
/**
* @psalm-suppress PossiblyNullReference
* @psalm-suppress RedundantConditionGivenDocblockType
* @psalm-suppress RedundantCondition
*/
if ($exception->getResponse() !== null && $exception->getResponse()->getBody() !== null) {
return ['message' => $exception->getResponse()->getBody()->getContents()];
Expand Down
4 changes: 3 additions & 1 deletion src/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ public function __construct(
*/
public function format(LogRecord $record): string
{
/** @var array{timestamp: int, datetime: string} $data */
/** @var array{timestamp: int, datetime: string, extra?:array, context?:array} $data */
$data = parent::format($record);
/** @psalm-suppress RiskyTruthyFalsyComparison this is okay null or empty string */
if (empty($data['datetime'])) {
$data['datetime'] = gmdate('c');
}
Expand Down Expand Up @@ -100,6 +101,7 @@ protected function normalize(mixed $data, int $depth = 0): mixed
$data = array_merge($data, $data['extra']['newrelic-context']);
/** @psalm-suppress MixedArrayAccess we checked that it is an array */
unset($data['extra']['newrelic-context']);
/** @psalm-suppress RiskyTruthyFalsyComparison this is okay null or empty string */
if (empty($data['extra'])) {
unset($data['extra']);
}
Expand Down
17 changes: 16 additions & 1 deletion src/Laravel/LaravelLoggerCreating.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace Gotphoto\Logging\Laravel;

use App\Lib\Log\OtelFormatter;
use Aws\Exception\AwsException;
use Gotphoto\Logging\ExceptionContext\AwsExceptionContext;
use Gotphoto\Logging\ExceptionContext\ExceptionContext;
use Gotphoto\Logging\ExceptionContext\GuzzleRequestExceptionContext;
use Gotphoto\Logging\Formatter;
use Gotphoto\Logging\NewrelicProcessor;
Expand All @@ -17,6 +17,9 @@
use Monolog\Logger;
use Monolog\Processor\ProcessorInterface;
use Monolog\Processor\PsrLogMessageProcessor;
use OpenTelemetry\API\Globals;
use OpenTelemetry\Contrib\Logs\Monolog\Handler;
use Psr\Log\LogLevel;

final class LaravelLoggerCreating
{
Expand Down Expand Up @@ -56,6 +59,18 @@ public function __invoke(array $config)
);
$log->pushHandler($handler);

$otelHandler = new Handler(
Globals::loggerProvider(),
LogLevel::INFO
);
$otelHandler->setFormatter(
new \Gotphoto\Logging\OtelFormatter([
RequestException::class => [new GuzzleRequestExceptionContext()],
AwsException::class => [new AwsExceptionContext()],
])
);
$log->pushHandler($otelHandler);

return $log;
}
}
2 changes: 2 additions & 0 deletions src/NewrelicProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class NewrelicProcessor implements ProcessorInterface
* Returns the given record with the New Relic linking metadata added
* if a compatible New Relic extension is loaded, otherwise returns the
* given record unmodified
*
* @return LogRecord The processed record
*/
public function __invoke(LogRecord $record)
{
Expand Down
62 changes: 62 additions & 0 deletions src/OtelFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Gotphoto\Logging;

use Monolog\Formatter\NormalizerFormatter;

/**
* @internal
*/
final class OtelFormatter extends NormalizerFormatter
{
/**
* @param array<string, array<callable>> $exceptionContextProviderMap
*/
public function __construct(private readonly array $exceptionContextProviderMap = [])
{
parent::__construct();
}

/**
* @return array
*/
protected function normalizeException(\Throwable $e, int $depth = 0)
{
/** @var array{message: string, context?: array<string, mixed>} $data */
$data = parent::normalizeException($e, $depth);

$exceptionProviders = $this->getExceptionContexts($e);

foreach ($exceptionProviders as $exceptionProvider) {
/**
* @var array<string, mixed> $additionalContext
*/
$additionalContext = $exceptionProvider($e);
if (!empty($additionalContext)) {
$data['context'] = ($data['context'] ?? []) + $additionalContext;
}
}

return $data;
}

/**
* @return array<callable>
*/
protected function getExceptionContexts(\Throwable $e): array
{
if (isset($this->exceptionContextProviderMap[\get_class($e)])) {
return $this->exceptionContextProviderMap[\get_class($e)];
}
$exceptionContexts = [];
foreach (array_keys($this->exceptionContextProviderMap) as $className) {
if ($e instanceof $className) {
$exceptionContexts = array_merge($exceptionContexts + $this->exceptionContextProviderMap[$className]);
}
}

return $exceptionContexts;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Gotphoto\Logging\Symfony\DependencyInjection\Compiler;

use Gotphoto\Logging\Formatter;
use Gotphoto\Logging\OtelFormatter;
use ReflectionClass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -22,7 +23,7 @@ public function process(ContainerBuilder $container)
if (!$reflectionClass->hasMethod('__invoke')) {
throw new \Exception($definition->getClass() . ' has to have __invoke method.');
}
$reflectionMethod = $reflectionClass->getMethod('__invoke');
$reflectionMethod = $reflectionClass->getMethod('__invoke');
$typehintClassName = $reflectionMethod->getParameters()[0]->getClass()->getName();
if (!is_subclass_of($typehintClassName, Throwable::class)) {
throw new \Exception($definition->getClass() . ' has to have __invoke method with argument "is_subclass_of Throwable".');
Expand All @@ -31,7 +32,8 @@ public function process(ContainerBuilder $container)
$exceptionContextMap[$typehintClassName][] = new Reference($id);
}

$definition = $container->getDefinition(Formatter::class);
$definition->setArgument('$exceptionContextProviderMap', $exceptionContextMap);
$container->getDefinition(Formatter::class)->setArgument('$exceptionContextProviderMap', $exceptionContextMap);

$container->getDefinition(OtelFormatter::class)->setArgument('$exceptionContextProviderMap', $exceptionContextMap);
}
}
3 changes: 0 additions & 3 deletions src/Symfony/SymfonyLoggingBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@

class SymfonyLoggingBundle extends Bundle
{
/**
* @psalm-suppress MissingReturnType can not use with php 7.0
*/
public function build(ContainerBuilder $container)
{
parent::build($container);
Expand Down
15 changes: 15 additions & 0 deletions src/Symfony/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@
use Gotphoto\Logging\ExceptionContext\GuzzleRequestExceptionContext;
use Gotphoto\Logging\Formatter;
use Gotphoto\Logging\NewrelicProcessor;
use Gotphoto\Logging\OtelFormatter;
use Monolog\Processor\PsrLogMessageProcessor;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\Contrib\Logs\Monolog\Handler;
use Psr\Log\LogLevel;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\inline_service;

return static function (ContainerConfigurator $containerConfigurator) {
$s = $containerConfigurator->services();
Expand All @@ -28,4 +34,13 @@
->tag('gotphoto_logging.exception_context');
$s->set(GuzzleRequestExceptionContext::class)
->tag('gotphoto_logging.exception_context');

$s->set(Handler::class)
->arg(
'$loggerProvider',
inline_service(LoggerProviderInterface::class)
->factory([Globals::class, 'loggerProvider']),
)
->arg('$level', LogLevel::INFO);
$s->set(OtelFormatter::class);
};

0 comments on commit 10be21f

Please sign in to comment.