OpenTelemetry давно вышел за рамки эксперимента и стал стандартом де-факто для телеметрии: трассировки, метрики и логи в единой модели и протоколах. На собственном VDS это особенно удобно: вы контролируете конфигурацию, безопасность и источники затрат. Ниже — как развернуть OpenTelemetry Collector, настроить сбор трассировок и метрик и подключить PHP, Node.js и Go.
Зачем OpenTelemetry на VDS и как всё устроено
Классическая архитектура состоит из трёх слоёв:
- SDK/автоинструментирование в приложении (PHP/Node/Go), которое формирует спаны и метрики.
- OpenTelemetry Collector — универсальный агент/шлюз, принимающий данные по OTLP и отправляющий их в хранилища. Он же нормализует, семплирует, обогащает, буферизует.
- Бэкенды наблюдаемости: хранилища трассировок и метрик. В статье сфокусируемся на Collector и приложениях; конкретный бэкенд подставите под свои требования.
Плюсы Collector на VDS:
- Единая точка приёма: приложения шлют в локальный Collector, меньше завязок на внешние адреса и проще ACL.
- Гибкость: смена экспортёра без перезапуска приложений.
- Производительность: батчирование, ретраи и контроль «напора» телеметрии.
- Безопасность: TLS, клиентские сертификаты, приватные сети внутри VDS.
Разворачиваем OpenTelemetry Collector на Ubuntu/Debian
Понадобится бинарник otelcol-contrib (вариант contrib с расширенным набором ресиверов/экспортёров), отдельный системный пользователь, директории для конфигурации и данных и unit для systemd.
Создаём пользователя и директории
sudo useradd --system --no-create-home --shell /usr/sbin/nologin otel
sudo mkdir -p /etc/otelcol-contrib
sudo mkdir -p /var/lib/otelcol-contrib
sudo chown -R otel:otel /etc/otelcol-contrib /var/lib/otelcol-contrib
Скачайте релизный архив otelcol-contrib под вашу архитектуру, распакуйте бинарник в /usr/local/bin/otelcol-contrib и назначьте права:
sudo mv otelcol-contrib /usr/local/bin/otelcol-contrib
sudo chown root:root /usr/local/bin/otelcol-contrib
sudo chmod 755 /usr/local/bin/otelcol-contrib
Systemd unit
[Unit]
Description=OpenTelemetry Collector Contrib
After=network-online.target
Wants=network-online.target
[Service]
User=otel
Group=otel
ExecStart=/usr/local/bin/otelcol-contrib --config /etc/otelcol-contrib/config.yaml
Restart=on-failure
RestartSec=3s
MemoryLimit=512M
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
Сохраните как /etc/systemd/system/otelcol-contrib.service, затем:
sudo systemctl daemon-reload
sudo systemctl enable --now otelcol-contrib
Базовая конфигурация Collector
Минимальный config.yaml с приёмом OTLP по gRPC/HTTP, сбором метрик хоста и экспозом метрик Collector на порту 8889. Трассировки для отладки выводим в лог (logging), метрики — через prometheus-экспортёр.
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
memory: {}
disk: {}
filesystem: {}
load: {}
network: {}
exporters:
logging:
loglevel: info
prometheus:
endpoint: 0.0.0.0:8889
namespace: otelcol
processors:
batch:
send_batch_size: 8192
timeout: 5s
memory_limiter:
check_interval: 5s
limit_percentage: 75
spike_limit_percentage: 15
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 127.0.0.1:1777
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, pprof]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [logging]
metrics:
receivers: [otlp, hostmetrics]
processors: [memory_limiter, batch]
exporters: [prometheus]
Перезапустите Collector, чтобы применить конфиг:
sudo systemctl restart otelcol-contrib
sudo systemctl status otelcol-contrib
Открываем порты в фаерволе
Если используете UFW, откройте 4317 (gRPC), 4318 (HTTP), 8889 (экспорт метрик Collector). Health-check 13133 можно оставить доступным только локально.
sudo ufw allow 4317/tcp comment "OTLP gRPC"
sudo ufw allow 4318/tcp comment "OTLP HTTP"
sudo ufw allow 8889/tcp comment "Prometheus exporter"
Подключаем PHP: трассировки и метрики
В PHP доступны автоинструментирование расширениями и ручное инструментирование через SDK. Для явного контроля ниже — минимальный пример ручного способа. В любом случае убедитесь, что у PHP-процессов выставлены OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_SERVICE_NAME и при необходимости OTEL_RESOURCE_ATTRIBUTES.
Переменные окружения в PHP-FPM
Добавьте в пул www.conf (или ваш пул):
env[OTEL_EXPORTER_OTLP_ENDPOINT]=http://127.0.0.1:4318
env[OTEL_EXPORTER_OTLP_PROTOCOL]=http/protobuf
env[OTEL_SERVICE_NAME]=php-frontend
env[OTEL_RESOURCE_ATTRIBUTES]=deployment.environment=prod,service.version=1.2.3
Перезапустите FPM:
sudo systemctl reload php8.2-fpm
Минимальный пример трассировки на PHP (SDK)
<?php
require __DIR__ . '/vendor/autoload.php';
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\Contrib\Otlp\Exporter as OtlpExporter;
$exporter = new OtlpExporter('http://127.0.0.1:4318/v1/traces');
$tracerProvider = new TracerProvider(new SimpleSpanProcessor($exporter));
Globals::registerTracerProvider($tracerProvider);
$tracer = Globals::tracerProvider()->getTracer('php-demo');
$root = $tracer->spanBuilder('http.request')
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
try {
usleep(100000);
} finally {
$root->end();
$tracerProvider->shutdown();
}
Если используете middleware фреймворка, подключайте инициализацию SDK в ранней точке загрузки. Для метрик создайте счётчики и гистограммы в нужных местах кода (время рендеринга, обработанные задачи, размер ответа).

Node.js: автоинструментирование и метрики
В Node.js проще всего стартовать с @opentelemetry/sdk-node и набором автоинструментов. Те же базовые переменные: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_SERVICE_NAME, а также OTEL_TRACES_SAMPLER при необходимости. Если разворачиваете сервисы на ARM, обратите внимание на заметки по производительности в материале про оптимизацию ARM VDS: ARM VDS: PHP/Node производительность.
Инициализация трассировок
// tracer.js
'use strict';
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const traceExporter = new OTLPTraceExporter({ url: 'http://127.0.0.1:4318/v1/traces' });
const sdk = new NodeSDK({
traceExporter,
instrumentations: [getNodeAutoInstrumentations()],
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'node-api',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.DEPLOYMENT_ENVIRONMENT || 'prod'
})
});
sdk.start().then(() => {
console.log('OTel SDK started');
}).catch((err) => console.error(err));
process.on('SIGTERM', async () => {
await sdk.shutdown();
process.exit(0);
});
Старт приложения с предварительной загрузкой tracer.js:
OTEL_SERVICE_NAME=node-api OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 node -r ./tracer.js app.js
Пример метрик в Node.js
// metrics.js
'use strict';
const { MeterProvider } = require('@opentelemetry/sdk-metrics');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { AggregationTemporality } = require('@opentelemetry/sdk-metrics');
const metricExporter = new OTLPMetricExporter({ url: 'http://127.0.0.1:4318/v1/metrics', temporalityPreference: AggregationTemporality.CUMULATIVE });
const meterProvider = new MeterProvider();
meterProvider.addMetricReader(metricExporter);
const meter = meterProvider.getMeter('node-metrics');
const requests = meter.createCounter('http_requests_total', { description: 'HTTP requests' });
function onRequest(path) {
requests.add(1, { path });
}
module.exports = { onRequest };
Следите за кардинальностью лейблов: не добавляйте «сырые» пути с уникальными идентификаторами. Нормализуйте путь, например, /orders/:id вместо /orders/123.
Go: чистое SDK, OTLP и контекст
В Go SDK стабилен и лаконичен: создаём экспортер OTLP (HTTP или gRPC), настраиваем TracerProvider и MeterProvider. Обязательно прокидывайте context.Context по стеку вызовов — иначе потеряете связность спанов.
Пример трассировки в Go
package main
import (
"context"
"log"
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
func initTracer(ctx context.Context) func(context.Context) error {
exp, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpointURL("http://127.0.0.1:4318/v1/traces"))
if err != nil { log.Fatal(err) }
res, _ := resource.Merge(resource.Default(), resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("go-api"),
attribute.String("deployment.environment", "prod"),
))
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
return tp.Shutdown
}
func main() {
ctx := context.Background()
shutdown := initTracer(ctx)
defer func() { _ = shutdown(ctx) }()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Tracer("go-api").Start(r.Context(), "http.request")
defer span.End()
time.Sleep(120 * time.Millisecond)
span.SetAttributes(attribute.String("path", r.URL.Path))
_, _ = w.Write([]byte("ok"))
_ = ctx
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Метрики в Go настраиваются аналогично: поднимите MeterProvider и используйте OTLP HTTP на http://127.0.0.1:4318/v1/metrics; Collector примет и отдаст наружу через prometheus-экспортёр.
Пропагация контекста через прокси и балансировщики
Главное в трассировках — сквозная корреляция. Сохраняйте и передавайте заголовки W3C Trace Context: traceparent и baggage. Большинство HTTP-прокси (включая Nginx) проксируют неизвестные заголовки, но при строгих политиках добавьте их явно.
location /api/ {
proxy_pass http://backend;
proxy_set_header traceparent $http_traceparent;
proxy_set_header baggage $http_baggage;
proxy_set_header x-request-id $request_id;
}
Логи веб-сервера с x-request-id упростят сопоставление с трассировками. Для фоновых воркеров и демонов пригодится систематизация запуска под systemd: Supervisor/systemd: воркеры и перезапуски.
Сэмплирование и производительность
Собирать 100% трассировок в проде часто избыточно. Начните со 100% в стейджинге и сниженного процента в проде. Есть два уровня сэмплинга:
- На стороне SDK:
OTEL_TRACES_SAMPLER=parentbased_traceidratioсOTEL_TRACES_SAMPLER_ARG(например,0.1для 10%). - На стороне Collector: процессоры
tail_sampling(по атрибутам/длительности/статусам) иprobabilistic_sampler.
Пример tail_sampling в Collector: оставляем ошибки и долгие запросы, остальное — до 5%.
processors:
tail_sampling:
decision_wait: 5s
num_traces: 50000
policies:
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
- name: long_traces
type: latency
latency:
threshold_ms: 500
- name: default_prob
type: probabilistic
probabilistic:
sampling_percentage: 5
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [logging]
Для метрик следите за кардинальностью лейблов: высококардинальные значения (UUID, e-mail, полный путь) «размножают» ряды и перегружают систему. Нормализуйте лейблы и используйте гистограммы для латенси и размеров.
TLS и безопасность транспорта
OTLP поддерживает шифрование. Рекомендации:
- Слушайте только
127.0.0.1и/или внутренний адрес VDS. - Ограничьте доступ по фаерволу: разрешайте только хостам приложений.
- Включите TLS на
otlp-ресивере и храните ключи с ограниченными правами. - При междатацентровом обмене используйте mTLS.
Включение TLS в otlp с сертификатами:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
tls:
cert_file: /etc/otelcol-contrib/tls/cert.pem
key_file: /etc/otelcol-contrib/tls/key.pem
http:
endpoint: 0.0.0.0:4318
tls:
cert_file: /etc/otelcol-contrib/tls/cert.pem
key_file: /etc/otelcol-contrib/tls/key.pem
Если нужен публичный TLS, заранее оформите и обновляйте SSL-сертификаты.
Надёжность: буферы, ретраи, лимиты
Collector переживает кратковременные сбои экспортёров, аккумулируя данные в памяти. Для прод-среды задайте лимиты памяти (memory_limiter) и разумный размер батча в batch-процессоре. В приложениях включайте батч-экспортёры, снижайте частоту экспорта метрик (30–60 секунд), не публикуйте метрики чаще, чем нужно.
Плохой запах: синхронный экспорт OTLP из HTTP-обработчика. Используйте асинхронные батч-экспортёры.
Проверка и отладка
- Проверьте
health_checkCollector: статус сервиса и метрики на0.0.0.0:8888. - Смотрите логи
logging-экспортёра: входящие спаны и метрики. - Временно поднимите сэмплер до 100% в стейджинге.
- Проверьте синхронизацию времени: сдвиги ломают окна метрик.
- Убедитесь, что приложение и Collector согласованы по OTLP: gRPC vs HTTP.

Типичные проблемы
- Пустые трассы: забыты заголовки
traceparentпри проксировании или всегда создаются новые корневые спаны. - Большие задержки экспорта: слишком маленький
send_batch_sizeили слишком частый экспорт метрик. - Взрыв кардинальности: лейблы с уникальными значениями.
- Сбои в PHP при поздней инициализации: SDK подключайте до логики приложения и корректно завершайте.
Пример «боевого» конфига Collector
Добавим retry для экспортёров, transform для нормализации атрибутов, разнесём пайплайны. Это удобная основа для прод-среды.
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 30s
scrapers:
cpu: {}
memory: {}
disk: {}
filesystem: {}
network: {}
processors:
memory_limiter:
check_interval: 2s
limit_percentage: 70
spike_limit_percentage: 10
batch:
send_batch_size: 4096
send_batch_max_size: 8192
timeout: 5s
transform/traces:
trace_statements:
- context: span
statements:
- set(attributes["service.env"], attributes["deployment.environment"]) where attributes["deployment.environment"] != nil
exporters:
logging:
loglevel: warn
prometheus:
endpoint: 0.0.0.0:8889
namespace: otel
send_timestamps: true
service:
telemetry:
logs:
level: info
pipelines:
metrics:
receivers: [otlp, hostmetrics]
processors: [memory_limiter, batch]
exporters: [prometheus]
traces:
receivers: [otlp]
processors: [memory_limiter, transform/traces, batch]
exporters: [logging]
Далее замените logging-экспортёр трассировок на нужный вам (OTLP, Zipkin-совместимый и т. п.) и при необходимости добавьте аутентификацию.
Практические советы по внедрению
- Начните с одного сервиса и «сквозной» ручки (endpoint), убедитесь, что пропагация работает, затем масштабируйте.
- Стандартизируйте ресурс:
service.name,service.version,deployment.environment,host.name. - Сразу договоритесь об именовании метрик и лейблов; избегайте скрытых динамических значений.
- Разделяйте уровни сэмплинга: SDK — грубый рычаг, Collector — тонкая настройка по атрибутам/длительности.
- Следите за GC и выделением памяти: огромные атрибуты в спанах ведут к паузам.
- Лимитируйте размеры батчей и включите ретраи в экспортёрах — дешевле, чем терять критичные спаны.
Чеклист
- Collector установлен как сервис, слушает
4317/4318, метрики на8889. - Фаервол пропускает нужные порты только от доверенных хостов.
- Приложения PHP/Node/Go настроены с
OTEL_EXPORTER_OTLP_ENDPOINTиOTEL_SERVICE_NAME. - Пропагация
traceparentиbaggageчерез прокси подтверждена. - Сэмплирование согласовано: SDK и/или Collector; есть правила на ошибки и долгие запросы.
- Метрики без высококардинальных лейблов; латенси — гистограммы.
- TLS и/или приватная сеть для OTLP включены, ключи защищены.
- Логи Collector чистые, health-check зелёный, тестовые спаны видны в экспортёре.
С такой базой вы быстро получите сквозную наблюдаемость: где тормозит запрос, какой микросервис виноват, как ведёт себя система на уровне CPU/памяти/сети и как меняются SLO. OpenTelemetry масштабируется и не привязывает к вендору: меняйте бэкенды, не трогая код — именно ради этого на VDS и разворачивается Collector.


