Настройка APM — мониторинга производительности приложения
APM (Application Performance Monitoring) отслеживает производительность на уровне кода: медленные запросы, узкие места в базе данных, время выполнения функций, распределённые трассировки между микросервисами.
Что даёт APM
Трассировки — полный путь запроса: HTTP → роутер → контроллер → ORM → SQL → Redis → HTTP ответ. Каждый шаг занимает измеренное время. Медленный SQL-запрос виден сразу без анализа логов.
Профилирование — flamegraph по функциям: куда уходит процессорное время.
Метрики SLO — apdex (доля запросов быстрее порога), p50/p95/p99 latency, error rate.
OpenTelemetry: стандарт для APM
OpenTelemetry — вендор-нейтральный стандарт. Один SDK → данные в любой бэкенд: Jaeger, Zipkin, Datadog, New Relic, Grafana Tempo.
// composer.json
// "open-telemetry/sdk": "^1.0",
// "open-telemetry/exporter-otlp": "^1.0"
// bootstrap/telemetry.php
use OpenTelemetry\API\Globals;
use OpenTelemetry\SDK\Trace\TracerProviderFactory;
use OpenTelemetry\Contrib\Otlp\OtlpHttpSpanExporter;
$exporter = OtlpHttpSpanExporter::fromConnectionString(
'http://otel-collector:4318',
'myapp',
'1.0.0'
);
$tracerProvider = (new TracerProviderFactory())->create($exporter);
Globals::registerInitializer(fn() => $tracerProvider);
Grafana Tempo + OpenTelemetry
Self-hosted альтернатива Datadog APM:
# docker-compose.tracing.yml
services:
tempo:
image: grafana/tempo:2.4.0
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./monitoring/tempo.yaml:/etc/tempo.yaml
- tempo_data:/var/tempo
ports:
- "3200:3200" # HTTP API
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
otel-collector:
image: otel/opentelemetry-collector-contrib:0.97.0
volumes:
- ./monitoring/otel-collector.yaml:/etc/otelcol-contrib/config.yaml
ports:
- "4317:4317"
- "4318:4318"
volumes:
tempo_data:
# monitoring/otel-collector.yaml
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
batch:
timeout: 1s
exporters:
otlp/tempo:
endpoint: tempo:4317
tls: { insecure: true }
prometheus:
endpoint: "0.0.0.0:8889"
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
Laravel: трассировка запросов
// Middleware для трассировки HTTP запросов
class TraceMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$tracer = Globals::tracerProvider()->getTracer('myapp');
$span = $tracer->spanBuilder($request->method() . ' ' . $request->path())
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
$span->setAttributes([
'http.method' => $request->method(),
'http.url' => $request->fullUrl(),
'http.route' => $request->route()?->uri(),
'user.id' => auth()->id(),
]);
$scope = $span->activate();
try {
$response = $next($request);
$span->setAttribute('http.status_code', $response->getStatusCode());
return $response;
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR);
throw $e;
} finally {
$scope->detach();
$span->end();
}
}
}
// Трассировка SQL запросов
DB::listen(function (QueryExecuted $query) {
$span = Globals::tracerProvider()
->getTracer('myapp')
->spanBuilder('db.query')
->setSpanKind(SpanKind::KIND_CLIENT)
->startSpan();
$span->setAttributes([
'db.system' => 'postgresql',
'db.statement' => $query->sql,
'db.duration' => $query->time,
]);
$span->end();
});
Node.js: @opentelemetry/auto-instrumentations
// tracing.ts — импортировать первым!
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'myapp-api',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION || '1.0.0',
environment: process.env.NODE_ENV || 'production',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-express': { enabled: true },
'@opentelemetry/instrumentation-pg': { enabled: true },
'@opentelemetry/instrumentation-redis': { enabled: true },
'@opentelemetry/instrumentation-http': { enabled: true },
})],
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown());
Sentry Performance (простой вариант)
Если полноценный APM избыточен, Sentry Performance даёт базовые трассировки с minimal setup:
// config/sentry.php
'dsn' => env('SENTRY_LARAVEL_DSN'),
'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE', 0.1), // 10% запросов
'profiles_sample_rate' => 0.1,
Sentry автоматически инструментирует Eloquent, HTTP запросы, очереди.
Blackfire: профилирование PHP
Для deep dive в производительность конкретного эндпоинта:
# Профилировать HTTP запрос
blackfire curl https://example.com/api/heavy-endpoint
# Continuous profiling в production
blackfire agent --server-id=... --server-token=...
Ключевые SLO метрики
| Метрика | Хорошо | Требует внимания |
|---|---|---|
| p50 latency | <100 мс | >200 мс |
| p95 latency | <500 мс | >1 сек |
| p99 latency | <1 сек | >3 сек |
| Error rate | <0.1% | >1% |
| Apdex (t=0.5s) | >0.95 | <0.85 |
Срок реализации
| Задача | Срок |
|---|---|
| Sentry Performance (быстрый старт) | 0.5 дня |
| OpenTelemetry + Grafana Tempo (self-hosted) | 3–4 дня |
| Datadog/New Relic APM | 1–2 дня |
| Полная инструментация (HTTP + DB + Redis + очереди) | 2–3 дня |
| SLO дашборды и алерты | +1–2 дня |







