From f2cd44ddf54ede982ffa17003c22885149c082dc Mon Sep 17 00:00:00 2001 From: Andrii Havryliuk Date: Fri, 24 Apr 2026 16:54:25 +0200 Subject: [PATCH 1/6] exp: configuration for updated images --- docker/compose.yml | 7 +++++-- docker/loki.yaml | 8 ++++++-- docker/otel-collector.yaml | 9 +++++---- docker/prometheus.yaml | 5 +++++ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docker/compose.yml b/docker/compose.yml index 31ae67b..e773134 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -1,4 +1,3 @@ -version: "3" services: # And put them in an OTEL collector pipeline... otel-collector: @@ -16,7 +15,9 @@ services: # To eventually offload to Tempo... tempo: - image: grafana/tempo:latest + # Pin 2.x.x. Docker Hub has no rolling :2 or :2.10 tag — only full versions. + # Bump this patch when Grafana ships a new 2.x release. + image: grafana/tempo:2.10.4 command: [ "-config.file=/etc/tempo.yaml" ] stop_signal: SIGKILL volumes: @@ -29,6 +30,8 @@ services: prometheus: image: prom/prometheus:latest + extra_hosts: + - "host.docker.internal:host-gateway" command: - --config.file=/etc/prometheus.yaml - --web.enable-remote-write-receiver diff --git a/docker/loki.yaml b/docker/loki.yaml index 8b2c9a8..9423114 100644 --- a/docker/loki.yaml +++ b/docker/loki.yaml @@ -23,12 +23,16 @@ query_range: enabled: true max_size_mb: 100 +limits_config: + # Required for OTLP log ingestion (see Loki OTEL docs). + allow_structured_metadata: true + schema_config: configs: - from: 2020-10-24 - store: boltdb-shipper + store: tsdb object_store: filesystem - schema: v11 + schema: v13 index: prefix: index_ period: 24h diff --git a/docker/otel-collector.yaml b/docker/otel-collector.yaml index 4cbb321..9da465a 100644 --- a/docker/otel-collector.yaml +++ b/docker/otel-collector.yaml @@ -6,7 +6,6 @@ receivers: http: endpoint: 0.0.0.0:4318 exporters: - logging: debug: otlp/tempo: endpoint: tempo:4317 @@ -17,8 +16,10 @@ exporters: # endpoint: dd-agent:4317 # tls: # insecure: true - loki: - endpoint: http://loki:3100/loki/api/v1/push + # Loki removed the legacy push API exporter from the collector; use native OTLP. + # https://grafana.com/docs/loki/latest/send-data/otel/ + otlp_http/loki: + endpoint: http://loki:3100/otlp prometheus: endpoint: 0.0.0.0:8889 # adding labels to the metrics @@ -61,7 +62,7 @@ service: exporters: [otlp/tempo, debug, datadog] logs: receivers: [otlp] - exporters: [loki, logging] + exporters: [otlp_http/loki, debug] metrics/delta: receivers: [otlp] # if you want to export through datadog agent, not to api directly, use otlp/datadog instead of datadog diff --git a/docker/prometheus.yaml b/docker/prometheus.yaml index 9c00ba8..0862d58 100644 --- a/docker/prometheus.yaml +++ b/docker/prometheus.yaml @@ -12,3 +12,8 @@ scrape_configs: - job_name: 'otel-collector' static_configs: - targets: [ 'otel-collector:8889' ] + - job_name: 'shopware_prom_exp' + metrics_path: '/api/_internal/prometheus' + static_configs: + - targets: [ 'host.docker.internal:8000' ] + From aefecf6a566d0f2047bc0fd3da3127ebf1e5174f Mon Sep 17 00:00:00 2001 From: Andrii Havryliuk Date: Fri, 15 May 2026 18:16:55 +0200 Subject: [PATCH 2/6] updated docker configuration to work with the latest container versions --- README.md | 14 +++++++++++--- docker/otel-collector.yaml | 37 ++++++++++++++++++------------------- docker/tempo.yaml | 2 ++ 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index dd558f0..c27ce25 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # OpenTelemetry for Shopware 6 +This package provides OpenTelemetry instrumentation for Shopware 6: tracing, metrics, and logs. + ## Requirements - `ext-opentelemetry` PHP extension @@ -11,9 +13,17 @@ composer require shopware/opentelemetry ``` +You also need to install the OpenTelemetry SDK and selected exporter and transport. Below is an example with OTLP over gRPC: + +```bash +composer require open-telemetry/sdk +composer require open-telemetry/exporter-otlp +composer require open-telemetry/transport-grpc +``` + ## Configuration -Enable open telemetry with the following environment variables: +Enable OpenTelemetry SDK with the following environment variables: ```bash OTEL_PHP_AUTOLOAD_ENABLED=true @@ -35,8 +45,6 @@ OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 ``` -You will need to install also composer packages: `open-telemetry/transport-grpc` and `open-telemetry/exporter-otlp`. - ### Enabling Shopware custom tracing To enable tracing for Shopware, you need to add the following config: diff --git a/docker/otel-collector.yaml b/docker/otel-collector.yaml index 9da465a..c5636c2 100644 --- a/docker/otel-collector.yaml +++ b/docker/otel-collector.yaml @@ -7,7 +7,7 @@ receivers: endpoint: 0.0.0.0:4318 exporters: debug: - otlp/tempo: + otlp_grpc/tempo: endpoint: tempo:4317 tls: insecure: true @@ -31,23 +31,22 @@ exporters: metric_expiration: 180m # exporter to datadog using datadog api # for configuration example see https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/datadogexporter/examples/collector.yaml - datadog: - api: - site: ${env:DD_SITE} - key: ${env:DD_API_KEY} - metrics: - resource_attributes_as_tags: true - histograms: - mode: counters - send_aggregation_metrics: true - traces: - # trace_buffer: 10 + # datadog: + # api: + # site: ${env:DD_SITE} + # key: ${env:DD_API_KEY} + # metrics: + # resource_attributes_as_tags: true + # histograms: + # mode: counters + # send_aggregation_metrics: true + # traces: + # # trace_buffer: 10 processors: batch: deltatocumulative: max_stale: 60m - max_streams: 0 service: @@ -59,15 +58,15 @@ service: receivers: [otlp] # if you want to export through datadog agent, not to api directly, use otlp/datadog instead of datadog # in such case otlp/datadog exporter have to be enabled in exporters section - exporters: [otlp/tempo, debug, datadog] + exporters: [otlp_grpc/tempo, debug] logs: receivers: [otlp] exporters: [otlp_http/loki, debug] - metrics/delta: - receivers: [otlp] - # if you want to export through datadog agent, not to api directly, use otlp/datadog instead of datadog - # in such case otlp/datadog exporter have to be enabled in exporters section - exporters: [debug, datadog] + # metrics/delta: + # receivers: [otlp] + # # if you want to export through datadog agent, not to api directly, use otlp/datadog instead of datadog + # # in such case otlp/datadog exporter have to be enabled in exporters section + # exporters: [debug, datadog] metrics/cumulative: receivers: [otlp] processors: [deltatocumulative] diff --git a/docker/tempo.yaml b/docker/tempo.yaml index 030afe8..8545713 100644 --- a/docker/tempo.yaml +++ b/docker/tempo.yaml @@ -23,7 +23,9 @@ distributor: otlp: protocols: http: + endpoint: 0.0.0.0:4318 grpc: + endpoint: 0.0.0.0:4317 opencensus: ingester: From a68bf5d589c6402aa83f70726b04ccf02067b349 Mon Sep 17 00:00:00 2001 From: Andrii Havryliuk Date: Fri, 15 May 2026 18:39:34 +0200 Subject: [PATCH 3/6] implementation for updated MetricTransportInterface --- .../Transports/OpenTelemetryMetricTransport.php | 14 +++++++++++++- .../OpenTelemetryTransportWorkflowTest.php | 2 +- .../OpenTelemetryMetricTransportTest.php | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Metrics/Transports/OpenTelemetryMetricTransport.php b/src/Metrics/Transports/OpenTelemetryMetricTransport.php index 91d4a1d..17570dd 100644 --- a/src/Metrics/Transports/OpenTelemetryMetricTransport.php +++ b/src/Metrics/Transports/OpenTelemetryMetricTransport.php @@ -54,10 +54,22 @@ public function emit(Metric $metric): void } } + public function flush(): void + { + if ($this->meterProvider instanceof \OpenTelemetry\SDK\Metrics\MeterProviderInterface) { + $this->meterProvider->forceFlush(); + } + } + + /** + * @deprecated - use flush instead + */ public function forceFlush(): bool { if ($this->meterProvider instanceof \OpenTelemetry\SDK\Metrics\MeterProviderInterface) { - return $this->meterProvider->forceFlush(); + $this->flush(); + + return true; } return false; diff --git a/tests/integration/Metrics/Transports/OpenTelemetryTransportWorkflowTest.php b/tests/integration/Metrics/Transports/OpenTelemetryTransportWorkflowTest.php index ffafc0b..8ed912c 100644 --- a/tests/integration/Metrics/Transports/OpenTelemetryTransportWorkflowTest.php +++ b/tests/integration/Metrics/Transports/OpenTelemetryTransportWorkflowTest.php @@ -62,7 +62,7 @@ public function testEmit(): void ]), ); - $transport->forceFlush(); + $transport->flush(); $output = $this->getActualOutputForAssertion(); $data = json_decode($output, true); diff --git a/tests/unit/Metrics/Transports/OpenTelemetryMetricTransportTest.php b/tests/unit/Metrics/Transports/OpenTelemetryMetricTransportTest.php index 3312536..5b92e3d 100644 --- a/tests/unit/Metrics/Transports/OpenTelemetryMetricTransportTest.php +++ b/tests/unit/Metrics/Transports/OpenTelemetryMetricTransportTest.php @@ -159,13 +159,13 @@ public function testEmit(Metric $metric, string $name, int|float $value, ?string $this->createTransport($namespace)->emit($metric); } - public function testForceFlush(): void + public function testFlush(): void { $meterProviderMock = $this->createMock(\OpenTelemetry\SDK\Metrics\MeterProviderInterface::class); $meterProviderMock->expects(static::once()) ->method('forceFlush')->willReturn(true); $transport = (new OpenTelemetryMetricTransport($meterProviderMock, new MetricNameFormatter('namespace'), 'namespace')); - static::assertTrue($transport->forceFlush()); + $transport->flush(); } /** From 887e84bd7386443459c94ca0ca8c37ad4cc70dbc Mon Sep 17 00:00:00 2001 From: Andrii Havryliuk Date: Sun, 17 May 2026 13:21:23 +0200 Subject: [PATCH 4/6] ignore phpstan false positive --- src/OpenTelemetryShopwareBundle.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/OpenTelemetryShopwareBundle.php b/src/OpenTelemetryShopwareBundle.php index a9d6c8d..a7e0639 100644 --- a/src/OpenTelemetryShopwareBundle.php +++ b/src/OpenTelemetryShopwareBundle.php @@ -11,7 +11,6 @@ use Shopware\OpenTelemetry\Metrics\Transports\OpenTelemetryMetricTransportFactory; use Shopware\OpenTelemetry\Profiler\OtelProfiler; use OpenTelemetry\Contrib\Logs\Monolog\Handler; -use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -46,10 +45,7 @@ public function build(ContainerBuilder $container) public function configure(DefinitionConfigurator $definition): void { - $rootNode = $definition->rootNode(); - \assert($rootNode instanceof ArrayNodeDefinition); - - $rootNode + $definition->rootNode() // @phpstan-ignore class.notFound ->children() ->arrayNode('metrics') ->addDefaultsIfNotSet() @@ -61,7 +57,7 @@ public function configure(DefinitionConfigurator $definition): void ->defaultValue('io.opentelemetry.contrib.php.shopware') ->end() ->end() - ->end() + ->end() // metrics ->end() ; } From 7abaa7b70bf54dfe73158f7cb6610cfebae51559 Mon Sep 17 00:00:00 2001 From: Andrii Havryliuk Date: Fri, 25 Sep 2026 17:16:18 +0200 Subject: [PATCH 5/6] -remove metrics prefix support -update docs -keep compatibility with older core versions --- .gitignore | 3 + README.md | 49 ++++++- docker/.env.example | 3 + src/Feature.php | 13 ++ .../OpenTelemetryMetricTransport.php | 4 +- .../OpenTelemetryMetricTransportFactory.php | 39 +++++- src/OpenTelemetryShopwareBundle.php | 21 ++- .../OpenTelemetryTransportWorkflowTest.php | 26 ++-- .../OpenTelemetryShopwareBundleTest.php | 60 ++++++++- ...penTelemetryMetricTransportFactoryTest.php | 123 +++++++++++++++--- 10 files changed, 283 insertions(+), 58 deletions(-) create mode 100644 docker/.env.example diff --git a/.gitignore b/.gitignore index 4b49ecc..edfc76f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /.php-cs-fixer.cache /vendor /composer.lock +/.phpunit.result.cache +/coverage.xml +/docker/.env diff --git a/README.md b/README.md index e089657..8561e0f 100644 --- a/README.md +++ b/README.md @@ -89,18 +89,20 @@ monolog: ## Transport metrics to OpenTelemetry -You can enable OpenTelemetry metrics transport with the following configuration: +Shopware core collects metrics through its telemetry abstraction. This bundle +registers a transport that sends those metrics to OpenTelemetry. + +Enable the transport with the following configuration: ```yaml # config/packages/opentelemetry.yaml open_telemetry_shopware: - metrics: - enabled: true - namespace: 'io.opentelemetry.contrib.php.shopware' # or your custom namespace + metrics: + enabled: true ``` -Please note that OpenTelemetry SDK has to be configured to send metrics to the collector. -It is configured using the same environment variables. Example configuration could look like this: +The OpenTelemetry SDK must be configured to send metrics to the collector. It +uses the same environment variables as tracing. Example: ```bash OTEL_SERVICE_NAME=shopware @@ -111,6 +113,41 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta ``` +Metrics are only emitted when the `TELEMETRY_METRICS` feature flag is enabled in +Shopware. Since Shopware 6.7.12, `shopware.telemetry.metrics.enabled` must be +`true` as well. + +### Metric names + +Metric names are prefixed with the namespace configured in Shopware core (`shopware.telemetry.metrics.namespace`). +Shopware core passes it to transports since version 6.7.12. On older versions the prefix +`io.opentelemetry.contrib.php.shopware` is used. + +This bundle has no namespace option of its own. The option `open_telemetry_shopware.metrics.namespace` +from `0.2.0-alpha` is deprecated and ignored. It will be removed in 1.0. + +### Flushing + +Since Shopware 6.7.12, core calls `flush()` on every transport at the end of each request +and console command, and about every 60 seconds inside a running Messenger worker. This transport +then exports the collected metrics immediately, so metrics from long-running workers +do not wait until the worker stops. + +On older Shopware versions the metrics are exported once, when the PHP process shuts down. + +The method `OpenTelemetryMetricTransport::forceFlush()` is deprecated. Use `flush()` instead. + +### Shopware compatibility + +| Shopware version | Behavior | +|--------------------|--------------------------------------------------------------------------| +| below 6.6.7 | No metrics support in core. Tracing and log forwarding still work. | +| 6.6.7 to 6.7.11 | Metrics are exported when the PHP process shuts down. Metric names are prefixed with `io.opentelemetry.contrib.php.shopware`. | +| 6.7.12 and later | Metrics are flushed by core (see above). Metric names are prefixed with the namespace from Shopware core. | + +The telemetry abstraction in Shopware core is experimental until Shopware 6.8. +This bundle stays at version `0.x` until then. + ### Temporality configuration OpenTelemetry PHP SDK does not support storage for accumulation of metrics. As PHP processes are short-lived, it's best to emit metrics with delta temporality and aggregate them on receiving side. Unfortunately at the moment of writing this diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..60ed18a --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,3 @@ +# Copy to docker/.env and fill in your values +DD_SITE=datadoghq.eu +DD_API_KEY= diff --git a/src/Feature.php b/src/Feature.php index ef5f983..94af4cc 100644 --- a/src/Feature.php +++ b/src/Feature.php @@ -4,10 +4,23 @@ namespace Shopware\OpenTelemetry; +use Shopware\Core\Framework\Telemetry\Metrics\Config\TransportConfig; + class Feature { public static function metricsSupported(): bool { return class_exists('Shopware\Core\Framework\Telemetry\Metrics\Metric\Metric'); } + + /** + * shopware/core >= 6.7.12 passes `shopware.telemetry.metrics.namespace` to transports via TransportConfig. + * The assertion tells PHPStan that the property exists when analysing against an older core. + * + * @phpstan-assert-if-true TransportConfig&object{namespace: string|null} $transportConfig + */ + public static function metricNamespaceSupported(TransportConfig $transportConfig): bool + { + return property_exists($transportConfig, 'namespace'); + } } diff --git a/src/Metrics/Transports/OpenTelemetryMetricTransport.php b/src/Metrics/Transports/OpenTelemetryMetricTransport.php index 17570dd..afc6926 100644 --- a/src/Metrics/Transports/OpenTelemetryMetricTransport.php +++ b/src/Metrics/Transports/OpenTelemetryMetricTransport.php @@ -20,9 +20,9 @@ public function __construct( // careful: if link to meterProvider is removed, SDK will not transmit metrics (it uses weak references) private MeterProviderInterface $meterProvider, private MetricNameFormatter $formatter, - private string $namespace, + private string $instrumentationScope, ) { - $this->meter = $this->meterProvider->getMeter($this->namespace); + $this->meter = $this->meterProvider->getMeter($this->instrumentationScope); } diff --git a/src/Metrics/Transports/OpenTelemetryMetricTransportFactory.php b/src/Metrics/Transports/OpenTelemetryMetricTransportFactory.php index 4844748..34965d9 100644 --- a/src/Metrics/Transports/OpenTelemetryMetricTransportFactory.php +++ b/src/Metrics/Transports/OpenTelemetryMetricTransportFactory.php @@ -8,17 +8,32 @@ use Shopware\Core\Framework\Telemetry\Metrics\Factory\MetricTransportFactoryInterface; use Shopware\Core\Framework\Telemetry\Metrics\Metric\Type; use Shopware\Core\Framework\Telemetry\Metrics\MetricTransportInterface; +use Shopware\OpenTelemetry\Feature; use Shopware\OpenTelemetry\Metrics\MetricNameFormatter; class OpenTelemetryMetricTransportFactory implements MetricTransportFactoryInterface { + /** + * Name of the OpenTelemetry instrumentation scope that owns the metrics. + * Same convention as the tracing instrumentations shipped in this bundle. + */ + public const INSTRUMENTATION_SCOPE = 'io.opentelemetry.contrib.php.shopware'; + + /** + * Metric name prefix used with shopware/core < 6.7.12, where core does not pass a namespace to transports. + * Intentionally equal to the old default prefix name to limit prefix changes on updates at least for + * installations with default values. + */ + public const LEGACY_NAMESPACE = 'io.opentelemetry.contrib.php.shopware'; + public function __construct( - private readonly string $namespace, - private MetricNameFormatter $formatter, - private OpenTelemetryMeterProviderFactory $meterProviderFactory, + private readonly OpenTelemetryMeterProviderFactory $meterProviderFactory, ) {} + public function create(TransportConfig $transportConfig): MetricTransportInterface { + $formatter = new MetricNameFormatter($this->resolveNamespace($transportConfig)); + $buckets = []; foreach ($transportConfig->metricsConfig as $metric) { if (!$metric->enabled) { @@ -30,7 +45,7 @@ public function create(TransportConfig $transportConfig): MetricTransportInterfa && \is_array($metric->parameters['buckets']) && \count($metric->parameters['buckets']) > 0 ) { - $metricName = $this->formatter->format($metric->name); + $metricName = $formatter->format($metric->name); assert(!empty($metricName)); $buckets[$metricName] = $metric->parameters['buckets']; @@ -39,6 +54,20 @@ public function create(TransportConfig $transportConfig): MetricTransportInterfa $meterProvider = $this->meterProviderFactory->createMeterProvider($buckets); - return new OpenTelemetryMetricTransport($meterProvider, $this->formatter, $this->namespace); + return new OpenTelemetryMetricTransport($meterProvider, $formatter, self::INSTRUMENTATION_SCOPE); + } + + /** + * The metric name prefix is `shopware.telemetry.metrics.namespace`, passed by Shopware core since 6.7.12. + * A null value from core means "no prefix". Older core versions do not pass a namespace at all, + * they get self::LEGACY_NAMESPACE. + */ + private function resolveNamespace(TransportConfig $transportConfig): string + { + if (!Feature::metricNamespaceSupported($transportConfig)) { + return self::LEGACY_NAMESPACE; + } + + return \is_string($transportConfig->namespace) ? $transportConfig->namespace : ''; } } diff --git a/src/OpenTelemetryShopwareBundle.php b/src/OpenTelemetryShopwareBundle.php index a7e0639..e534d74 100644 --- a/src/OpenTelemetryShopwareBundle.php +++ b/src/OpenTelemetryShopwareBundle.php @@ -6,7 +6,6 @@ use Shopware\OpenTelemetry\Logging\OpenTelemetryLoggerFactory; use Shopware\OpenTelemetry\Messenger\MessageBusSubscriber; -use Shopware\OpenTelemetry\Metrics\MetricNameFormatter; use Shopware\OpenTelemetry\Metrics\Transports\OpenTelemetryMeterProviderFactory; use Shopware\OpenTelemetry\Metrics\Transports\OpenTelemetryMetricTransportFactory; use Shopware\OpenTelemetry\Profiler\OtelProfiler; @@ -19,7 +18,7 @@ use function Symfony\Component\DependencyInjection\Loader\Configurator\service; /** - * @phpstan-type Config array{metrics: array{enabled: bool, namespace: string}} + * @phpstan-type Config array{metrics: array{enabled: bool, namespace?: mixed}} */ class OpenTelemetryShopwareBundle extends AbstractBundle { @@ -53,8 +52,13 @@ public function configure(DefinitionConfigurator $definition): void ->booleanNode('enabled') ->defaultFalse() ->end() + // kept without a default so that only installations that still set it get the deprecation ->scalarNode('namespace') - ->defaultValue('io.opentelemetry.contrib.php.shopware') + ->setDeprecated( + 'shopware/opentelemetry', + '0.2.0', + 'The "%path%.%node%" option is ignored and will be removed in 1.0. Replace by "shopware.telemetry.metrics.namespace" instead (Shopware 6.7.12+).', + ) ->end() ->end() ->end() // metrics @@ -70,21 +74,12 @@ public function loadExtension(array $config, ContainerConfigurator $container, C if (!Feature::metricsSupported()) { return; } - $metricsConfig = $config['metrics']; - $namespace = $metricsConfig['namespace']; - - if ($metricsConfig['enabled']) { - $container->services() - ->set(MetricNameFormatter::class) - ->arg('$namespace', $namespace); - + if ($config['metrics']['enabled']) { $container->services() ->set(OpenTelemetryMeterProviderFactory::class); $container->services() ->set(OpenTelemetryMetricTransportFactory::class) - ->arg('$namespace', $namespace) - ->arg('$formatter', service(MetricNameFormatter::class)) ->arg('$meterProviderFactory', service(OpenTelemetryMeterProviderFactory::class)) ->tag('shopware.metric_transport_factory'); } diff --git a/tests/integration/Metrics/Transports/OpenTelemetryTransportWorkflowTest.php b/tests/integration/Metrics/Transports/OpenTelemetryTransportWorkflowTest.php index 8ed912c..7ef7b41 100644 --- a/tests/integration/Metrics/Transports/OpenTelemetryTransportWorkflowTest.php +++ b/tests/integration/Metrics/Transports/OpenTelemetryTransportWorkflowTest.php @@ -33,13 +33,9 @@ public function testEmit(): void static::markTestSkipped('Installed version of shopware/core does not support metrics'); } - $namespace = 'test.namespace'; + $transportFactory = new OpenTelemetryMetricTransportFactory(new OpenTelemetryMeterProviderFactory()); - $nameFormatter = new MetricNameFormatter($namespace); - $providerFactory = new OpenTelemetryMeterProviderFactory(); - - $transportFactory = new OpenTelemetryMetricTransportFactory($namespace, $nameFormatter, $providerFactory); - $transportConfig = new TransportConfig([ + $metricsConfig = [ new MetricConfig( name: 'testHistogram', description: 'test histogram description', @@ -47,7 +43,19 @@ public function testEmit(): void enabled: true, parameters: ['buckets' => [5, 10]], ), - ]); + ]; + + // TransportConfig::$namespace exists since shopware/core 6.7.12; older versions get the legacy prefix + if (Feature::metricNamespaceSupported(new TransportConfig([]))) { + $expectedPrefix = 'test.namespace'; + $transportConfig = (new \ReflectionClass(TransportConfig::class))->newInstanceArgs([ + 'metricsConfig' => $metricsConfig, + 'namespace' => $expectedPrefix, + ]); + } else { + $expectedPrefix = OpenTelemetryMetricTransportFactory::LEGACY_NAMESPACE; + $transportConfig = new TransportConfig($metricsConfig); + } $transport = $transportFactory->create($transportConfig); $this->assertInstanceOf(OpenTelemetryMetricTransport::class, $transport); @@ -70,13 +78,13 @@ public function testEmit(): void // Validate scope.name $this->assertArrayHasKey('name', $data['scope']); - $this->assertEquals('test.namespace', $data['scope']['name']); + $this->assertEquals(OpenTelemetryMetricTransportFactory::INSTRUMENTATION_SCOPE, $data['scope']['name']); // Validate metrics[0].name, description, and unit $this->assertArrayHasKey('metrics', $data['scope']); $this->assertArrayHasKey(0, $data['scope']['metrics']); $this->assertArrayHasKey('name', $data['scope']['metrics'][0]); - $this->assertEquals('test.namespace.testHistogram', $data['scope']['metrics'][0]['name']); + $this->assertEquals($expectedPrefix . '.testHistogram', $data['scope']['metrics'][0]['name']); $this->assertArrayHasKey('description', $data['scope']['metrics'][0]); $this->assertEquals('description', $data['scope']['metrics'][0]['description']); $this->assertArrayHasKey('unit', $data['scope']['metrics'][0]); diff --git a/tests/integration/OpenTelemetryShopwareBundleTest.php b/tests/integration/OpenTelemetryShopwareBundleTest.php index 54d22c1..3afbe9f 100644 --- a/tests/integration/OpenTelemetryShopwareBundleTest.php +++ b/tests/integration/OpenTelemetryShopwareBundleTest.php @@ -9,8 +9,10 @@ use PHPUnit\Framework\TestCase; use Shopware\OpenTelemetry\Feature; use Shopware\OpenTelemetry\Metrics\Transports\OpenTelemetryMetricTransportFactory; +use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Shopware\OpenTelemetry\OpenTelemetryShopwareBundle; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; @@ -24,19 +26,73 @@ class OpenTelemetryShopwareBundleTest extends TestCase { public function testServicesLoadedWhenMetricsEnabled(): void { - $config = ['metrics' => ['enabled' => true, 'namespace' => 'io.opentelemetry.contrib.php.shopware']]; + $config = ['metrics' => ['enabled' => true]]; $container = $this->loadBundleWithConfig($config); if (Feature::metricsSupported()) { $this->assertTrue($container->has(OpenTelemetryMetricTransportFactory::class)); + $definition = $container->getDefinition(OpenTelemetryMetricTransportFactory::class); + $this->assertTrue($definition->hasTag('shopware.metric_transport_factory')); } else { $this->assertFalse($container->has(OpenTelemetryMetricTransportFactory::class)); } } + public function testMetricsAreDisabledByDefault(): void + { + $config = $this->processConfiguration([]); + + $this->assertSame(['metrics' => ['enabled' => false]], $config); + } + + public function testNamespaceOptionIsDeprecatedAndIgnored(): void + { + $deprecations = []; + set_error_handler(static function (int $errno, string $errstr) use (&$deprecations): bool { + $deprecations[] = $errstr; + + return true; + }, \E_USER_DEPRECATED); + + try { + $config = $this->processConfiguration([['metrics' => ['enabled' => true, 'namespace' => 'my.namespace']]]); + } finally { + restore_error_handler(); + } + + $this->assertCount(1, $deprecations); + $this->assertStringContainsString('"open_telemetry_shopware.metrics.namespace" option is ignored', $deprecations[0]); + + // the option is accepted, so existing configuration files keep working + $this->assertSame(['metrics' => ['enabled' => true, 'namespace' => 'my.namespace']], $config); + + if (Feature::metricsSupported()) { + $container = $this->loadBundleWithConfig(['metrics' => ['enabled' => true, 'namespace' => 'my.namespace']]); + $definition = $container->getDefinition(OpenTelemetryMetricTransportFactory::class); + $this->assertArrayNotHasKey('$namespace', $definition->getArguments()); + } + } + + /** + * @param array> $configs + * + * @return array + */ + private function processConfiguration(array $configs): array + { + $bundle = new OpenTelemetryShopwareBundle(); + $extension = $bundle->getContainerExtension(); + $this->assertInstanceOf(ConfigurationExtensionInterface::class, $extension); + + $configuration = $extension->getConfiguration([], new ContainerBuilder()); + $this->assertNotNull($configuration); + + return (new Processor())->processConfiguration($configuration, $configs); + } + public function testServicesNotLoadedWhenMetricsDisabled(): void { - $config = ['metrics' => ['enabled' => false, 'namespace' => 'io.opentelemetry.contrib.php.shopware']]; + $config = ['metrics' => ['enabled' => false]]; $container = $this->loadBundleWithConfig($config); $this->assertFalse($container->has(OpenTelemetryMetricTransportFactory::class)); diff --git a/tests/unit/Metrics/Transports/OpenTelemetryMetricTransportFactoryTest.php b/tests/unit/Metrics/Transports/OpenTelemetryMetricTransportFactoryTest.php index 1258bf6..5a634b2 100644 --- a/tests/unit/Metrics/Transports/OpenTelemetryMetricTransportFactoryTest.php +++ b/tests/unit/Metrics/Transports/OpenTelemetryMetricTransportFactoryTest.php @@ -5,8 +5,10 @@ namespace Shopware\OpenTelemetry\Tests\Unit\Metrics\Transports; use OpenTelemetry\API\Metrics\MeterInterface; +use OpenTelemetry\API\Metrics\MeterProviderInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Shopware\Core\Framework\Telemetry\Metrics\Config\MetricConfig; use Shopware\Core\Framework\Telemetry\Metrics\Config\TransportConfig; @@ -16,25 +18,38 @@ use Shopware\OpenTelemetry\Metrics\Transports\OpenTelemetryMetricTransport; use Shopware\OpenTelemetry\Metrics\Transports\OpenTelemetryMetricTransportFactory; use Shopware\OpenTelemetry\Metrics\Transports\OpenTelemetryMeterProviderFactory; -use OpenTelemetry\API\Metrics\MeterProviderInterface; #[CoversClass(OpenTelemetryMetricTransportFactory::class)] #[UsesClass(Feature::class)] +#[UsesClass(MetricNameFormatter::class)] #[UsesClass(OpenTelemetryMetricTransport::class)] class OpenTelemetryMetricTransportFactoryTest extends TestCase { - public function testCreate(): void + /** + * @var OpenTelemetryMeterProviderFactory&MockObject + */ + private OpenTelemetryMeterProviderFactory $meterProviderFactory; + + /** + * @var MeterProviderInterface&MockObject + */ + private MeterProviderInterface $meterProvider; + + public function setUp(): void { if (!Feature::metricsSupported()) { static::markTestSkipped('Installed version of shopware/core does not support metrics'); } - $namespace = 'testNamespace'; - $formatter = $this->createMock(MetricNameFormatter::class); - $meterProviderFactory = $this->createMock(OpenTelemetryMeterProviderFactory::class); - $meterProvider = $this->createMock(MeterProviderInterface::class); + $this->meterProviderFactory = $this->createMock(OpenTelemetryMeterProviderFactory::class); + $this->meterProvider = $this->createMock(MeterProviderInterface::class); + } - $transportConfig = new TransportConfig([ + public function testCreatePassesPrefixedBucketsOfEnabledHistogramsOnly(): void + { + $this->skipIfCoreNamespaceUnsupported(); + + $transportConfig = $this->createTransportConfig([ new MetricConfig( name: 'testHistogram', description: 'testDescription', @@ -42,28 +57,94 @@ public function testCreate(): void enabled: true, parameters: ['buckets' => [1, 2, 3]], ), - ]); + new MetricConfig( + name: 'disabledHistogram', + description: 'testDescription', + type: Type::HISTOGRAM, + enabled: false, + parameters: ['buckets' => [1, 2, 3]], + ), + new MetricConfig( + name: 'testCounter', + description: 'testDescription', + type: Type::COUNTER, + enabled: true, + ), + ], 'core.namespace'); + + $this->expectMeterProvider(['core.namespace.testHistogram' => [1, 2, 3]]); + + $transport = (new OpenTelemetryMetricTransportFactory($this->meterProviderFactory))->create($transportConfig); - $formatter->expects($this->once()) - ->method('format') - ->with('testHistogram') - ->willReturn('testNamespace.testHistogram'); + $this->assertInstanceOf(OpenTelemetryMetricTransport::class, $transport); + } - // checking if buckets are provided to the meterProviderFactory - $meterProviderFactory->expects($this->once()) + public function testNullCoreNamespaceMeansNoPrefix(): void + { + $this->skipIfCoreNamespaceUnsupported(); + $this->expectMeterProvider(['testHistogram' => [1]]); + + (new OpenTelemetryMetricTransportFactory($this->meterProviderFactory)) + ->create($this->createTransportConfig([$this->histogram()], null)); + } + + public function testLegacyNamespaceIsUsedWhenCoreDoesNotPassOne(): void + { + if (Feature::metricNamespaceSupported(new TransportConfig([]))) { + static::markTestSkipped('Installed version of shopware/core passes a namespace to transports'); + } + + $this->expectMeterProvider([OpenTelemetryMetricTransportFactory::LEGACY_NAMESPACE . '.testHistogram' => [1]]); + + (new OpenTelemetryMetricTransportFactory($this->meterProviderFactory)) + ->create(new TransportConfig([$this->histogram()])); + } + + /** + * @param array> $expectedBuckets + */ + private function expectMeterProvider(array $expectedBuckets): void + { + $this->meterProviderFactory->expects($this->once()) ->method('createMeterProvider') - ->with(['testNamespace.testHistogram' => [1, 2, 3]]) - ->willReturn($meterProvider); + ->with($expectedBuckets) + ->willReturn($this->meterProvider); - $meterProvider->expects($this->once()) + $this->meterProvider->expects($this->once()) ->method('getMeter') - ->with($namespace) + ->with(OpenTelemetryMetricTransportFactory::INSTRUMENTATION_SCOPE) ->willReturn($this->createMock(MeterInterface::class)); + } + private function histogram(): MetricConfig + { + return new MetricConfig( + name: 'testHistogram', + description: 'testDescription', + type: Type::HISTOGRAM, + enabled: true, + parameters: ['buckets' => [1]], + ); + } - $factory = new OpenTelemetryMetricTransportFactory($namespace, $formatter, $meterProviderFactory); - $transport = $factory->create($transportConfig); + /** + * TransportConfig::$namespace exists since shopware/core 6.7.12, so the object is built via reflection + * to keep this test file loadable (and analysable) with older core versions. + * + * @param array $metricsConfig + */ + private function createTransportConfig(array $metricsConfig, ?string $coreNamespace): TransportConfig + { + return (new \ReflectionClass(TransportConfig::class))->newInstanceArgs([ + 'metricsConfig' => $metricsConfig, + 'namespace' => $coreNamespace, + ]); + } - $this->assertInstanceOf(OpenTelemetryMetricTransport::class, $transport); + private function skipIfCoreNamespaceUnsupported(): void + { + if (!Feature::metricNamespaceSupported(new TransportConfig([]))) { + static::markTestSkipped('Installed version of shopware/core does not pass a namespace to transports'); + } } } From 89247affb237d3aa652f855e6a120a0239dc6ecd Mon Sep 17 00:00:00 2001 From: Andrii Havryliuk Date: Fri, 25 Sep 2026 17:34:59 +0200 Subject: [PATCH 6/6] ignore type errors caused by version differences --- phpstan.neon.dist | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f13b841..3f5e876 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,3 +7,10 @@ parameters: reportUnmatchedIgnoredErrors: true excludePaths: ignoreErrors: + # Feature::metricNamespaceSupported() asserts TransportConfig::$namespace, which exists in + # shopware/core >= 6.7.12 only. Against a newer core the assertion is redundant, against an + # older core it is required, so the report is ignored without being mandatory. + - + message: '#^Asserted type .*TransportConfig.* does not narrow down the type\.$#' + path: src/Feature.php + reportUnmatched: false