From e0948b24e4f0d5e41d4591deca14811bba243c08 Mon Sep 17 00:00:00 2001 From: Mykhailo Shtanko Date: Thu, 28 Mar 2024 10:06:38 +0200 Subject: [PATCH] [MetricsPower] Added ContextExtractor for logger [MetricsPower] Added ContextExtractor for logger --- .github/workflows/build.yml | 3 - Attribute/LoggerOptions.php | 12 +- Attribute/OptionsInterface.php | 5 +- Attribute/PrometheusOptions.php | 10 +- Attribute/SentryOptions.php | 6 + Handler/MetricsHandler.php | 4 +- .../ContextExtractorInterface.php | 37 ++++ .../DefaultContextExtractor.php | 71 ++++++++ .../ExceptionEventContextExtractor.php | 54 ++++++ ...ssageToTransportsEventContextExtractor.php | 63 +++++++ ...rkerMessageFailedEventContextExtractor.php | 71 ++++++++ ...kerMessageHandledEventContextExtractor.php | 38 +++++ ...erMessageReceivedEventContextExtractor.php | 38 +++++ ...kerMessageRetriedEventContextExtractor.php | 38 +++++ Logger/ContextExtractorLocator.php | 42 +++++ Logger/ContextExtractorLocatorInterface.php | 29 ++++ Logger/Data/Context.php | 27 +++ Logger/MetricsPowerLogger.php | 36 ++-- Logger/MetricsPowerLoggerInterface.php | 13 +- Logger/Traits/CanExtractContext.php | 49 ++++++ .../Resolver/LoggerOptionsResolver.php | 4 +- Resources/bundles.php | 11 ++ Resources/config/services.yaml | 4 +- .../Logger/ContextExtractorLocatorTest.php | 160 ++++++++++++++++++ .../Feature/Logger/MetricsPowerLoggerTest.php | 76 ++++++--- .../OptionsResolverLocatorTest.php | 4 +- .../OnWorkerMessageEventListenerTest.php | 11 +- Tests/Pest.php | 12 ++ Tests/Stub/TestConstants.php | 1 + .../Resolver/LoggerOptionResolverTest.php | 4 +- .../Resolver/PrometheusOptionResolverTest.php | 13 +- composer.json | 7 +- 32 files changed, 874 insertions(+), 79 deletions(-) create mode 100644 Logger/ContextExtractor/ContextExtractorInterface.php create mode 100644 Logger/ContextExtractor/DefaultContextExtractor.php create mode 100644 Logger/ContextExtractor/ExceptionEventContextExtractor.php create mode 100644 Logger/ContextExtractor/SendMessageToTransportsEventContextExtractor.php create mode 100644 Logger/ContextExtractor/WorkerMessageFailedEventContextExtractor.php create mode 100644 Logger/ContextExtractor/WorkerMessageHandledEventContextExtractor.php create mode 100644 Logger/ContextExtractor/WorkerMessageReceivedEventContextExtractor.php create mode 100644 Logger/ContextExtractor/WorkerMessageRetriedEventContextExtractor.php create mode 100644 Logger/ContextExtractorLocator.php create mode 100644 Logger/ContextExtractorLocatorInterface.php create mode 100644 Logger/Data/Context.php create mode 100644 Logger/Traits/CanExtractContext.php create mode 100644 Tests/Feature/Logger/ContextExtractorLocatorTest.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03537fa..8f91d13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,12 +32,9 @@ jobs: strategy: matrix: php: - - "8.1" - "8.2" - "8.3" include: - - php-version: "8.1" - composer-options: "--ignore-platform-reqs" - php-version: "8.2" composer-options: "--ignore-platform-reqs" - php-version: "8.3" diff --git a/Attribute/LoggerOptions.php b/Attribute/LoggerOptions.php index 92c219a..3101510 100644 --- a/Attribute/LoggerOptions.php +++ b/Attribute/LoggerOptions.php @@ -16,4 +16,14 @@ namespace FRZB\Component\MetricsPower\Attribute; #[\Attribute(\Attribute::TARGET_CLASS)] -final class LoggerOptions implements OptionsInterface {} +final class LoggerOptions implements OptionsInterface +{ + public function __construct( + public readonly bool $isSerializable = true, + ) {} + + public function isSerializable(): bool + { + return $this->isSerializable; + } +} diff --git a/Attribute/OptionsInterface.php b/Attribute/OptionsInterface.php index 76a17f9..c8e1bc1 100644 --- a/Attribute/OptionsInterface.php +++ b/Attribute/OptionsInterface.php @@ -15,4 +15,7 @@ namespace FRZB\Component\MetricsPower\Attribute; -interface OptionsInterface {} +interface OptionsInterface +{ + public function isSerializable(): bool; +} diff --git a/Attribute/PrometheusOptions.php b/Attribute/PrometheusOptions.php index 645d27f..24cf80b 100644 --- a/Attribute/PrometheusOptions.php +++ b/Attribute/PrometheusOptions.php @@ -15,8 +15,6 @@ namespace FRZB\Component\MetricsPower\Attribute; -use FRZB\Component\MetricsPower\Helper\MetricalHelper; - #[\Attribute(\Attribute::TARGET_CLASS)] final class PrometheusOptions implements OptionsInterface { @@ -28,7 +26,11 @@ public function __construct( public readonly string $help, public readonly array $labels, public readonly array $values, - ) { - $this->counterName = MetricalHelper::getCounterName($this); + public readonly bool $isSerializable = true, + ) {} + + public function isSerializable(): bool + { + return $this->isSerializable; } } diff --git a/Attribute/SentryOptions.php b/Attribute/SentryOptions.php index 17f2cdf..9995407 100644 --- a/Attribute/SentryOptions.php +++ b/Attribute/SentryOptions.php @@ -21,5 +21,11 @@ final class SentryOptions implements OptionsInterface public function __construct( public readonly bool $waitRetry = true, public readonly bool $onHandleFlush = true, + public readonly bool $isSerializable = true, ) {} + + public function isSerializable(): bool + { + return $this->isSerializable; + } } diff --git a/Handler/MetricsHandler.php b/Handler/MetricsHandler.php index cfe800d..c109f32 100644 --- a/Handler/MetricsHandler.php +++ b/Handler/MetricsHandler.php @@ -35,9 +35,9 @@ public function handle(AbstractWorkerMessageEvent|SendMessageToTransportsEvent $ foreach (MetricalHelper::getOptions($event->getEnvelope()->getMessage()) as $option) { try { $this->locator->get($option)($event, $option); - $this->logger->logInfo($event, $option); + $this->logger->info($event, $option); } catch (\Throwable $e) { - $this->logger->logError($event, $option, $e); + $this->logger->error($event, $option, $e); } } } diff --git a/Logger/ContextExtractor/ContextExtractorInterface.php b/Logger/ContextExtractor/ContextExtractorInterface.php new file mode 100644 index 0000000..6c2f392 --- /dev/null +++ b/Logger/ContextExtractor/ContextExtractorInterface.php @@ -0,0 +1,37 @@ + + */ +#[AsService, AsTagged(ContextExtractorInterface::class)] +class DefaultContextExtractor implements ContextExtractorInterface +{ + public const DEFAULT_TYPE = 'Default'; + private const MESSAGE_INFO = '[MetricsPower] [INFO] [MESSAGE: Operation succeed] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}]'; + private const MESSAGE_ERROR = '[MetricsPower] [ERROR] [MESSAGE: Operation failed] [OPTIONS_CLASS: {options_class}] [MESSAGE_CLASS: {message_class}] [EXCEPTION_CLASS: {exception_class}] [EXCEPTION_MESSAGE: {exception_message}] [OPTIONS_VALUES: {option_values}]'; + + public function __construct( + private readonly SerializerInterface $serializer, + ) {} + + public function extract(mixed $target, ?OptionsInterface $options = null, ?\Throwable $exception = null): Context + { + $message = $exception ? self::MESSAGE_ERROR : self::MESSAGE_INFO; + $context = ['target_class' => ClassHelper::getShortName($target)]; + + if ($options?->isSerializable()) { + $context += ['target_values' => $this->serializer->serialize($target, JsonEncoder::FORMAT)]; + } + + if ($options) { + $context += [ + 'options_class' => ClassHelper::getShortName($options), + 'option_values' => ClassHelper::getProperties($options), + ]; + } + + if ($exception) { + $context += [ + 'exception_class' => ClassHelper::getShortName($exception), + 'exception_message' => $exception->getMessage(), + 'exception_trace' => $exception->getTrace(), + ]; + } + + return new Context($message, $context); + } + + public static function getType(): string + { + return self::DEFAULT_TYPE; + } +} diff --git a/Logger/ContextExtractor/ExceptionEventContextExtractor.php b/Logger/ContextExtractor/ExceptionEventContextExtractor.php new file mode 100644 index 0000000..dc1315b --- /dev/null +++ b/Logger/ContextExtractor/ExceptionEventContextExtractor.php @@ -0,0 +1,54 @@ + + */ +#[AsService, AsTagged(ContextExtractorInterface::class)] +class ExceptionEventContextExtractor implements ContextExtractorInterface +{ + private const MESSAGE = '[MetricsPower] [ERROR] [MESSAGE: Operation failed] [TARGET_CLASS: {target_class}] [EXCEPTION_CLASS: {exception_class}] [EXCEPTION_MESSAGE: {exception_message}]'; + + public function extract(mixed $target, ?OptionsInterface $options = null, ?\Throwable $exception = null): Context + { + $context = [ + 'target_class' => ClassHelper::getShortName($target), + ]; + + if ($exception) { + $context += [ + 'exception_class' => ClassHelper::getShortName($target->getThrowable()), + 'exception_message' => $target->getThrowable()->getMessage(), + 'exception_trace' => $target->getThrowable()->getTrace(), + ]; + } + + return new Context(self::MESSAGE, $context); + } + + public static function getType(): string + { + return ExceptionEvent::class; + } +} diff --git a/Logger/ContextExtractor/SendMessageToTransportsEventContextExtractor.php b/Logger/ContextExtractor/SendMessageToTransportsEventContextExtractor.php new file mode 100644 index 0000000..d01af21 --- /dev/null +++ b/Logger/ContextExtractor/SendMessageToTransportsEventContextExtractor.php @@ -0,0 +1,63 @@ + + */ +#[AsService, AsTagged(ContextExtractorInterface::class)] +class SendMessageToTransportsEventContextExtractor implements ContextExtractorInterface +{ + private const MESSAGE = '[MetricsPower] [INFO] [MESSAGE: Sent to transport] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}] [MESSAGE_CLASS: {message_class}]'; + + public function __construct( + private readonly SerializerInterface $serializer, + ) {} + + public function extract(mixed $target, ?OptionsInterface $options = null, ?\Throwable $exception = null): Context + { + $context = [ + 'target_class' => ClassHelper::getShortName($target), + 'message_class' => ClassHelper::getShortName($target->getEnvelope()->getMessage()), + ]; + + if ($options?->isSerializable()) { + $context += ['message_values' => $this->serializer->serialize($target->getEnvelope()->getMessage(), JsonEncoder::FORMAT)]; + } + + if ($options) { + $context += [ + 'options_class' => ClassHelper::getShortName($options), + ]; + } + + return new Context(self::MESSAGE, $context); + } + + public static function getType(): string + { + return SendMessageToTransportsEvent::class; + } +} diff --git a/Logger/ContextExtractor/WorkerMessageFailedEventContextExtractor.php b/Logger/ContextExtractor/WorkerMessageFailedEventContextExtractor.php new file mode 100644 index 0000000..ef24bde --- /dev/null +++ b/Logger/ContextExtractor/WorkerMessageFailedEventContextExtractor.php @@ -0,0 +1,71 @@ + + */ +#[AsService, AsTagged(ContextExtractorInterface::class)] +final class WorkerMessageFailedEventContextExtractor implements ContextExtractorInterface +{ + private const MESSAGE = '[MetricsPower] [ERROR] [MESSAGE: Handle failed] [TARGET_CLASS: {target_class}] [OPTIONS_CLASS: {options_class}] [MESSAGE_CLASS: {message_class}] [EXCEPTION_CLASS: {exception_class}] [EXCEPTION_MESSAGE: {exception_message}] [OPTIONS_VALUES: {option_values}]'; + + public function __construct( + private readonly SerializerInterface $serializer, + ) {} + + public function extract(mixed $target, ?OptionsInterface $options = null, ?\Throwable $exception = null): Context + { + $context = [ + 'target_class' => ClassHelper::getShortName($target), + ]; + + if ($options?->isSerializable()) { + $context += ['target_values' => $this->serializer->serialize($target->getEnvelope()->getMessage(), JsonEncoder::FORMAT)]; + } + + if ($options) { + $context += [ + 'options_class' => ClassHelper::getShortName($options), + 'option_values' => ClassHelper::getProperties($options), + ]; + } + + if ($exception) { + $context += [ + 'exception_class' => ClassHelper::getShortName($exception), + 'exception_message' => $exception->getMessage(), + 'exception_trace' => $exception->getTrace(), + ]; + } + + return new Context(self::MESSAGE, $context); + } + + public static function getType(): string + { + return WorkerMessageFailedEvent::class; + } +} diff --git a/Logger/ContextExtractor/WorkerMessageHandledEventContextExtractor.php b/Logger/ContextExtractor/WorkerMessageHandledEventContextExtractor.php new file mode 100644 index 0000000..0c441aa --- /dev/null +++ b/Logger/ContextExtractor/WorkerMessageHandledEventContextExtractor.php @@ -0,0 +1,38 @@ + + */ +#[AsService, AsTagged(ContextExtractorInterface::class)] +final class WorkerMessageHandledEventContextExtractor implements ContextExtractorInterface +{ + use CanExtractContext; + + private const MESSAGE = '[MetricsPower] [INFO] [MESSAGE: Handle succeed] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}] [MESSAGE_CLASS: {message_class}]'; + + public static function getType(): string + { + return WorkerMessageHandledEvent::class; + } +} diff --git a/Logger/ContextExtractor/WorkerMessageReceivedEventContextExtractor.php b/Logger/ContextExtractor/WorkerMessageReceivedEventContextExtractor.php new file mode 100644 index 0000000..b7b24db --- /dev/null +++ b/Logger/ContextExtractor/WorkerMessageReceivedEventContextExtractor.php @@ -0,0 +1,38 @@ + + */ +#[AsService, AsTagged(ContextExtractorInterface::class)] +final class WorkerMessageReceivedEventContextExtractor implements ContextExtractorInterface +{ + use CanExtractContext; + + private const MESSAGE = '[MetricsPower] [INFO] [MESSAGE: Handle received] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}] [MESSAGE_CLASS: {message_class}]'; + + public static function getType(): string + { + return WorkerMessageReceivedEvent::class; + } +} diff --git a/Logger/ContextExtractor/WorkerMessageRetriedEventContextExtractor.php b/Logger/ContextExtractor/WorkerMessageRetriedEventContextExtractor.php new file mode 100644 index 0000000..3a304a0 --- /dev/null +++ b/Logger/ContextExtractor/WorkerMessageRetriedEventContextExtractor.php @@ -0,0 +1,38 @@ + + */ +#[AsService, AsTagged(ContextExtractorInterface::class)] +final class WorkerMessageRetriedEventContextExtractor implements ContextExtractorInterface +{ + use CanExtractContext; + + private const MESSAGE = '[MetricsPower] [INFO] [MESSAGE: Handle retried] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}] [MESSAGE_CLASS: {message_class}]'; + + public static function getType(): string + { + return WorkerMessageRetriedEvent::class; + } +} diff --git a/Logger/ContextExtractorLocator.php b/Logger/ContextExtractorLocator.php new file mode 100644 index 0000000..7137d53 --- /dev/null +++ b/Logger/ContextExtractorLocator.php @@ -0,0 +1,42 @@ + */ + private readonly HashMap $resolvers; + + public function __construct( + #[TaggedIterator(ContextExtractorInterface::class, defaultIndexMethod: 'getType')] + iterable $resolvers, + ) { + $this->resolvers = HashMap::collect($resolvers); + } + + public function get(object|string $target): ContextExtractorInterface + { + return $this->resolvers->get(\is_object($target) ? $target::class : $target)->get() + ?? $this->resolvers->get(DefaultContextExtractor::DEFAULT_TYPE)->getUnsafe(); + } +} diff --git a/Logger/ContextExtractorLocatorInterface.php b/Logger/ContextExtractorLocatorInterface.php new file mode 100644 index 0000000..f37a409 --- /dev/null +++ b/Logger/ContextExtractorLocatorInterface.php @@ -0,0 +1,29 @@ + + */ + public function get(object|string $target): ContextExtractorInterface; +} diff --git a/Logger/Data/Context.php b/Logger/Data/Context.php new file mode 100644 index 0000000..9b64cdc --- /dev/null +++ b/Logger/Data/Context.php @@ -0,0 +1,27 @@ + ClassHelper::getShortName($options), - 'message_class' => ClassHelper::getShortName($event->getEnvelope()->getMessage()), - ]; + $context = $this->contextExtractorLocator + ->get($target::class) + ->extract($target, $options); - $this->metricsPowerLogger->info(self::MESSAGE_INFO, $context); + $this->logger->info($context->message, $context->context); } - public function logError(AbstractWorkerMessageEvent|SendMessageToTransportsEvent $event, OptionsInterface $options, \Throwable $e): void + public function error(object $target, OptionsInterface $options, \Throwable $exception): void { - $context = [ - 'option_class' => ClassHelper::getShortName($options), - 'option_values' => ClassHelper::getProperties($options), - 'message_class' => ClassHelper::getShortName($event->getEnvelope()->getMessage()), - 'reason_message' => $e->getMessage(), - 'reason_trace' => $e->getTrace(), - ]; - - $this->metricsPowerLogger->error(self::MESSAGE_ERROR, $context); + $context = $this->contextExtractorLocator + ->get($target::class) + ->extract($target, $options, $exception); + + $this->logger->error($context->message, $context->context); } } diff --git a/Logger/MetricsPowerLoggerInterface.php b/Logger/MetricsPowerLoggerInterface.php index 9f4c3a6..2a28a84 100644 --- a/Logger/MetricsPowerLoggerInterface.php +++ b/Logger/MetricsPowerLoggerInterface.php @@ -1,5 +1,7 @@ ClassHelper::getShortName($target), + 'message_class' => ClassHelper::getShortName($target->getEnvelope()->getMessage()), + ]; + + if ($options?->isSerializable()) { + $context += ['message_values' => $this->serializer->serialize($target->getEnvelope()->getMessage(), JsonEncoder::FORMAT)]; + } + + if ($options) { + $context += [ + 'options_class' => ClassHelper::getShortName($options), + ]; + } + + return new Context(self::MESSAGE, $context); + } +} diff --git a/OptionsResolver/Resolver/LoggerOptionsResolver.php b/OptionsResolver/Resolver/LoggerOptionsResolver.php index 5e5e7e6..a27a5ac 100644 --- a/OptionsResolver/Resolver/LoggerOptionsResolver.php +++ b/OptionsResolver/Resolver/LoggerOptionsResolver.php @@ -34,8 +34,8 @@ public function __construct( public function __invoke(AbstractWorkerMessageEvent|SendMessageToTransportsEvent $event, OptionsInterface $options): void { match ($event::class) { - WorkerMessageFailedEvent::class => $this->logger->logError($event, $options, $event->getThrowable()), - default => $this->logger->logInfo($event, $options), + WorkerMessageFailedEvent::class => $this->logger->error($event, $options, $event->getThrowable()), + default => $this->logger->info($event, $options), }; } diff --git a/Resources/bundles.php b/Resources/bundles.php index 554660f..c326d2a 100644 --- a/Resources/bundles.php +++ b/Resources/bundles.php @@ -16,6 +16,17 @@ use FRZB\Component\MetricsPower\MetricsPowerBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +/** + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * + * Copyright (c) 2024 Mykhailo Shtanko fractalzombie@gmail.com + * + * For the full copyright and license information, please view the LICENSE.MD + * file that was distributed with this source code. + */ + return [ FrameworkBundle::class => ['all' => true], DependencyInjectionBundle::class => ['all' => true], diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml index 3804bc7..c933cba 100644 --- a/Resources/config/services.yaml +++ b/Resources/config/services.yaml @@ -7,8 +7,8 @@ services: &services autoconfigure: true FRZB\Component\MetricsPower\: - resource: '../../{Action,EventListener,Handler,Factory,TypeExtractor,Logger,OptionsResolver}/**' - exclude: '../../{Attribute,Configuration,DependencyInjection,Enum,Exception,Helper,Tests,Traits}/**' + resource: '../../{Action,EventListener,Handler,Factory,TypeExtractor,Logger,OptionsResolver}' + exclude: '../../**/{Attribute,Configuration,Data,DependencyInjection,Enum,Exception,Helper,Tests,Traits}' when@test: services: diff --git a/Tests/Feature/Logger/ContextExtractorLocatorTest.php b/Tests/Feature/Logger/ContextExtractorLocatorTest.php new file mode 100644 index 0000000..f89c315 --- /dev/null +++ b/Tests/Feature/Logger/ContextExtractorLocatorTest.php @@ -0,0 +1,160 @@ +ensureKernelShutdown(); + $this->bootKernel(); +}); + +test( + 'It can extract event with custom extractor', + function (object $target, OptionsInterface $options, string $extractorType, string $expectedMessage, array $expectedContext): void { + $extractor = $this->getContainer() + ->get(ContextExtractorLocatorInterface::class) + ->get($target); + + $context = $extractor->extract($target, $options); + + expect() + ->and($extractor::getType())->toBe($extractorType) + ->and($context->message)->toBe($expectedMessage) + ->and($context->context)->toBe($expectedContext); + } +)->with(function () { + $kernel = \Mockery::mock(HttpKernelInterface::class); + $request = createRequest(); + + yield ClassHelper::getShortName(ExceptionEvent::class) => [ + 'event' => new ExceptionEvent( + $kernel, + $request, + HttpKernelInterface::MAIN_REQUEST, + SomethingGoesWrongException::wrong() + ), + 'options' => new LoggerOptions(), + 'extractor_type' => ExceptionEvent::class, + 'expected_message' => '[MetricsPower] [ERROR] [MESSAGE: Operation failed] [TARGET_CLASS: {target_class}] [EXCEPTION_CLASS: {exception_class}] [EXCEPTION_MESSAGE: {exception_message}]', + 'expected_context' => [ + 'target_class' => 'ExceptionEvent', + ], + ]; + + yield ClassHelper::getShortName(SendMessageToTransportsEvent::class) => [ + 'event' => new SendMessageToTransportsEvent(createTestEnvelope(), [TestConstants::DEFAULT_RECEIVER_NAME]), + 'options' => new LoggerOptions(), + 'extractor_type' => SendMessageToTransportsEvent::class, + 'expected_message' => '[MetricsPower] [INFO] [MESSAGE: Sent to transport] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}] [MESSAGE_CLASS: {message_class}]', + 'expected_context' => [ + 'target_class' => 'SendMessageToTransportsEvent', + 'message_class' => 'TestMessage', + 'message_values' => '{"id":"ID-1234"}', + 'options_class' => 'LoggerOptions', + ], + ]; + + yield ClassHelper::getShortName(WorkerMessageFailedEvent::class) => [ + 'event' => new WorkerMessageFailedEvent( + createTestEnvelope(), + TestConstants::DEFAULT_RECEIVER_NAME, + SomethingGoesWrongException::wrong() + ), + 'options' => new LoggerOptions(), + 'extractor_type' => WorkerMessageFailedEvent::class, + 'expected_message' => '[MetricsPower] [ERROR] [MESSAGE: Handle failed] [TARGET_CLASS: {target_class}] [OPTIONS_CLASS: {options_class}] [MESSAGE_CLASS: {message_class}] [EXCEPTION_CLASS: {exception_class}] [EXCEPTION_MESSAGE: {exception_message}] [OPTIONS_VALUES: {option_values}]', + 'expected_context' => [ + 'target_class' => 'WorkerMessageFailedEvent', + 'target_values' => '{"id":"ID-1234"}', + 'options_class' => 'LoggerOptions', + 'option_values' => ['isSerializable' => true], + ], + ]; + + yield ClassHelper::getShortName(WorkerMessageHandledEvent::class) => [ + 'event' => new WorkerMessageHandledEvent(createTestEnvelope(), TestConstants::DEFAULT_RECEIVER_NAME), + 'options' => new LoggerOptions(), + 'extractor_type' => WorkerMessageHandledEvent::class, + 'expected_message' => '[MetricsPower] [INFO] [MESSAGE: Handle succeed] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}] [MESSAGE_CLASS: {message_class}]', + 'expected_context' => [ + 'target_class' => 'WorkerMessageHandledEvent', + 'message_class' => 'TestMessage', + 'message_values' => '{"id":"ID-1234"}', + 'options_class' => 'LoggerOptions', + ], + ]; + + yield ClassHelper::getShortName(WorkerMessageReceivedEvent::class) => [ + 'event' => new WorkerMessageReceivedEvent(createTestEnvelope(), TestConstants::DEFAULT_RECEIVER_NAME), + 'options' => new LoggerOptions(), + 'extractor_type' => WorkerMessageReceivedEvent::class, + 'expected_message' => '[MetricsPower] [INFO] [MESSAGE: Handle received] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}] [MESSAGE_CLASS: {message_class}]', + 'expected_context' => [ + 'target_class' => 'WorkerMessageReceivedEvent', + 'message_class' => 'TestMessage', + 'message_values' => '{"id":"ID-1234"}', + 'options_class' => 'LoggerOptions', + ], + ]; + + yield ClassHelper::getShortName(WorkerMessageRetriedEvent::class) => [ + 'event' => new WorkerMessageRetriedEvent(createTestEnvelope(), TestConstants::DEFAULT_RECEIVER_NAME), + 'options' => new LoggerOptions(), + 'extractor_type' => WorkerMessageRetriedEvent::class, + 'expected_message' => '[MetricsPower] [INFO] [MESSAGE: Handle retried] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}] [MESSAGE_CLASS: {message_class}]', + 'expected_context' => [ + 'target_class' => 'WorkerMessageRetriedEvent', + 'message_class' => 'TestMessage', + 'message_values' => '{"id":"ID-1234"}', + 'options_class' => 'LoggerOptions', + ], + ]; +}); + +test('It can extract any event', function (): void { + $contextExtractorLocator = $this->getContainer()->get(ContextExtractorLocatorInterface::class); + $request = createRequest(); + $target = new RequestEvent(\Mockery::mock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); + + $extractor = $contextExtractorLocator->get($target); + + $context = $extractor->extract($target, new LoggerOptions()); + + expect() + ->and($extractor::getType())->toBe('Default') + ->and($context->message)->toBe('[MetricsPower] [INFO] [MESSAGE: Operation succeed] [OPTIONS_CLASS: {options_class}] [TARGET_CLASS: {target_class}]') + ->and($context->context)->toHaveKey('target_class') + ->and($context->context)->toHaveKey('target_values') + ->and($context->context['target_class'])->toBe('RequestEvent') + ->and($context->context['target_values'])->toBeJson(); +}); diff --git a/Tests/Feature/Logger/MetricsPowerLoggerTest.php b/Tests/Feature/Logger/MetricsPowerLoggerTest.php index efc7f3e..b550c31 100644 --- a/Tests/Feature/Logger/MetricsPowerLoggerTest.php +++ b/Tests/Feature/Logger/MetricsPowerLoggerTest.php @@ -17,6 +17,9 @@ use FRZB\Component\MetricsPower\Helper\ClassHelper; use FRZB\Component\MetricsPower\Helper\MetricalHelper; +use FRZB\Component\MetricsPower\Logger\ContextExtractor\ContextExtractorInterface; +use FRZB\Component\MetricsPower\Logger\ContextExtractorLocatorInterface; +use FRZB\Component\MetricsPower\Logger\Data\Context; use FRZB\Component\MetricsPower\Logger\MetricsPowerLogger; use FRZB\Component\MetricsPower\Tests\Stub\Exception\SomethingGoesWrongException; use FRZB\Component\MetricsPower\Tests\Stub\TestConstants; @@ -33,48 +36,81 @@ test('It can map info log', function (): void { $decoratedLogger = \Mockery::mock(LoggerInterface::class); - $metricsPowerLogger = new MetricsPowerLogger($decoratedLogger); + $contextExtractor = \Mockery::mock(ContextExtractorInterface::class); + $contextExtractorLocator = \Mockery::mock(ContextExtractorLocatorInterface::class); + $metricsPowerLogger = new MetricsPowerLogger($contextExtractorLocator, $decoratedLogger); $message = createTestMessage(); $envelope = createTestEnvelope($message); $options = MetricalHelper::getFirstOptions($message); $event = new WorkerMessageHandledEvent($envelope, TestConstants::DEFAULT_RECEIVER_NAME); + $context = new Context( + '[MetricsPower] [ERROR] [OPTIONS_CLASS: {option_class}] Metrics registration failed for [MESSAGE_CLASS: {message_class}] [REASON: {reason_message}] [OPTIONS_VALUES: {option_values}]', + [ + 'option_class' => ClassHelper::getShortName($options), + 'message_class' => ClassHelper::getShortName($message), + ] + ); + + $contextExtractor + ->expects('extract') + ->once() + ->andReturn($context); + + $contextExtractorLocator + ->expects('get') + ->once() + ->andReturn($contextExtractor); $decoratedLogger->expects('info') ->once() - ->andReturnUsing(function (string $logMessage, array $logContext) use ($options, $message): void { + ->andReturnUsing(function (string $logMessage, array $logContext) use ($context): void { expect() - ->and($logMessage)->toBe('[MetricsPower] [INFO] [OPTIONS_CLASS: {option_class}] Metrics registration success for [MESSAGE_CLASS: {message_class}]') - ->and($logContext)->toBe([ - 'option_class' => ClassHelper::getShortName($options), - 'message_class' => ClassHelper::getShortName($message), - ]); + ->and($logMessage)->toBe($context->message) + ->and($logContext)->toBe($context->context); }); - $metricsPowerLogger->logInfo($event, $options); + $metricsPowerLogger->info($event, $options); }); test('It can map error log', function (): void { $decoratedLogger = \Mockery::mock(LoggerInterface::class); - $metricsPowerLogger = new MetricsPowerLogger($decoratedLogger); + $contextExtractor = \Mockery::mock(ContextExtractorInterface::class); + $contextExtractorLocator = \Mockery::mock(ContextExtractorLocatorInterface::class); + $metricsPowerLogger = new MetricsPowerLogger($contextExtractorLocator, $decoratedLogger); $message = createTestMessage(); $envelope = createTestEnvelope($message); $exception = SomethingGoesWrongException::wrong(); $options = MetricalHelper::getFirstOptions($message); $event = new WorkerMessageHandledEvent($envelope, TestConstants::DEFAULT_RECEIVER_NAME); + $context = new Context( + '[MetricsPower] [ERROR] [OPTIONS_CLASS: {option_class}] Metrics registration failed for [MESSAGE_CLASS: {message_class}] [REASON: {reason_message}] [OPTIONS_VALUES: {option_values}]', + [ + 'option_class' => ClassHelper::getShortName($options), + 'option_values' => ClassHelper::getProperties($options), + 'message_class' => ClassHelper::getShortName($event->getEnvelope()->getMessage()), + 'reason_message' => $exception->getMessage(), + 'reason_trace' => $exception->getTrace(), + ] + ); + + $contextExtractor + ->expects('extract') + ->once() + ->andReturn($context); + + $contextExtractorLocator + ->expects('get') + ->once() + ->andReturn($contextExtractor); - $decoratedLogger->expects('error') + $decoratedLogger + ->expects('error') ->once() - ->andReturnUsing(function (string $logMessage, array $logContext) use ($options, $event, $exception): void { + ->andReturnUsing(function (string $logMessage, array $logContext) use ($context): void { expect() - ->and($logMessage)->toBe('[MetricsPower] [ERROR] [OPTIONS_CLASS: {option_class}] Metrics registration failed for [MESSAGE_CLASS: {message_class}] [REASON: {reason_message}] [OPTIONS_VALUES: {option_values}]') - ->and($logContext)->toBe([ - 'option_class' => ClassHelper::getShortName($options), - 'option_values' => ClassHelper::getProperties($options), - 'message_class' => ClassHelper::getShortName($event->getEnvelope()->getMessage()), - 'reason_message' => $exception->getMessage(), - 'reason_trace' => $exception->getTrace(), - ]); + ->and($logMessage)->toBe($context->message) + ->and($logContext)->toBe($context->context); }); - $metricsPowerLogger->logError($event, $options, $exception); + $metricsPowerLogger->error($event, $options, $exception); }); diff --git a/Tests/Feature/OptionResolver/OptionsResolverLocatorTest.php b/Tests/Feature/OptionResolver/OptionsResolverLocatorTest.php index 6025429..ed8d290 100644 --- a/Tests/Feature/OptionResolver/OptionsResolverLocatorTest.php +++ b/Tests/Feature/OptionResolver/OptionsResolverLocatorTest.php @@ -67,8 +67,8 @@ $options = $metrical->options[0]; $logger = \Mockery::mock(MetricsPowerLoggerInterface::class); - $logger->expects('logInfo')->once(); - $logger->expects('logError')->once(); + $logger->expects('info')->once(); + $logger->expects('error')->once(); $defaultOptionResolver = new DefaultOptionsResolver($logger); $this->getContainer()->set(PrometheusOptionsResolver::class, $defaultOptionResolver); diff --git a/Tests/Feature/Prometheus/OnWorkerMessageEventListenerTest.php b/Tests/Feature/Prometheus/OnWorkerMessageEventListenerTest.php index bc9fd9e..48d1dbc 100644 --- a/Tests/Feature/Prometheus/OnWorkerMessageEventListenerTest.php +++ b/Tests/Feature/Prometheus/OnWorkerMessageEventListenerTest.php @@ -17,6 +17,7 @@ use FRZB\Component\MetricsPower\EventListener\Prometheus\OnWorkerMessageEventListener; use FRZB\Component\MetricsPower\Handler\MetricsHandlerInterface; +use FRZB\Component\MetricsPower\Helper\ClassHelper; use FRZB\Component\MetricsPower\Tests\Stub\Exception\SomethingGoesWrongException; use FRZB\Component\MetricsPower\Tests\Stub\TestConstants; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -44,23 +45,23 @@ $this->getContainer()->get(OnWorkerMessageEventListener::class)($event); })->with(function () { - yield WorkerMessageHandledEvent::class => [ + yield ClassHelper::getShortName(WorkerMessageHandledEvent::class) => [ 'event' => new WorkerMessageHandledEvent(createTestEnvelope(), TestConstants::DEFAULT_RECEIVER_NAME), ]; - yield WorkerMessageReceivedEvent::class => [ + yield ClassHelper::getShortName(WorkerMessageReceivedEvent::class) => [ 'event' => new WorkerMessageReceivedEvent(createTestEnvelope(), TestConstants::DEFAULT_RECEIVER_NAME), ]; - yield WorkerMessageRetriedEvent::class => [ + yield ClassHelper::getShortName(WorkerMessageRetriedEvent::class) => [ 'event' => new WorkerMessageRetriedEvent(createTestEnvelope(), TestConstants::DEFAULT_RECEIVER_NAME), ]; - yield WorkerMessageFailedEvent::class => [ + yield ClassHelper::getShortName(WorkerMessageFailedEvent::class) => [ 'event' => new WorkerMessageFailedEvent(createTestEnvelope(), TestConstants::DEFAULT_RECEIVER_NAME, SomethingGoesWrongException::wrong()), ]; - yield SendMessageToTransportsEvent::class => [ + yield ClassHelper::getShortName(SendMessageToTransportsEvent::class) => [ 'event' => new SendMessageToTransportsEvent(createTestEnvelope(), [TestConstants::DEFAULT_RECEIVER_NAME]), ]; }); diff --git a/Tests/Pest.php b/Tests/Pest.php index 3e57622..adf32bb 100644 --- a/Tests/Pest.php +++ b/Tests/Pest.php @@ -18,6 +18,8 @@ use Mockery\LegacyMockInterface; use Mockery\MockInterface; use Sentry\State\HubInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Messenger\Envelope; // uses(KernelTestCase::class)->in('Feature'); @@ -67,3 +69,13 @@ function createTestEnvelope(?object $message = null): Envelope { return EnvelopeHelper::wrap($message ?? createTestMessage()); } + +function createRequest(): Request +{ + $request = Request::createFromGlobals(); + + $request + ->setSession(new Session()); + + return $request; +} diff --git a/Tests/Stub/TestConstants.php b/Tests/Stub/TestConstants.php index a676fb8..02df9a2 100644 --- a/Tests/Stub/TestConstants.php +++ b/Tests/Stub/TestConstants.php @@ -19,6 +19,7 @@ interface TestConstants { public const DEFAULT_ID = 'ID-1234'; + public const DEFAULT_NAME = 'NAME-1234'; public const DEFAULT_RECEIVER_NAME = 'test-receiver'; public const DEFAULT_NAMESPACE = 'test-namespace'; } diff --git a/Tests/Unit/OptionsResolver/Resolver/LoggerOptionResolverTest.php b/Tests/Unit/OptionsResolver/Resolver/LoggerOptionResolverTest.php index 2b01a7e..f2459df 100644 --- a/Tests/Unit/OptionsResolver/Resolver/LoggerOptionResolverTest.php +++ b/Tests/Unit/OptionsResolver/Resolver/LoggerOptionResolverTest.php @@ -32,7 +32,7 @@ $options = new LoggerOptions(); $logger - ->expects('logError') + ->expects('error') ->once(); $loggerOptionsResolver($event, $options); @@ -46,7 +46,7 @@ $options = new LoggerOptions(); $logger - ->expects('logInfo') + ->expects('info') ->once(); $loggerOptionsResolver($event, $options); diff --git a/Tests/Unit/OptionsResolver/Resolver/PrometheusOptionResolverTest.php b/Tests/Unit/OptionsResolver/Resolver/PrometheusOptionResolverTest.php index 3462f50..b6cbebb 100644 --- a/Tests/Unit/OptionsResolver/Resolver/PrometheusOptionResolverTest.php +++ b/Tests/Unit/OptionsResolver/Resolver/PrometheusOptionResolverTest.php @@ -17,6 +17,7 @@ use FRZB\Component\MetricsPower\Attribute\PrometheusOptions; use FRZB\Component\MetricsPower\Exception\MetricsRegistrationException; +use FRZB\Component\MetricsPower\Helper\ClassHelper; use FRZB\Component\MetricsPower\OptionsResolver\Resolver\PrometheusOptionsResolver; use FRZB\Component\MetricsPower\Tests\Stub\Exception\SomethingGoesWrongException; use FRZB\Component\MetricsPower\Tests\Stub\TestConstants; @@ -63,32 +64,32 @@ })->with(function () { $envelope = createTestEnvelope(); - yield WorkerMessageHandledEvent::class => [ + yield ClassHelper::getShortName(WorkerMessageHandledEvent::class) => [ 'counter_name' => 'test_receiver_test_name_handled', 'event' => new WorkerMessageHandledEvent($envelope, TestConstants::DEFAULT_RECEIVER_NAME), ]; - yield WorkerMessageReceivedEvent::class => [ + yield ClassHelper::getShortName(WorkerMessageReceivedEvent::class) => [ 'counter_name' => 'test_receiver_test_name_received', 'event' => new WorkerMessageReceivedEvent($envelope, TestConstants::DEFAULT_RECEIVER_NAME), ]; - yield WorkerMessageRetriedEvent::class => [ + yield ClassHelper::getShortName(WorkerMessageRetriedEvent::class) => [ 'counter_name' => 'test_receiver_test_name_retried', 'event' => new WorkerMessageRetriedEvent($envelope, TestConstants::DEFAULT_RECEIVER_NAME), ]; - yield WorkerMessageFailedEvent::class => [ + yield ClassHelper::getShortName(WorkerMessageFailedEvent::class) => [ 'counter_name' => 'test_receiver_test_name_failed', 'event' => new WorkerMessageFailedEvent($envelope, TestConstants::DEFAULT_RECEIVER_NAME, SomethingGoesWrongException::wrong()), ]; - yield SendMessageToTransportsEvent::class => [ + yield ClassHelper::getShortName(SendMessageToTransportsEvent::class) => [ 'counter_name' => 'test_receiver_test_name_sent', 'event' => new SendMessageToTransportsEvent($envelope, [TestConstants::DEFAULT_RECEIVER_NAME]), ]; - yield sprintf('%s with exception', WorkerMessageFailedEvent::class) => [ + yield sprintf('%s with exception', ClassHelper::getShortName(WorkerMessageFailedEvent::class)) => [ 'counter_name' => 'test_receiver_test_name_failed', 'event' => new WorkerMessageFailedEvent($envelope, TestConstants::DEFAULT_RECEIVER_NAME, SomethingGoesWrongException::wrong()), 'throws' => true, diff --git a/composer.json b/composer.json index b199bc8..cee6b3c 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "fp4php/functional": "^6.0", "promphp/prometheus_client_php": "^v2.10", "symfony/http-kernel": "^6|^7", @@ -30,7 +30,10 @@ "frzb/dependency-injection": "^2.1", "symfony/yaml": "^6|^7", "symfony/messenger": "^6|^7", - "sentry/sdk": "^4.0" + "sentry/sdk": "^4.0", + "symfony/serializer": "^6|^7", + "symfony/serializer-pack": "^1.3" + }, "require-dev": { "phpunit/phpunit": "^10.5",