PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

OpenTelemetry – distributed tracing, auto-instrumentacja, Jaeger w DDEV

by Henryk Tews / wtorek, 20 sierpnia 2024 / Opublikowano w Środowiska

Gdy aplikacja działa jako zestaw mikroserwisów, jeden request użytkownika może przejść przez Magento, serwis zamówień, API płatności i warehouse management. Gdy coś idzie wolno lub się psuje – jak znaleźć winowajcę? OpenTelemetry to standard obserwability który łączy traces (śledzenie requestów), metrics i logs w jeden spójny obraz. Pokazuję jak instrumentować PHP i Magento 2.

Trzy filary observability

  • Traces – ślad pełnego przebiegu requestu przez wiele serwisów z czasami każdego kroku
  • Metrics – zagregowane liczby: liczba requestów, czasy odpowiedzi, error rate
  • Logs – strukturalne zdarzenia z kontekstem trace_id który łączy je z tracesem

OpenTelemetry (OTel) to otwarte SDK i protokół który zbiera wszystkie trzy i wysyła do backendu (Jaeger, Zipkin, Grafana Tempo, Honeycomb, Datadog).

Instalacja PHP SDK

composer require open-telemetry/sdk
composer require open-telemetry/exporter-otlp
composer require open-telemetry/opentelemetry-auto-psr18  # auto-instrumentation dla HTTP clients
composer require open-telemetry/opentelemetry-auto-pdo    # auto-instrumentation dla PDO

# Opcjonalnie - auto-instrumentation przez extension (zero-code)
# pecl install opentelemetry

Inicjalizacja TracerProvider

<?php

declare(strict_types=1);

use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\Contrib\Otlp\OtlpHttpExporter;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SemConv\ResourceAttributes;

// Konfiguracja - najlepiej przez zmienne środowiskowe
// OTEL_SERVICE_NAME=magento-store
// OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318

function initOpenTelemetry(): \OpenTelemetry\API\Trace\TracerInterface
{
    $resource = ResourceInfo::create(Attributes::create([
        ResourceAttributes::SERVICE_NAME    => $_ENV['OTEL_SERVICE_NAME'] ?? 'php-app',
        ResourceAttributes::SERVICE_VERSION => '1.0.0',
        ResourceAttributes::DEPLOYMENT_ENVIRONMENT => $_ENV['APP_ENV'] ?? 'production',
    ]));

    $exporter = OtlpHttpExporter::fromConnectionString(
        $_ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? 'http://localhost:4318'
    );

    $tracerProvider = TracerProvider::builder()
        ->setResource($resource)
        ->addSpanProcessor(new BatchSpanProcessor($exporter))
        ->build();

    // Zarejestruj jako globalny provider
    \OpenTelemetry\API\Globals::registerInitializer(fn() => $tracerProvider);

    return $tracerProvider->getTracer('app');
}

Ręczna instrumentacja kodu PHP

<?php

declare(strict_types=1);

use OpenTelemetry\API\Globals;

class ProductImportService
{
    public function importBatch(array $products): array
    {
        $tracer = Globals::tracerProvider()->getTracer('vendor/product-import');

        // Główny span dla całego importu
        $span = $tracer->spanBuilder('product.import.batch')
            ->setAttribute('import.batch_size', count($products))
            ->setAttribute('import.source', 'csv')
            ->startSpan();

        $scope = $span->activate();
        $results = ['success' => 0, 'errors' => 0];

        try {
            foreach (array_chunk($products, 100) as $chunk) {
                // Child span dla każdego chunk
                $chunkSpan = $tracer->spanBuilder('product.import.chunk')
                    ->setAttribute('import.chunk_size', count($chunk))
                    ->startSpan();

                $chunkScope = $chunkSpan->activate();

                try {
                    $saved = $this->procesChunk($chunk);
                    $results['success'] += $saved;

                    $chunkSpan->setAttribute('import.saved', $saved);
                } catch (\Exception $e) {
                    $chunkSpan->recordException($e);
                    $chunkSpan->setStatus(
                        \OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR,
                        $e->getMessage()
                    );
                    $results['errors']++;
                } finally {
                    $chunkScope->detach();
                    $chunkSpan->end();
                }
            }

            $span->setAttribute('import.total_success', $results['success']);
            $span->setAttribute('import.total_errors', $results['errors']);

        } catch (\Exception $e) {
            $span->recordException($e);
            $span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR, $e->getMessage());
            throw $e;
        } finally {
            $scope->detach();
            $span->end();
        }

        return $results;
    }

    private function procesChunk(array $chunk): int
    {
        // Logika importu...
        return count($chunk);
    }
}

Auto-instrumentacja w Magento 2

<?php

declare(strict_types=1);

// Plugin do Magento 2 który dodaje tracing do Repository
namespace Vendor\OtelModule\Plugin;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use OpenTelemetry\API\Globals;

class ProductRepositoryTracingPlugin
{
    public function aroundGetById(
        ProductRepositoryInterface $subject,
        callable $proceed,
        int $productId,
        bool $editMode = false,
        ?int $storeId = null,
        bool $forceReload = false
    ): ProductInterface {
        $tracer = Globals::tracerProvider()->getTracer('magento/catalog');

        $span = $tracer->spanBuilder('catalog.product.getById')
            ->setAttribute('product.id', $productId)
            ->setAttribute('product.edit_mode', $editMode)
            ->setAttribute('product.store_id', $storeId ?? 0)
            ->setAttribute('db.operation', 'SELECT')
            ->setAttribute('db.table', 'catalog_product_entity')
            ->startSpan();

        $scope = $span->activate();

        try {
            $result = $proceed($productId, $editMode, $storeId, $forceReload);
            $span->setAttribute('product.sku', $result->getSku());
            return $result;
        } catch (\Exception $e) {
            $span->recordException($e);
            $span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR, $e->getMessage());
            throw $e;
        } finally {
            $scope->detach();
            $span->end();
        }
    }
}

Konfiguracja DDEV z Jaeger

# .ddev/docker-compose.otel.yaml
version: '3.6'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"   # Jaeger UI
      - "4318:4318"     # OTLP HTTP receiver
    environment:
      COLLECTOR_OTLP_ENABLED: "true"
    labels:
      com.ddev.site-name: ${DDEV_SITENAME}

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - ./otel-config.yaml:/etc/otel/config.yaml
    command: ["--config=/etc/otel/config.yaml"]
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
    labels:
      com.ddev.site-name: ${DDEV_SITENAME}
# .ddev/otel-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:
    timeout: 1s

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true
  logging:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers:  [otlp]
      processors: [batch]
      exporters:  [jaeger, logging]
# Uruchom i otwórz Jaeger UI
ddev restart
ddev launch :16686

# Ustaw zmienne w .ddev/.env
echo "OTEL_SERVICE_NAME=magento-store" >> .ddev/.env
echo "OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318" >> .ddev/.env

Łączenie logów z tracesami

<?php

declare(strict_types=1);

// Logger który dodaje trace_id do każdego logu
class TracingAwareLogger extends \Psr\Log\AbstractLogger
{
    public function __construct(
        private \Psr\Log\LoggerInterface $decorated
    ) {}

    public function log($level, string|\Stringable $message, array $context = []): void
    {
        $span = \OpenTelemetry\API\Trace\Span::getCurrent();

        if ($span->isRecording()) {
            $spanContext = $span->getContext();
            // Dodaj trace context do każdego logu
            $context['trace_id'] = $spanContext->getTraceId();
            $context['span_id']  = $spanContext->getSpanId();
        }

        $this->decorated->log($level, $message, $context);
    }
}

// Wynikowy log wygląda tak:
// [2024-08-20 14:32:11] INFO: Product saved {"sku":"SKU-001","trace_id":"abc123def456","span_id":"xyz789"}
// W Grafana możesz kliknąć trace_id i przejść prosto do tracesu w Tempo/Jaeger

Podsumowanie

OpenTelemetry to standard który wypiera proprietary APM agents – jeden SDK, wiele backendów. Dla pojedynczego Magento 2 monolitu Blackfire i New Relic APM mogą wystarczyć. Gdy pojawia się headless frontend, serwis płatności, PIM i warehouse API – distributed tracing przez OTel staje się niezbędnym narzędziem diagnostycznym. Auto-instrumentacja PDO i HTTP klientów jest dostępna out-of-the-box, ręczna instrumentacja biznesowej logiki wymaga kilku linii kodu w kluczowych miejscach.

About Henryk Tews

Co możesz przeczytać następne

DDEV zaawansowany – mutagen, własne serwisy, hooks, współdzielona konfiguracja w teamie
GitHub Actions – pipeline dla PHP, matrix testów, deploy na staging przez SSH
XAMPP vs DDEV vs Warden – pełne porównanie w tabeli
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}