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.
