Skip to content

Commit

Permalink
refs AB#7245 Add error logs for console commands and add ability to s…
Browse files Browse the repository at this point in the history
…end the X-Context-ID header on a response (#28)
  • Loading branch information
marforon authored Apr 24, 2023
1 parent 0e9bedc commit c4c59ce
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 2 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"mongodb/mongodb": "^1.9",
"monolog/monolog": "^2.7",
"nelmio/api-doc-bundle": "^4.8",
"symfony/console": "^6.0",
"symfony/dependency-injection": ">=5.3|^6.0",
"symfony/dotenv": ">=5.3|^6.0",
"symfony/expression-language": "^5.3|^6.0",
Expand Down
18 changes: 16 additions & 2 deletions src/DependencyInjection/AnzuSystemsCommonExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use AnzuSystems\CommonBundle\Domain\PermissionGroup\PermissionGroupFacade;
use AnzuSystems\CommonBundle\Domain\PermissionGroup\PermissionGroupManager;
use AnzuSystems\CommonBundle\Domain\User\CurrentAnzuUserProvider;
use AnzuSystems\CommonBundle\Event\Listener\ConsoleExceptionListener;
use AnzuSystems\CommonBundle\Event\Listener\ContextIdOnResponseListener;
use AnzuSystems\CommonBundle\Event\Listener\ExceptionListener;
use AnzuSystems\CommonBundle\Event\Subscriber\AuditLogSubscriber;
use AnzuSystems\CommonBundle\Event\Subscriber\CommandLockSubscriber;
Expand Down Expand Up @@ -55,7 +57,6 @@
use AnzuSystems\CommonBundle\Validator\Validator;
use AnzuSystems\SerializerBundle\Metadata\MetadataRegistry;
use AnzuSystems\SerializerBundle\Serializer;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use MongoDB;
Expand All @@ -71,6 +72,7 @@
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\KernelEvents;

final class AnzuSystemsCommonExtension extends Extension implements PrependExtensionInterface
{
Expand Down Expand Up @@ -228,6 +230,12 @@ private function loadSettings(ContainerBuilder $container): void
->getDefinition(CurrentAnzuUserProvider::class)
->replaceArgument('$userEntityClass', $settings['user_entity_class']);

if ($settings['send_context_id_with_response']) {
$container->register(ContextIdOnResponseListener::class)
->addTag('kernel.event_listener', ['event' => KernelEvents::RESPONSE])
;
}

$definition = $this->createControllerDefinition(DebugController::class);
$container->setDefinition(DebugController::class, $definition);

Expand Down Expand Up @@ -319,7 +327,7 @@ private function loadHealthCheck(ContainerBuilder $container): void

if ($hasModule(MysqlModule::class)) {
$definition = new Definition(MysqlModule::class);
$definition->setArgument('$connection', new Reference(Connection::class));
$definition->setArgument('$connection', new Reference('database_connection'));
$definition->setArgument('$tableName', $healthCheck['mysql_table_name']);
$definition->addTag(AnzuSystemsCommonBundle::TAG_HEALTH_CHECK_MODULE);
$container->setDefinition(MysqlModule::class, $definition);
Expand Down Expand Up @@ -392,6 +400,12 @@ private function loadLogs(LoaderInterface $loader, ContainerBuilder $container):
->replaceArgument('$ignoredExceptions', $logs['app']['ignored_exceptions'])
;

$container
->getDefinition(ConsoleExceptionListener::class)
->replaceArgument('$logContextFactory', new Reference(LogContextFactory::class))
->replaceArgument('$ignoredExceptions', $logs['app']['ignored_exceptions'])
;

$appLogMongo = $logs['app']['mongo'];
$appLogClientDefinition = new Definition(MongoDB\Client::class);
$appLogClientDefinition->setArgument('$uri', $appLogMongo['uri']);
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ private function addSettingsSection(): NodeDefinition
->scalarNode('app_entity_namespace')->defaultValue('App\\Entity')->end()
->scalarNode('app_value_object_namespace')->defaultValue('App\\Model\\ValueObject')->end()
->scalarNode('app_enum_namespace')->defaultValue('App\\Model\\Enum')->end()
->booleanNode('send_context_id_with_response')->defaultFalse()->end()
->arrayNode('unlocked_commands')
->defaultValue(self::DEFAULT_UNLOCKED_COMMANDS)
->validate()
Expand Down
42 changes: 42 additions & 0 deletions src/Event/Listener/ConsoleExceptionListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace AnzuSystems\CommonBundle\Event\Listener;

use AnzuSystems\CommonBundle\Log\Factory\LogContextFactory;
use AnzuSystems\SerializerBundle\Exception\SerializerException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Event\ConsoleErrorEvent;

final class ConsoleExceptionListener
{
public function __construct(
private readonly LoggerInterface $appLogger,
private readonly array $ignoredExceptions = [],
private readonly ?LogContextFactory $logContextFactory = null,
) {
}

/**
* @throws SerializerException
*/
public function __invoke(ConsoleErrorEvent $event): void
{
$exception = $event->getError();
if (in_array($exception::class, $this->ignoredExceptions, true)) {
return;
}

$context = [];
if ($this->logContextFactory instanceof LogContextFactory) {
$context = $this->logContextFactory->buildFromConsoleErrorEventToArray($event);
}

$this->appLogger->critical(sprintf(
'[Command] [%s] %s',
(string) $event->getCommand()?->getName(),
$exception->getMessage()
), $context);
}
}
22 changes: 22 additions & 0 deletions src/Event/Listener/ContextIdOnResponseListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace AnzuSystems\CommonBundle\Event\Listener;

use AnzuSystems\CommonBundle\Kernel\AnzuKernel;
use AnzuSystems\Contracts\AnzuApp;
use Symfony\Component\HttpKernel\Event\ResponseEvent;

final class ContextIdOnResponseListener
{
public function __invoke(ResponseEvent $event): void
{
$response = $event->getResponse();
if ($response->headers->has(AnzuKernel::CONTEXT_IDENTITY_HEADER)) {
return;
}

$response->headers->set(AnzuKernel::CONTEXT_IDENTITY_HEADER, AnzuApp::getContextId());
}
}
22 changes: 22 additions & 0 deletions src/Log/Factory/LogContextFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use AnzuSystems\SerializerBundle\Exception\SerializerException;
use AnzuSystems\SerializerBundle\Serializer;
use JsonException;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

Expand Down Expand Up @@ -85,6 +86,27 @@ public function buildFromRequestToArray(Request $request, Response $response = n
);
}

/**
* @throws SerializerException
*/
public function buildFromConsoleErrorEventToArray(ConsoleErrorEvent $event): array
{
return $this->serializer->toArray(
$this->buildFromConsoleErrorEvent($event)
);
}

public function buildFromConsoleErrorEvent(ConsoleErrorEvent $event): LogContext
{
return $this->buildBaseContext()
->setContent((string) $event->getError())
->setPath((string) $event->getCommand()?->getName())
->setParams(['args' => $event->getInput()->getArguments(), 'opts' => $event->getInput()->getOptions()])
->setUserId((int) $this->userProvider->getCurrentUser()->getId())
->setHttpStatus($event->getExitCode())
;
}

public function buildCustomFromRequest(Request $request, LogDto $logDto): LogContext
{
return $this->buildBaseContext()
Expand Down
9 changes: 9 additions & 0 deletions src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use AnzuSystems\CommonBundle\Domain\Job\JobManager;
use AnzuSystems\CommonBundle\Domain\Job\JobProcessor;
use AnzuSystems\CommonBundle\Domain\User\CurrentAnzuUserProvider;
use AnzuSystems\CommonBundle\Event\Listener\ConsoleExceptionListener;
use AnzuSystems\CommonBundle\Event\Listener\ExceptionListener;
use AnzuSystems\CommonBundle\Event\Listener\LockReleaseListener;
use AnzuSystems\CommonBundle\Event\Subscriber\CommandLockSubscriber;
Expand All @@ -25,6 +26,7 @@
use AnzuSystems\CommonBundle\Validator\Validator;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
Expand Down Expand Up @@ -115,6 +117,13 @@
->tag('kernel.event_listener', ['event' => KernelEvents::EXCEPTION])
;

$services->set(ConsoleExceptionListener::class)
->arg('$ignoredExceptions', null)
->arg('$appLogger', service('monolog.logger'))
->arg('$logContextFactory', null)
->tag('kernel.event_listener', ['event' => ConsoleEvents::ERROR])
;

$services->set(Validator::class)
->arg('$validator', service(ValidatorInterface::class))
;
Expand Down
26 changes: 26 additions & 0 deletions tests/Controller/ContentIdOnResponseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace AnzuSystems\CommonBundle\Tests\Controller;

use AnzuSystems\CommonBundle\Kernel\AnzuKernel;
use AnzuSystems\Contracts\AnzuApp;
use JsonException;

final class ContentIdOnResponseTest extends AbstractControllerTest
{
/**
* @throws JsonException
*/
public function testContextId(): void
{
$this->get(uri: '/something');

$this->assertTrue(self::$client->getResponse()->headers->has(AnzuKernel::CONTEXT_IDENTITY_HEADER));
$this->assertSame(
expected: AnzuApp::getContextId(),
actual: self::$client->getResponse()->headers->get(AnzuKernel::CONTEXT_IDENTITY_HEADER),
);
}
}
1 change: 1 addition & 0 deletions tests/Controller/HealthCheckControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ public function testHealthCheck(): void
self::assertContains('redis', $moduleResultsKeys);
self::assertContains('dataMount', $moduleResultsKeys);
self::assertContains('mongo', $moduleResultsKeys);
self::assertContains('mysql', $moduleResultsKeys);
}
}
2 changes: 2 additions & 0 deletions tests/config/packages/anzu_systems_common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ anzu_systems_common:
user_entity_class: AnzuSystems\CommonBundle\Tests\Data\Entity\User
app_entity_namespace: AnzuSystems\CommonBundle\Tests\Data\Entity
app_value_object_namespace: AnzuSystems\CommonBundle\Tests\Data\Model\ValueObject
send_context_id_with_response: true
unlocked_commands:
- Symfony\Bundle\FrameworkBundle\Command\AssetsInstallCommand
- Symfony\Bundle\FrameworkBundle\Command\CacheWarmupCommand
Expand All @@ -19,6 +20,7 @@ anzu_systems_common:
- AnzuSystems\CommonBundle\HealthCheck\Module\RedisModule
- AnzuSystems\CommonBundle\HealthCheck\Module\DataMountModule
- AnzuSystems\CommonBundle\HealthCheck\Module\MongoModule
- AnzuSystems\CommonBundle\HealthCheck\Module\MysqlModule
errors:
enabled: true
default_exception_handler: AnzuSystems\CommonBundle\Exception\Handler\DefaultExceptionHandler
Expand Down

0 comments on commit c4c59ce

Please sign in to comment.