Skip to content

Commit

Permalink
NEXT-36658 - Add system metrics structure
Browse files Browse the repository at this point in the history
Co-authored-by: Andrii Havryliuk <[email protected]>
Co-authored-by: Ghaith Olabi <[email protected]>
  • Loading branch information
Gaitholabi and h1k3r committed Aug 8, 2024
1 parent e8f4d27 commit 4fbdffe
Show file tree
Hide file tree
Showing 57 changed files with 2,210 additions and 14 deletions.
40 changes: 40 additions & 0 deletions .danger.php
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,32 @@ function checkMigrationForBundle(string $bundle, Context $context): void
$missingUnitTests = [];
$unitTestsName = [];

// prepare phpunit code coverage exclude lists
$phpUnitConfig = __DIR__ . '/phpunit.xml.dist';
$excludedDirs = [];
$excludedFiles = [];
$dom = new \DOMDocument();
$loaded = $dom->load($phpUnitConfig);
if ($loaded) {
$xpath = new \DOMXPath($dom);
$dirsDomElements = $xpath->query('//source/exclude/directory');

foreach ($dirsDomElements as $dirDomElement) {
$excludedDirs[] = [
'path'=> rtrim($dirDomElement->nodeValue, '/') . '/',
'suffix' => $dirDomElement->getAttribute('suffix') ?: '',
];
}

$filesDomElements = $xpath->query('//source/exclude/file');

foreach ($filesDomElements as $fileDomElements) {
$excludedFiles[] = $fileDomElements->nodeValue;
}
} else {
$context->warning(sprintf('Was not able to load phpunit config file %s. Please check configuration.', $phpUnitConfig));
}

foreach ($addedUnitTests as $file) {
$content = $file->getContent();

Expand Down Expand Up @@ -439,6 +465,20 @@ function checkMigrationForBundle(string $bundle, Context $context): void
continue;
}

// process phpunit code coverage exclude lists
if (in_array($file->name, $excludedFiles, true)) {
continue;
}

$dir = dirname($file->name);
$fileName = basename($file->name);

foreach ($excludedDirs as $excludedDir) {
if (str_starts_with($dir, $excludedDir['path']) && str_ends_with($fileName, $excludedDir['suffix'])) {
continue 2;
}
}

$ignoreSuffixes = [
'Entity',
'Collection',
Expand Down
2 changes: 1 addition & 1 deletion .gitlab/stages/10-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Danger:
extends: .base-no-setup
stage: lint
image:
name: ghcr.io/shyim/danger-php:0.3.0
name: ghcr.io/shyim/danger-php:0.3.4
entrypoint: [""]
rules:
- !reference [.rules, skip]
Expand Down
72 changes: 72 additions & 0 deletions adr/2024-07-30-add-telemetry-abstraction-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: Telemetry abstraction layer
date: 2024-07-30
area: core
tags: [core, profile, performance, datadog, metrics, monitoring]
---
## Context

Observability is a key aspect of modern software development. It is essential to have the right tools in place to monitor and analyze runtime statistics of the application.

Many tools and backends are available to enable telemetry and monitoring. The context of this ADR is to provide a streamlined and simple way to enable the integration of any observability tool into the Shopware platform.

## Decision

To address the need for a unified way to track metrics and performance data, we will introduce a telemetry abstraction layer. This layer will provide a common interface for integrating different monitoring tools into the Shopware platform.

The telemetry abstraction layer will consist of the following components:

### Shopware's abstraction layer

The abstraction layer will provide a common interface for telemetry integration. It will define the methods and data structures required to send telemetry data to the monitoring backend.

### Events subsystem attachment

The telemetry abstraction layer will be integrated with the existing events subsystem. This integration will allow developers to hook into specific events and capture telemetry data related to those events.

### Transport layer (integrations)

Vendor specific implementation will not be part of the core. Those would be shipped as external libraries that implement the telemetry abstraction layer specification. The core will provide documentation on how to integrate these libraries into the Shopware platform.

Each transport layer should at least be aware of the following metrics objects:
- `Shopware\Core\Framework\Telemetry\Metrics\Metric\Counter`
- `Shopware\Core\Framework\Telemetry\Metrics\Metric\Gauge`
- `Shopware\Core\Framework\Telemetry\Metrics\Metric\Histogram`
- `Shopware\Core\Framework\Telemetry\Metrics\Metric\UpDownCounter`

Or more generally, should aim to cover all the metric types defined inside the `Shopware\Core\Framework\Telemetry\Metrics\Metric` namespace.

### Implementation and Considerations

Each transport should implement the `MetricTransportInterface`. This interface defines a method `emit` that takes a `MetricInterface` object as an argument. The `MetricInterface` object represents a single metric that needs to be sent to the monitoring backend.

If an instance of an unsupported metric type is passed to the transport, it should throw a `MetricNotSupportedException`. This ensures that the transport layer is decoupled from the core and can be extended to support new metric types in the future.

> `MetricNotSupportedException` is gracefully handled, and the application will skip over the unsupported metric type.
```php
interface MetricTransportInterface
{
/**
* @throws MetricNotSupportedException
*/
public function emit(MetricInterface $metric): void;}
```

The `MetricInterface` is a generic empty interface. This approach provides flexibility for different monitoring tools to define their own metric structures alongside the core ones.

```php
interface MetricInterface
{
}
```


## Consequences

By implementing a telemetry abstraction layer, we provide a unified way to integrate monitoring tools into the Shopware platform. This approach simplifies the process of adding telemetry to the application and ensures consistency across different monitoring tools.


## Usage

See [README.md](../src/Core/Framework/Telemetry/README.md) for the implementation and usage details.
16 changes: 16 additions & 0 deletions changelog/_unreleased/2024-08-05-metrics-abstraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: Metrics abstraction
issue: NEXT-36658
flag: TELEMETRY_METRICS

---
# Core
* Added a new metrics abstraction layer `Shopware\Core\Framework\Telemetry` to the core. This layer allows collecting metrics from different sources and sending them to different targets. See documentation in the package folder for more details.
* Added `TELEMETRY_METRICS` feature flag to enable/disable metrics collection.
* Changed default system event dispatcher to the `MetricEventDispatcher` to listen on all events and this way support metrics collection for system events.
* Changed attributes of `Shopware\Core\Framework\Adapter\Cache\InvalidateCacheEvent` to enable emitting of `cache.invalidate` metric.
* Changed attributes of `Shopware\Core\Framework\App\Event\AppInstalledEvent` to enable emitting of `app.install` metric.
* Changed attributes of `Shopware\Core\Framework\Plugin\Event\PluginPostInstallEvent` to enable emitting of `plugin.install` metric.
* Changed `Shopware\Core\Framework\MessageQueue\Subscriber\MessageQueueSubscriber` to listen on `onMessageReceived` event and emit `messenger.message.size` metric.
* Added `Shopware\Core\Framework\DataAbstractionLayer\Subscriber\EntityStatsSubscriber` to listen on `onEntitySearched` event and emit `dal.association.count` metric.
* Changed `Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery` and `Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableTransaction` to emit `database.locked` metric.
5 changes: 5 additions & 0 deletions config/services_test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,10 @@
<service id="Shopware\Tests\Integration\Core\Framework\DataAbstractionLayer\fixture\AttributeEntityAgg">
<tag name="shopware.entity"/>
</service>


<service id="Shopware\Core\Framework\Test\Telemetry\Transport\TraceableTransport">
<tag name="shopware.metric_transport"/>
</service>
</services>
</container>
10 changes: 0 additions & 10 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2320,16 +2320,6 @@ parameters:
count: 1
path: src/Core/DevOps/Locust/setup.php

-
message: "#^Method Shopware\\\\Core\\\\Framework\\\\Adapter\\\\Cache\\\\InvalidateCacheEvent\\:\\:__construct\\(\\) has parameter \\$keys with no value type specified in iterable type array\\.$#"
count: 1
path: src/Core/Framework/Adapter/Cache/InvalidateCacheEvent.php

-
message: "#^Method Shopware\\\\Core\\\\Framework\\\\Adapter\\\\Cache\\\\InvalidateCacheEvent\\:\\:getKeys\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Core/Framework/Adapter/Cache/InvalidateCacheEvent.php

-
message: "#^Method Shopware\\\\Core\\\\Framework\\\\Adapter\\\\Cache\\\\StoreApiRouteCacheTagsEvent\\:\\:__construct\\(\\) has parameter \\$tags with no value type specified in iterable type array\\.$#"
count: 1
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<file>src/Core/Framework/Adapter/Twig/functions.php</file>
<directory suffix=".php">src/Core/Test/Integration/Builder</directory>
<directory suffix=".php">src/Core/Framework/Test</directory>
<directory suffix=".php">src/Core/Framework/Telemetry/Metrics/Metric</directory>
<directory suffix=".php">src/Core/Content/Test</directory>
<directory suffix=".php">src/Core/DevOps/StaticAnalyze</directory>
<directory suffix=".php">src/Core/DevOps/Test/Command/stubs</directory>
Expand Down
8 changes: 8 additions & 0 deletions src/Core/Framework/Adapter/Cache/InvalidateCacheEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
namespace Shopware\Core\Framework\Adapter\Cache;

use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Telemetry\Metrics\Attribute\Counter;
use Symfony\Contracts\EventDispatcher\Event;

#[Package('core')]
#[Counter(name: 'cache.invalidate', value: 1, description: 'Number of cache invalidations')]
class InvalidateCacheEvent extends Event
{
/**
* @param array<string> $keys
*/
public function __construct(protected array $keys)
{
}

/**
* @return array<string>
*/
public function getKeys(): array
{
return $this->keys;
Expand Down
2 changes: 2 additions & 0 deletions src/Core/Framework/App/Event/AppInstalledEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
namespace Shopware\Core\Framework\App\Event;

use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Telemetry\Metrics\Attribute\Counter;

/**
* @final
*/
#[Package('core')]
#[Counter(name: 'app.install', value: 1, description: 'Number of app installations')]
class AppInstalledEvent extends ManifestChangedEvent
{
final public const NAME = 'app.installed';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use Doctrine\DBAL\Exception\RetryableException;
use Doctrine\DBAL\Statement;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Telemetry\Metrics\MeterProvider;
use Shopware\Core\Framework\Telemetry\Metrics\Metric\Counter;

#[Package('core')]
class RetryableQuery
Expand Down Expand Up @@ -55,6 +57,7 @@ private static function retry(?Connection $connection, \Closure $closure, int $c
try {
return $closure();
} catch (RetryableException $e) {
MeterProvider::meter()?->emit(new Counter('database.locked', 1, 'Number of database write locks'));
if ($connection && $connection->getTransactionNestingLevel() > 0) {
// If this closure was executed inside a transaction, do not retry. Remember that the whole (outermost)
// transaction was already rolled back by the database when any RetryableException is thrown. Rethrow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\RetryableException;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Telemetry\Metrics\MeterProvider;
use Shopware\Core\Framework\Telemetry\Metrics\Metric\Counter;

#[Package('core')]
class RetryableTransaction
Expand Down Expand Up @@ -39,6 +41,7 @@ private static function retry(Connection $connection, \Closure $closure, int $co
try {
return $connection->transactional($closure);
} catch (RetryableException $retryableException) {
MeterProvider::meter()?->emit(new Counter('database.locked', 1, 'Number of database write locks'));
if ($connection->getTransactionNestingLevel() > 0) {
// If this RetryableTransaction was executed inside another transaction, do not retry this nested
// transaction. Remember that the whole (outermost) transaction was already rolled back by the database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function __construct(
private readonly EntitySearcherInterface $searcher,
private readonly EntityAggregatorInterface $aggregator,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly EntityLoadedEventFactory $eventFactory
private readonly EntityLoadedEventFactory $eventFactory,
) {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Framework\DataAbstractionLayer\Subscriber;

use Shopware\Core\Framework\DataAbstractionLayer\Event\EntitySearchedEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Telemetry\Metrics\Meter;
use Shopware\Core\Framework\Telemetry\Metrics\Metric\Histogram;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* @internal
*/
#[Package('core')]
class EntityStatsSubscriber implements EventSubscriberInterface
{
/**
* @internal
*/
public function __construct(private readonly Meter $meter)
{
}

public static function getSubscribedEvents()
{
return [
EntitySearchedEvent::class => ['onEntitySearched', 99],
];
}

public function onEntitySearched(EntitySearchedEvent $event): void
{
$criteria = $event->getCriteria();
$associationsCount = $this->getAssociationsCountFromCriteria($criteria);
$this->meter->emit(new Histogram(
name: 'dal.association.count',
value: $associationsCount,
description: 'Number of associations in request',
));
}

private function getAssociationsCountFromCriteria(Criteria $criteria): int
{
return array_reduce(
$criteria->getAssociations(),
fn (int $carry, Criteria $association) => $carry + 1 + $this->getAssociationsCountFromCriteria($association),
0
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -720,5 +720,11 @@
<argument>%shopware.dal.versioning.expire_days%</argument>
<tag name="messenger.message_handler"/>
</service>

<service id="Shopware\Core\Framework\DataAbstractionLayer\Subscriber\EntityStatsSubscriber">
<argument type="service" id="Shopware\Core\Framework\Telemetry\Metrics\Meter"/>

<tag name="kernel.event_subscriber"/>
</service>
</services>
</container>
1 change: 1 addition & 0 deletions src/Core/Framework/DependencyInjection/message-queue.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<service id="Shopware\Core\Framework\MessageQueue\Subscriber\MessageQueueStatsSubscriber">
<argument type="service" id="shopware.increment.gateway.registry"/>
<argument type="service" id="Shopware\Core\Framework\Telemetry\Metrics\Meter"/>

<tag name="kernel.event_subscriber"/>
</service>
Expand Down
24 changes: 24 additions & 0 deletions src/Core/Framework/DependencyInjection/telemetry.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="Shopware\Core\Framework\Telemetry\Metrics\Meter" public="true" lazy="true">
<argument type="tagged_iterator" tag="shopware.metric_transport"/>
<argument type="service" id="logger"/>
</service>

<service id="Shopware\Core\Framework\Telemetry\Metrics\Extractor\MetricExtractor">
<argument type="service" id="logger"/>
</service>

<!-- should be the closest dispatcher to the main Symfony one, since it should catch the final state of ALL dispatched events -->
<service id="Shopware\Core\Framework\Telemetry\Metrics\MetricEventDispatcher" decorates="event_dispatcher" decoration-priority="9999">
<argument type="service" id="Shopware\Core\Framework\Telemetry\Metrics\MetricEventDispatcher.inner"/>
<argument type="service" id="Shopware\Core\Framework\Telemetry\Metrics\Extractor\MetricExtractor"/>
<argument type="service" id="Shopware\Core\Framework\Telemetry\Metrics\Meter"/>
</service>
</services>
</container>
4 changes: 4 additions & 0 deletions src/Core/Framework/Framework.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\MessageQueue\MessageHandlerCompilerPass;
use Shopware\Core\Framework\Migration\MigrationCompilerPass;
use Shopware\Core\Framework\Telemetry\Metrics\MeterProvider;
use Shopware\Core\Framework\Test\DependencyInjection\CompilerPass\ContainerVisibilityCompilerPass;
use Shopware\Core\Framework\Test\RateLimiter\DisableRateLimiterCompilerPass;
use Shopware\Core\Kernel;
Expand Down Expand Up @@ -102,6 +103,7 @@ public function build(ContainerBuilder $container): void
$loader->load('rate-limiter.xml');
$loader->load('increment.xml');
$loader->load('flag.xml');
$loader->load('telemetry.xml');

if ($container->getParameter('kernel.environment') === 'test') {
$loader->load('services_test.xml');
Expand Down Expand Up @@ -153,6 +155,8 @@ public function boot(): void
/** @var FeatureFlagRegistry $featureFlagRegistry */
$featureFlagRegistry = $this->container->get(FeatureFlagRegistry::class);
$featureFlagRegistry->register();
// Inject the meter early in the application lifecycle. This is needed to use the meter in special case (static contexts).
MeterProvider::bindMeter($this->container);

$this->registerEntityExtensions(
$this->container->get(DefinitionInstanceRegistry::class),
Expand Down
Loading

0 comments on commit 4fbdffe

Please sign in to comment.