HardПрактика26 min

OpenTelemetry: unified observability

OpenTelemetry SDK, Collector, distributed tracing, метрики и логи с примерами на PHP и Go

Зачем нужен OpenTelemetry

До OpenTelemetry каждый vendor предлагал свой SDK для сбора телеметрии. Jaeger — свой клиент для трейсинга, Datadog — свой агент, New Relic — свой SDK. Смена бэкенда означала переписывание инструментации во всех сервисах.

OpenTelemetry (OTel) — это vendor-neutral стандарт и набор инструментов для сбора, обработки и экспорта телеметрии. Он объединяет три сигнала наблюдаемости в единый SDK:

Проблема Без OTel С OTel
Vendor lock-in Переписываем код при смене бэкенда Меняем только exporter в конфиге
Разные SDK Jaeger client + Prometheus client + Logstash Один OTel SDK для всего
Корреляция trace_id в трейсах, но не в логах Автоматическая корреляция traces + metrics + logs
Стандартизация Каждый vendor — свой формат Единые semantic conventions

OpenTelemetry — это проект CNCF (Cloud Native Computing Foundation), второй по активности после Kubernetes.

Три сигнала OTel

┌─────────────────────────────────────────────────────────┐
│                    OpenTelemetry SDK                      │
│                                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │
│  │    Traces    │  │   Metrics    │  │     Logs     │   │
│  │              │  │              │  │              │   │
│  │ Distributed  │  │  Counters,   │  │  Structured  │   │
│  │ tracing:     │  │  Histograms, │  │  logs with   │   │
│  │ spans, ctx   │  │  Gauges      │  │  trace_id    │   │
│  │ propagation  │  │              │  │  correlation  │   │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘   │
│         │                 │                 │            │
│         └─────────────────┼─────────────────┘            │
│                           │                              │
│                    ┌──────▼───────┐                       │
│                    │   OTLP       │                       │
│                    │  Protocol    │                       │
│                    └──────────────┘                       │
└─────────────────────────────────────────────────────────┘
Сигнал Что описывает Пример использования
Traces Путь запроса через систему (spans) Где тормозит запрос, какой сервис вызвал ошибку
Metrics Числовые агрегаты во времени RPS, error rate, latency percentiles
Logs Дискретные события Детали ошибок, аудит, отладка бизнес-логики

Архитектура OpenTelemetry

┌──────────────────────────────────────────────────────────────────────┐
│                         Application Layer                            │
│                                                                      │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐     │
│  │   PHP Service    │  │   Go Service    │  │   Node Service  │     │
│  │                  │  │                 │  │                 │     │
│  │  OTel PHP SDK    │  │  OTel Go SDK   │  │  OTel JS SDK   │     │
│  │  ┌────────────┐  │  │  ┌───────────┐ │  │  ┌───────────┐ │     │
│  │  │Auto-instr. │  │  │  │ otelhttp  │ │  │  │Auto-instr.│ │     │
│  │  │ + Manual   │  │  │  │ + otelsql │ │  │  │ + Manual  │ │     │
│  │  └─────┬──────┘  │  │  └─────┬─────┘ │  │  └─────┬─────┘ │     │
│  └────────┼─────────┘  └───────┼────────┘  └───────┼────────┘     │
│           │    OTLP/gRPC       │                    │              │
└───────────┼────────────────────┼────────────────────┼──────────────┘
            │                    │                    │
            ▼                    ▼                    ▼
┌──────────────────────────────────────────────────────────────────────┐
│                       OTel Collector                                  │
│                                                                      │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────────┐         │
│  │  Receivers    │ → │  Processors  │ → │   Exporters      │         │
│  │              │   │              │   │                  │         │
│  │  otlp (gRPC) │   │  batch       │   │  otlp → Tempo   │         │
│  │  otlp (HTTP) │   │  memory_     │   │  prometheus     │         │
│  │  prometheus  │   │    limiter   │   │  loki           │         │
│  │  zipkin      │   │  filter      │   │  otlp → Datadog │         │
│  │  jaeger      │   │  attributes  │   │  otlp → NewRelic│         │
│  └──────────────┘   │  tail_       │   └──────────────────┘         │
│                     │    sampling  │                                 │
│                     └──────────────┘                                 │
└──────────────────────────────────────────────────────────────────────┘
            │                    │                    │
            ▼                    ▼                    ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│  Grafana Tempo   │ │   Prometheus     │ │   Grafana Loki   │
│  (Traces)        │ │   (Metrics)      │ │   (Logs)         │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
         │                    │                     │
         └────────────────────┼─────────────────────┘
                              ▼
                     ┌──────────────────┐
                     │     Grafana      │
                     │  Unified view:   │
                     │  traces+metrics  │
                     │  +logs           │
                     └──────────────────┘

Три ключевых компонента:

Компонент Роль Где работает
SDK Инструментация приложения (auto + manual) В процессе приложения
Collector Приём, обработка, экспорт телеметрии Отдельный сервис (sidecar или standalone)
Backend Хранение и визуализация Jaeger, Tempo, Prometheus, Grafana

Traces — Distributed Tracing

Ключевые концепции

Концепция Описание
Trace Полный путь запроса через все сервисы (дерево spans)
Span Одна операция: HTTP-запрос, SQL-запрос, вызов функции
SpanContext trace_id + span_id + trace_flags — передаётся между сервисами
Baggage key-value пары, передаваемые по всей цепочке (tenant_id, user_id)
Parent Span Span, породивший текущий (образует дерево)

Context Propagation

Когда Service A вызывает Service B, trace context передаётся через HTTP-заголовки по стандарту W3C Trace Context:

# W3C Trace Context headers
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             │   │                                │                │
             │   │ trace_id (128-bit)              │ parent_id      │ flags
             │ version                             │ (64-bit)       │ (sampled)
             │                                     │                │

tracestate: vendor1=value1,vendor2=value2
            # vendor-specific context

Это позволяет трейсу пересекать границы сервисов, языков и даже облачных провайдеров.

Пример трейса

Trace ID: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6

[API Gateway]────────────────────────── 250ms
 ├── [Auth Middleware]─────── 12ms
 │    └── [Redis: get session]── 2ms
 ├── [OrderController]──────────────── 220ms
 │    ├── [Doctrine: SELECT order]──── 15ms
 │    ├── [PaymentService HTTP]─────────────── 180ms
 │    │    ├── [PaymentAPI: validate]──── 45ms
 │    │    ├── [PaymentAPI: charge]────── 120ms
 │    │    └── [Redis: cache token]── 3ms
 │    └── [RabbitMQ: publish event]── 5ms
 └── [Serializer: response]── 3ms

Инструментирование с OpenTelemetry SDK

Установка зависимостей:

# PHP
composer require open-telemetry/sdk \
    open-telemetry/exporter-otlp \
    open-telemetry/transport-grpc

# Go
go get go.opentelemetry.io/otel \
    go.opentelemetry.io/otel/sdk \
    go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc \
    go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp \
    go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc

# C#
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http

# Python
pip install opentelemetry-sdk \
    opentelemetry-exporter-otlp-proto-grpc \
    opentelemetry-instrumentation-asgi

В PHP SDK инициализируется автозагрузчиком по переменным окружения — отдельного bootstrap-файла нет, поэтому вся настройка живёт в .env:

# .env
OTEL_SERVICE_NAME=order-service
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1  # sample 10% of traces
OTEL_PHP_AUTOLOAD_ENABLED=true

Дальше — инициализация SDK и трейсинг входящих HTTP-запросов. В Go и Python провайдер создаётся явно (init.go / init_tracing), в PHP его поднимает автозагрузчик, а middleware только извлекает parent context и открывает серверный span; в C# оба шага делает AddOpenTelemetry() в Program.cs.

<?php
// src/Middleware/TracingMiddleware.php

declare(strict_types=1);

namespace App\Middleware;

use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SDK\Trace\TracerProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class TracingMiddleware implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => ['onRequest', 255],
            KernelEvents::RESPONSE => ['onResponse', -255],
            KernelEvents::EXCEPTION => ['onException', 0],
        ];
    }

    public function onRequest(RequestEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        $request = $event->getRequest();
        $tracer = Globals::tracerProvider()->getTracer('symfony-app');

        // Extract parent context from incoming headers (W3C Trace Context)
        $parentContext = Globals::propagator()->extract(
            $request->headers->all(),
        );

        $span = $tracer->spanBuilder($request->getMethod() . ' ' . $request->getPathInfo())
            ->setParent($parentContext)
            ->setSpanKind(SpanKind::KIND_SERVER)
            ->setAttribute('http.method', $request->getMethod())
            ->setAttribute('http.url', $request->getUri())
            ->setAttribute('http.route', $request->attributes->get('_route', 'unknown'))
            ->setAttribute('http.user_agent', $request->headers->get('User-Agent', ''))
            ->startSpan();

        // Store span in request attributes for later use
        $request->attributes->set('_otel_span', $span);
        $request->attributes->set('_otel_scope', $span->activate());
    }

    public function onResponse(ResponseEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        $request = $event->getRequest();
        $span = $request->attributes->get('_otel_span');
        $scope = $request->attributes->get('_otel_scope');

        if ($span === null) {
            return;
        }

        $response = $event->getResponse();
        $span->setAttribute('http.status_code', $response->getStatusCode());

        if ($response->getStatusCode() >= 400) {
            $span->setStatus(StatusCode::STATUS_ERROR, 'HTTP ' . $response->getStatusCode());
        }

        $scope->detach();
        $span->end();
    }

    public function onException(ExceptionEvent $event): void
    {
        $span = $event->getRequest()->attributes->get('_otel_span');
        if ($span === null) {
            return;
        }

        $span->recordException($event->getThrowable());
        $span->setStatus(StatusCode::STATUS_ERROR, $event->getThrowable()->getMessage());
    }
}
Автоматической инструментации мало: HTTP-span покажет, что запрос шёл 250 мс, но не скажет, где именно. Ручные spans вокруг бизнес-логики — загрузка заказа, вызов платёжного шлюза — дают ту самую детализацию. Обратите внимание на одинаковые имена spans и атрибутов во всех четырёх реализациях: это условие того, чтобы дашборд в Grafana работал одинаково для любого сервиса.
<?php
// src/Service/OrderService.php

declare(strict_types=1);

namespace App\Service;

use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;

final readonly class OrderService
{
    public function __construct(
        private PaymentGateway $paymentGateway,
        private OrderRepository $orderRepository,
    ) {}

    public function processOrder(int $orderId): OrderResult
    {
        $tracer = Globals::tracerProvider()->getTracer('order-service');

        // Create a span for the entire order processing
        $span = $tracer->spanBuilder('OrderService.processOrder')
            ->setSpanKind(SpanKind::KIND_INTERNAL)
            ->setAttribute('order.id', $orderId)
            ->startSpan();

        $scope = $span->activate();

        try {
            // Child span: load order from DB
            $loadSpan = $tracer->spanBuilder('DB: load order')
                ->setAttribute('db.system', 'postgresql')
                ->setAttribute('db.operation', 'SELECT')
                ->startSpan();

            $order = $this->orderRepository->find($orderId);
            $loadSpan->setAttribute('order.total', $order->getTotal());
            $loadSpan->end();

            if ($order === null) {
                throw new \RuntimeException("Order {$orderId} not found");
            }

            // Child span: process payment (external API call)
            $paymentSpan = $tracer->spanBuilder('Payment: charge')
                ->setSpanKind(SpanKind::KIND_CLIENT)
                ->setAttribute('payment.amount', $order->getTotal())
                ->setAttribute('payment.currency', 'USD')
                ->startSpan();

            $paymentResult = $this->paymentGateway->charge($order);
            $paymentSpan->setAttribute('payment.transaction_id', $paymentResult->transactionId);
            $paymentSpan->end();

            $span->setAttribute('order.status', 'completed');

            return new OrderResult(success: true, transactionId: $paymentResult->transactionId);
        } catch (\Throwable $e) {
            $span->recordException($e);
            $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());

            return new OrderResult(success: false, error: $e->getMessage());
        } finally {
            $scope->detach();
            $span->end();
        }
    }
}
### Cross-service propagation

Когда сервис вызывает другой сервис, OTel SDK автоматически инжектит trace context в заголовки — достаточно использовать инструментированный HTTP-клиент и передавать в него контекст текущего запроса:

// internal/client/payment.go
package client

import (
	"context"
	"fmt"
	"net/http"

	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

// PaymentClient wraps HTTP client with OTel instrumentation.
// Automatically propagates trace context to downstream services.
type PaymentClient struct {
	httpClient *http.Client
	baseURL    string
}

func NewPaymentClient(baseURL string) *PaymentClient {
	return &PaymentClient{
		// otelhttp.NewTransport wraps http.DefaultTransport:
		// - creates client span for outgoing request
		// - injects W3C traceparent header
		// - records duration and status
		httpClient: &http.Client{
			Transport: otelhttp.NewTransport(http.DefaultTransport),
		},
		baseURL: baseURL,
	}
}

func (c *PaymentClient) Charge(ctx context.Context, amount float64) error {
	// Context carries the current span — otelhttp will create child span
	// and add traceparent header to the outgoing request
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		fmt.Sprintf("%s/api/charge", c.baseURL), nil)
	if err != nil {
		return err
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("payment failed: status %d", resp.StatusCode)
	}

	return nil
}
## Metrics — OTel Metrics API

OTel Metrics API определяет типы инструментов, совместимые с Prometheus:

OTel тип Prometheus аналог Описание
Counter Counter Только растёт (кол-во запросов)
UpDownCounter Gauge Растёт и уменьшается (active connections)
Histogram Histogram Распределение значений (latency)
Gauge Gauge Текущее значение (температура, queue size)

Пользовательские метрики

Инструменты создаются один раз при старте сервиса и переиспользуются: создание counter'а на каждый вызов приводит к дублированию временных рядов. Имена и единицы измерения одинаковы во всех реализациях — Prometheus видит их как одну метрику независимо от языка сервиса.

<?php
// src/Service/MetricsService.php

declare(strict_types=1);

namespace App\Service;

use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Metrics\CounterInterface;
use OpenTelemetry\API\Metrics\HistogramInterface;
use OpenTelemetry\API\Metrics\UpDownCounterInterface;

final class MetricsService
{
    private CounterInterface $ordersCreated;
    private HistogramInterface $orderProcessingDuration;
    private UpDownCounterInterface $activeConnections;

    public function __construct()
    {
        $meter = Globals::meterProvider()->getMeter('order-service');

        // Counter: number of created orders
        $this->ordersCreated = $meter->createCounter(
            'orders_created_total',
            'orders',
            'Total number of created orders'
        );

        // Histogram: order processing duration
        $this->orderProcessingDuration = $meter->createHistogram(
            'order_processing_duration_seconds',
            's',
            'Time to process an order'
        );

        // UpDownCounter: active DB connections
        $this->activeConnections = $meter->createUpDownCounter(
            'db_active_connections',
            'connections',
            'Number of active database connections'
        );
    }

    public function recordOrderCreated(string $paymentMethod, string $region): void
    {
        $this->ordersCreated->add(1, [
            'payment_method' => $paymentMethod,
            'region' => $region,
        ]);
    }

    public function recordProcessingDuration(float $seconds, string $orderType): void
    {
        $this->orderProcessingDuration->record($seconds, [
            'order_type' => $orderType,
        ]);
    }

    public function connectionOpened(): void
    {
        $this->activeConnections->add(1);
    }

    public function connectionClosed(): void
    {
        $this->activeConnections->add(-1);
    }
}
## Logs — корреляция с трейсами

Главная ценность OTel для логов — корреляция: автоматическое добавление trace_id и span_id в каждую запись лога. Это позволяет перейти от лога к трейсу одним кликом в Grafana.

Корреляция логов с трейсами

Идея одна во всех языках: взять span context из текущего контекста выполнения и дописать trace_id/span_id в каждую запись лога. В PHP это processor Monolog, в Go — обёртка над slog.Handler, в C# — logging scope поверх Activity.Current, в Python — logging.Filter.

<?php
// src/Logger/TraceContextProcessor.php

declare(strict_types=1);

namespace App\Logger;

use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\Context\Context;

/**
 * Adds OpenTelemetry trace_id and span_id to every log record.
 * Enables jumping from log entry to full trace in Grafana.
 */
final class TraceContextProcessor implements ProcessorInterface
{
    public function __invoke(LogRecord $record): LogRecord
    {
        $span = Span::fromContext(Context::getCurrent());
        $spanContext = $span->getContext();

        if (!$spanContext->isValid()) {
            return $record;
        }

        return $record->with(
            extra: array_merge($record->extra, [
                'trace_id' => $spanContext->getTraceId(),
                'span_id' => $spanContext->getSpanId(),
                'trace_flags' => $spanContext->getTraceFlags(),
            ]),
        );
    }
}
Конфигурация Monolog в Symfony:
# config/packages/monolog.yaml
monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            formatter: 'monolog.formatter.json'
            channels: ["!event"]
    processors:
        - App\Logger\TraceContextProcessor

Результат — каждая запись лога содержит trace_id:

{
  "message": "Order processed successfully",
  "context": {"order_id": 12345, "total": 99.99},
  "level": 200,
  "level_name": "INFO",
  "channel": "app",
  "datetime": "2026-03-01T10:23:45.123456+00:00",
  "extra": {
    "trace_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "span_id": "00f067aa0ba902b7",
    "trace_flags": 1
  }
}

Использование Go-обработчика:

// Setup
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})
logger := slog.New(logger.NewTraceHandler(jsonHandler))
slog.SetDefault(logger)

// In request handler — ctx carries the active span
slog.InfoContext(ctx, "order processed",
    slog.Int("order_id", 12345),
    slog.Float64("total", 99.99),
)

// Output:
// {"time":"2026-03-01T10:23:45Z","level":"INFO","msg":"order processed",
//  "order_id":12345,"total":99.99,
//  "trace_id":"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6","span_id":"00f067aa0ba902b7"}

OTel Collector Configuration

Полная конфигурация Collector с pipelines для всех трёх сигналов:

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  # Scrape Prometheus targets (optional — hybrid mode)
  prometheus:
    config:
      scrape_configs:
        - job_name: 'otel-collector'
          scrape_interval: 15s
          static_configs:
            - targets: ['localhost:8888']

processors:
  # Group spans/metrics/logs into batches for efficient export
  batch:
    send_batch_size: 1024
    send_batch_max_size: 2048
    timeout: 5s

  # Protect Collector from OOM
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128

  # Add common attributes to all telemetry
  attributes:
    actions:
      - key: environment
        value: production
        action: upsert
      - key: cluster
        value: eu-west-1
        action: upsert

  # Tail-based sampling: keep all error traces, sample 10% of success
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    policies:
      - name: errors-always
        type: status_code
        status_code: {status_codes: [ERROR]}
      - name: slow-traces
        type: latency
        latency: {threshold_ms: 2000}
      - name: percentage-sample
        type: probabilistic
        probabilistic: {sampling_percentage: 10}

exporters:
  # Traces → Grafana Tempo
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

  # Metrics → Prometheus (Collector exposes /metrics endpoint)
  prometheus:
    endpoint: "0.0.0.0:8889"
    resource_to_telemetry_conversion:
      enabled: true

  # Logs → Grafana Loki
  loki:
    endpoint: http://loki:3100/loki/api/v1/push
    labels:
      attributes:
        service.name: "service"
        level: "severity"

  # Debug: print to stdout (dev only)
  debug:
    verbosity: detailed

service:
  telemetry:
    logs:
      level: info
    metrics:
      address: 0.0.0.0:8888

  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch, attributes]
      exporters: [otlp/tempo]

    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, batch, attributes]
      exporters: [prometheus]

    logs:
      receivers: [otlp]
      processors: [memory_limiter, batch, attributes]
      exporters: [loki]

Docker Compose для local development

# docker-compose.observability.yaml
services:
  # OTel Collector — central telemetry pipeline
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.96.0
    command: ["--config", "/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "8889:8889"   # Prometheus metrics
    depends_on:
      tempo:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:13133/"]
      interval: 10s
      timeout: 5s
      retries: 3

  # Grafana Tempo — trace storage
  tempo:
    image: grafana/tempo:2.4.0
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./tempo.yaml:/etc/tempo.yaml:ro
      - tempo-data:/var/tempo
    ports:
      - "3200:3200"   # Tempo query API
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3200/ready"]
      interval: 10s
      timeout: 5s
      retries: 3

  # Prometheus — metrics storage
  prometheus:
    image: prom/prometheus:v2.50.0
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'
      - '--web.enable-remote-write-receiver'
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:9090/-/ready"]
      interval: 10s
      timeout: 5s
      retries: 3

  # Grafana Loki — log aggregation
  loki:
    image: grafana/loki:2.9.4
    ports:
      - "3100:3100"
    volumes:
      - loki-data:/loki
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3100/ready"]
      interval: 10s
      timeout: 5s
      retries: 3

  # Grafana — visualization
  grafana:
    image: grafana/grafana:10.3.0
    ports:
      - "3000:3000"
    environment:
      GF_AUTH_ANONYMOUS_ENABLED: "true"
      GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
    depends_on:
      - prometheus
      - tempo
      - loki

volumes:
  tempo-data:
  prometheus-data:
  loki-data:
  grafana-data:

Best Practices

Стратегии сэмплирования

Стратегия Описание Когда использовать
Always On Записывать 100% трейсов Dev/staging, дебаг
Head-based Решение о сэмплировании в начале трейса Основной подход для production
Tail-based Решение после завершения трейса (Collector) Когда нужны ВСЕ ошибки + % success
Parent-based Наследовать решение родительского span Кросс-сервисная согласованность

Tail-based sampling — самый мощный подход: Collector видит весь трейс целиком и может решить сохранить его, если хоть один span содержит ошибку или высокий latency.

Semantic Conventions

OTel определяет стандартные имена атрибутов. Используйте их вместо произвольных:

Атрибут Описание
http.method HTTP метод (GET, POST)
http.status_code HTTP код ответа
http.route Шаблон маршрута (/api/orders/{id})
db.system Тип БД (postgresql, redis, mongodb)
db.operation Операция (SELECT, INSERT, UPDATE)
db.sql.table Имя таблицы
messaging.system Система сообщений (rabbitmq, kafka)
messaging.operation Операция (publish, receive)
rpc.system RPC система (grpc)
rpc.method Имя метода

Performance overhead

Компонент Overhead Как минимизировать
SDK (auto-instrumentation) 1-3% CPU Сэмплирование, batch processing
Context propagation <0.1ms на запрос Уже минимально
Collector 100-500MB RAM memory_limiter, batch processor
Network (OTLP gRPC) <1% bandwidth Compression, batch export

Правило: OTel overhead не должен превышать 2% от базового latency. Если превышает — увеличьте интервал batch export или снизьте sampling rate.

Проверь себя

5 из 8

В конфигурации OTel Collector, что определяет блок `service.pipelines`?

Чем tail-based sampling отличается от head-based?

Какой максимальный overhead от OpenTelemetry SDK считается приемлемым для production?

Какой OTel metric type подходит для отслеживания количества активных подключений к базе данных?

Какой HTTP-заголовок используется для передачи trace context между сервисами по стандарту W3C?