Бандл приносит четыре observability-стека из коробки — все opt-in, все слушают одни и те же PSR-14 события. Стеки можно комбинировать; ничто ни с чем не конфликтует.
| Стек | Включатель | Где удобен |
|---|---|---|
| PSR-14 события | всегда работает | свой listener |
| Symfony Web Profiler | json_rpc_server.profiler.enabled (только debug) |
локальная разработка |
| PSR-3 логирование | json_rpc_server.logging.enabled |
структурированные app-логи |
| Sentry | json_rpc_server.sentry.enabled |
issue-трекинг с breadcrumbs / тегами / спанами |
| OpenTelemetry | json_rpc_server.opentelemetry.enabled |
vendor-нейтральные трейсы / метрики / propagation |
Dispatcher эмитит три события на каждый RPC-вызов:
namespace Knetesin\JsonRpcServerBundle\Event;
final readonly class MethodInvocationStartedEvent {
public MethodMetadata $method;
public RpcParams $params;
}
final readonly class MethodInvocationCompletedEvent {
public MethodMetadata $method;
public RpcParams $params;
public mixed $result; // нормализованная форма
public float $durationSec;
public bool $cacheHit;
}
final readonly class MethodInvocationFailedEvent {
public MethodMetadata $method;
public RpcParams $params;
public \Throwable $exception;
public float $durationSec;
}
Started фаирится, как только известно имя метода (после registry lookup,
до resolution аргументов). Затем ровно одно из Completed или Failed.
Поэтому клиентские ошибки (InvalidParamsException / -32602,
AccessDeniedException, rate limit, denormalize) всё равно дают
rpc.call.failed, строку в Web Profiler RPC и опционально Sentry/OTel — handler
может не выполниться, observability — да.
Для стриминговых методов Completed фаирится сразу как итератор возвращён
(до начала итерации). Три дополнительных события дают трекать саму итерацию:
final readonly class StreamRowEmittedEvent {
public MethodMetadata $method;
public mixed $row;
public int $index;
}
final readonly class StreamIterationCompletedEvent {
public MethodMetadata $method;
public int $rowCount;
public float $durationSec;
}
final readonly class StreamIterationFailedEvent {
public MethodMetadata $method;
public \Throwable $exception;
public int $rowCount;
public float $durationSec;
}
use Knetesin\JsonRpcServerBundle\Event\MethodInvocationCompletedEvent;
use Knetesin\JsonRpcServerBundle\Event\MethodInvocationFailedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class AuditSubscriber implements EventSubscriberInterface
{
public function __construct(private readonly Audit $audit) {}
public static function getSubscribedEvents(): array
{
return [
MethodInvocationCompletedEvent::class => 'onCompleted',
MethodInvocationFailedEvent::class => 'onFailed',
];
}
public function onCompleted(MethodInvocationCompletedEvent $e): void
{
if ($this->audit->isAudited($e->method->name)) {
$this->audit->log(
method: $e->method->name,
params: $e->params->all(),
result: $e->result,
);
}
}
public function onFailed(MethodInvocationFailedEvent $e): void { /* … */ }
}
С дефолтным autoconfigure: true ничего больше регистрировать не нужно.
На cache hit:
Started (params)
Completed (params, cached result, cacheHit=true, duration=0.0)
Handler не вызывается. На miss handler отрабатывает и Completed фаирится
со свежим результатом и cacheHit=false. На failure фаирится Failed —
Completed и Failed никогда не фаирятся для одного и того же вызова.
Когда kernel.debug = true и установлен symfony/web-profiler-bundle,
бандл добавляет панель RPC:
json_rpc_server:
profiler:
enabled: true # дефолт; no-op вне kernel.debug
В продакшене subscriber зарегистрирован, но никогда не вызывается —
framework-профайлер выключен, нагрузка нулевая. Оставляйте enabled: true.
composer require --dev symfony/web-profiler-bundle
Встроенный subscriber пишет одну строку лога на исход — без своего кода.
json_rpc_server:
logging:
enabled: true
channel: ~ # null = дефолтный сервис `logger`
# например monolog.logger.rpc для отдельного канала
level_started: debug # rpc.call.started
level_completed: info # rpc.call.completed
level_failed: warning # rpc.call.failed
log_params: true # включать params в контекст
log_result: false # включать result (бывает шумно)
slow_threshold_ms: ~ # эскалировать медленные вызовы до level_failed
Пример вывода:
[2026-05-22T15:00:00+00:00] app.INFO: rpc.call.completed
{"method":"user.update","duration_ms":42,"cache_hit":false,"params":{"id":7}}
# config/packages/monolog.yaml
monolog:
channels: ['rpc']
handlers:
rpc_file:
type: stream
path: '%kernel.logs_dir%/rpc.log'
channels: ['rpc']
# config/packages/json_rpc_server.yaml
json_rpc_server:
logging:
enabled: true
channel: monolog.logger.rpc
Если встроенного subscriber’а не хватает (редакция PII-полей, sampling, свои структурированные поля) — выключите его и напишите свой; события и есть канонический extension point.
Подключается через sentry/sentry-symfony.
composer require sentry/sentry-symfony
json_rpc_server:
sentry:
enabled: true
breadcrumbs: true # rpc-категория breadcrumb на каждый вызов
tag_method: true # ставит тег rpc.method на время вызова
transactions: false # child-спаны для Performance Monitoring
ignore_exceptions: # клиентские ошибки в Sentry не идут
- Knetesin\JsonRpcServerBundle\Exception\InvalidParamsException
- Knetesin\JsonRpcServerBundle\Exception\InvalidRequestException
- Knetesin\JsonRpcServerBundle\Exception\MethodNotFoundException
- Knetesin\JsonRpcServerBundle\Exception\ParseException
- Knetesin\JsonRpcServerBundle\Exception\AccessDeniedException
- Knetesin\JsonRpcServerBundle\Exception\RateLimitExceededException
В Sentry каждое issue из handler’а получает:
rpc.method=user.update — фильтр и группировка по методу;transactions: true — child-span в активной транзакции
(op: rpc.call, description: <имя-метода>).Неперехваченные исключения и так уходят в Sentry через PSR-3 логгер — этот
subscriber только добавляет контекст. Исключения из ignore_exceptions
пропускают error-breadcrumb / span. Полностью отфильтровать их из Sentry —
через before_send хук в Sentry-конфиге.
Subscriber подключается только если оба условия выполнены: enabled: true
И установлен sentry/sentry-symfony. Без SDK тихо no-op.
Vendor-нейтральный. Работает с Jaeger, Datadog, Grafana Tempo, Honeycomb, AWS X-Ray, Google Cloud Trace, New Relic, Lightstep — со всем что говорит OTLP.
composer require open-telemetry/sdk
# плюс экспортёр под ваш бэкенд, например:
# composer require open-telemetry/exporter-otlp
Инициализируйте SDK один раз на процесс (обычно в config/bootstrap.php —
см. PHP OTel SDK docs).
Бандл подтягивает tracer / meter из глобального OTel-провайдера.
json_rpc_server:
opentelemetry:
enabled: true
tracer_name: 'json-rpc'
traces: true # SERVER-kind span на каждый RPC-вызов
metrics: true # rpc.server.duration + rpc.server.requests
propagate_traceparent: true # подцепляться к parent trace из HTTP
record_params: false # пишем params в атрибуты (PII-warning)
record_result: false # пишем result в атрибуты (бывает шумно)
record_max_chars: 2048 # truncation выше
stream:
record_row_count: true # дёшево — атрибут rpc.stream.row_count
span_per_row: false # дорого — отдельный span на каждую строку
ignore_exceptions: [...] # тот же набор что у Sentry
rpc.system=jsonrpc, rpc.method=user.update, rpc.jsonrpc.version=2.0,
плюс rpc.jsonrpc.error_code / rpc.jsonrpc.error_message на ошибках.rpc.server.duration (histogram, мс) и
rpc.server.requests (counter) — обе с label’ами rpc.method и
outcome (ok / error). Стандартные дашборды работают без настройки.rpc.stream.row_count в финале. Per-row спаны
доступны через span_per_row: true для дебага row-level latency.traceparent из incoming HTTP-запроса становится
parent’ом RPC-спана, и трейс из мобильного клиента / API gateway уходит
сквозь ваш сервис.Subscriber подключается только если enabled: true И установлен
open-telemetry/sdk. Без SDK нулевой футпринт.
Нужен атрибут которого нет в дефолте? Свой subscriber на те же события:
public function onStarted(MethodInvocationStartedEvent $e): void
{
Span::getCurrent()->setAttribute('app.tenant', $this->tenantResolver->id());
}
Span-context уже активен на время вызова благодаря бандлу — больше ничего заводить не надо.
Ничто не мешает запустить всё одновременно. Типичная прод-настройка:
kernel.debugЧетыре subscriber’а друг с другом не общаются — каждый читает события независимо. Порядок не специфицирован и не должен иметь значения.
Встроенные стеки покрывают типовые формы. Свой нужен когда:
feed.list, 100% от payments.charge);События — публичный API. Относитесь к ним соответственно.