Vortos
Metrics

Auto-Instrumentation

What each framework module records automatically, label design, cardinality, and how to disable noisy modules.

Auto-Instrumentation

When MetricsPackage is registered with a non-NoOp adapter, the framework automatically records metrics for HTTP, CQRS, messaging, cache, persistence, security, rate limits, quotas, feature access, and feature flags. No code changes are required.


HTTP

HttpMetricsListener records request start time and then records metrics when the response is available.

{ns}_http_requests_total — Counter

Incremented after every response is sent.

LabelValuesExample
methodHTTP verbGET, POST
routeNamed routeorders.place, users.show
statusHTTP status code200, 404, 500

Use this metric to track:

  • Request rate by endpoint
  • Error rate (status=5xx / total)
  • Traffic distribution across routes

{ns}_http_request_duration_ms — Histogram

Observed (in milliseconds) after every response.

LabelValues
methodHTTP verb
routeNamed route

Buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000] ms

Use this metric to define SLAs: "p99 response time must stay under 500ms."

{ns}_http_blocked_total — Counter

Incremented for rejected or low-value traffic that should stay cheap.

LabelValues
reasonrate_limit, ip_filter, csrf, cors, signature, not_found
statusHTTP status code

Use this metric to detect scans, bot traffic, abuse spikes, and firewall/rate-limit pressure without paying for full trace enrichment on every rejected request.

Sub-requests are not recorded

Internal Symfony sub-requests (e.g. ESI fragments, error pages rendered internally) are silently skipped. Only the primary request is recorded.


CQRS

CqrsMetricsDecorator wraps CommandBusInterface via the DI decorator pattern. All three metrics use the command class short name as the command label — for example PlaceOrder, CancelOrder.

{ns}_cqrs_commands_total — Counter

Incremented on every successful command dispatch.

LabelExample
commandPlaceOrder

{ns}_cqrs_command_duration_ms — Histogram

Observed in milliseconds from dispatch to handler return.

LabelExample
commandPlaceOrder

Buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000] ms

{ns}_cqrs_command_failures_total — Counter

Incremented when a command handler throws an exception.

LabelExample
commandPlaceOrder

Use this to build an alert: failures_total / commands_total > 0.01 (more than 1% of commands failing) → page on-call.


Messaging

MessagingMetricsDecorator wraps event dispatch and message handling paths.

{ns}_messaging_events_dispatched_total — Counter

Incremented every time an event is dispatched to the bus.

LabelExample
eventOrderPlaced

{ns}_messaging_event_failures_total — Counter

Incremented when event dispatch or handling fails.

LabelExample
eventOrderPlaced

{ns}_messaging_event_duration_ms — Histogram

Observed around event handling.

LabelExample
eventOrderPlaced

{ns}_messaging_messages_consumed_total — Counter

Incremented for each event received from a broker.

LabelExample
eventOrderPlaced
consumerorders

{ns}_messaging_message_retries_total — Counter

Incremented when a consumed message is retried.

LabelExample
eventOrderPlaced
consumerorders

{ns}_messaging_message_duration_ms — Histogram

Time from message receipt to handler completion.

LabelExample
eventOrderPlaced
consumerorders

Buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000] ms

Backlog gauges

Queue and outbox modules can expose operational gauges:

MetricMeaning
{ns}_outbox_backlog_sizepending outbox message count
{ns}_outbox_oldest_pending_age_secondsage of oldest pending outbox message
{ns}_dlq_backlog_sizedead-letter queue size
{ns}_dlq_oldest_failed_age_secondsage of oldest failed message

Dead-letter gauges should alert quickly. A growing DLQ normally requires manual intervention.

These gauges are point-in-time values — they are not updated on every request. They are populated by running vortos:metrics:collect on a schedule. See Point-in-Time Collectors below.


Cache

CacheMetricsDecorator wraps the active cache adapter.

{ns}_cache_operations_total — Counter

Incremented for every cache operation.

LabelValues
operationget, set, delete
resulthit, miss (get only)

Derived metric: cache hit rate = get,hit / (get,hit + get,miss). A sudden drop in hit rate could mean Redis restarted and the cache is cold, or TTLs are too short.

Cache metrics can be very high volume

At 1000 requests/second with 10 cache lookups each, you're recording 10,000 counter increments per second. For StatsD this is fine — increments are cheap and batched. For Prometheus with Redis storage, each increment is a Redis HINCRBY — 10,000 Redis calls per second. Consider disabling cache metrics in very high-throughput scenarios:

$config->disableModule(MetricsModule::Cache);

Persistence (DBAL)

PersistenceMetricsStatement and PersistenceMetricsMiddleware wrap DBAL query execution via Doctrine's Middleware API.

{ns}_db_queries_total — Counter

Incremented per statement execution.

LabelValues
driverdbal
operationquery, execute, prepare.execute

{ns}_db_query_duration_ms — Histogram

Observed in milliseconds per statement execution.

LabelValues
driverdbal, mongo

Buckets: [1, 5, 10, 25, 50, 100, 250, 500, 1000] ms

Use p99 query duration to detect slow query regressions. Set an alert if p99 > 100ms.


Policy limits and security

Policy and limit middleware emit metrics only on meaningful enforcement points. Successful high-throughput paths avoid logs and extra trace spans by default.

Rate limits

{ns}_rate_limit_allowed_total{policy,scope,controller}
{ns}_rate_limit_blocked_total{policy,scope,controller}

Blocked requests also mark the request as low-value telemetry and increment:

{ns}_http_blocked_total{reason="rate_limit",status="429"}

Quotas

{ns}_quota_allowed_total{quota,bucket,period,controller}
{ns}_quota_blocked_total{quota,bucket,period,controller}
{ns}_quota_consumed_total{quota,bucket,period,controller}

Quota labels use the resolver bucket, not the raw subject ID. For example, use bucket="organization", not organization_id="org_123".

Feature access and flags

{ns}_feature_access_allowed_total{feature,controller}
{ns}_feature_access_denied_total{feature,controller}
{ns}_feature_flag_evaluations_total{flag,result}

Security events

{ns}_security_events_total{reason}

Security-blocked requests also increment {ns}_http_blocked_total with reasons such as ip_filter, csrf, cors, or signature.


Cardinality — the most important concept in metrics

Every unique combination of label values creates a separate time series. Labels multiply — if a metric has 3 labels each with 10 possible values, you have up to 1,000 time series for that one metric.

High cardinality is the most common mistake in metrics design. Every metrics system (Prometheus, Datadog, InfluxDB) has limits, and exceeding them causes performance problems or cost overruns.

Safe label values (low cardinality):

  • HTTP method: 5 values
  • HTTP status code: ~50 values
  • Route name: ~100 values (your app's routes)
  • Currency: ~50 values

Dangerous label values (high cardinality):

  • User ID: millions of values
  • Order ID: unbounded
  • Email address: unbounded
  • IP address: millions of values
  • Timestamp: unbounded

Never use IDs, emails, or free-text strings as label values. If you need to query by user, use tracing (where you can attach any attribute to a single span) not metrics.

The framework auto-instrumentation uses only low-cardinality values — method, route, status code, command short name, event short name, policy name, bucket name, and period. When you add custom metrics, apply the same discipline.

Framework metrics use enum-backed label keys internally. This keeps framework instrumentation consistent and avoids ad hoc label strings in hot paths.

Blocked traffic trace dropping

Rate-limited requests, security-blocked requests, and basic 404 responses are still counted, but full trace enrichment is skipped. Vortos records a cheap blocked counter and avoids building expensive telemetry for abuse traffic.

This protects both CPU and observability spend during scans, crawler bursts, and deliberate rate-limit attacks.


Disabling modules

Any auto-instrumentation module can be disabled:

config/metrics.php
$config->disableModule(MetricsModule::Cache);
$config->disableModule(MetricsModule::Persistence);
$config->disableModule(MetricsModule::Http);
$config->disableModule(MetricsModule::Cqrs);
$config->disableModule(MetricsModule::Messaging);

When a module is disabled, its decorator is not registered — no wrapper, no overhead, no metrics. The underlying service is wired directly as if the metrics package wasn't installed.

Common reasons to disable:

  • Cache — very high volume, generates many counter increments per request
  • Persistence — high volume, and DB query duration is often better captured by tracing spans (which include the SQL)
  • Http — if you have an existing metrics solution for request tracking

You can disable multiple modules in a single call:

$config->disableModule(MetricsModule::Cache, MetricsModule::Persistence);

Point-in-Time Collectors

Some metrics cannot be observed on the request path — they measure background system state that must be sampled on a schedule. The outbox and dead-letter backlog gauges fall into this category.

Run the collector command on a cron schedule (every 30–60 seconds is typical):

php bin/console vortos:metrics:collect

Output:

Running 2 metric collector(s)...

  ✔ OutboxBacklogCollector
  ✔ DeadLetterBacklogCollector

2 collector(s) ran successfully.

Each collector queries the database, sets the gauge to the current count, and returns. The command is idempotent — running it more frequently just updates the gauges more often.

Add it to supervisord.conf or a cron entry alongside your outbox relay:

docker/worker/supervisord.conf
[program:metrics-collect]
command=bash -c 'while true; do php /var/www/html/bin/console vortos:metrics:collect; sleep 30; done'
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/metrics-collect.out.log
stderr_logfile=/var/log/supervisor/metrics-collect.err.log

If no collectors are registered (for example, the Messaging module is disabled), the command reports zero collectors and exits cleanly — it is safe to run regardless of which modules are active.

On this page