waffle-commons / telemetry
SDK-free enterprise telemetry for Waffle Commons: contract-first tracing no-ops, a Prometheus /waffle-metrics exporter, and stateless worker-metric collectors.
Requires
- php: ^8.5
- waffle-commons/contracts: 0.1.0-beta6
Requires (Dev)
- carthage-software/mago: ^1.29
- cyclonedx/cyclonedx-php-composer: ^6.2
- igor-php/igor-php: ^0.7.0
- nyholm/psr7: ^1.8
- php-mock/php-mock-phpunit: ^2.15
- phpunit/phpunit: ^12.5
- vimeo/psalm: ^6.16
Suggests
- ext-apcu: To back MetricsRegistry with ApcuMetricStore β shared-memory counters that survive a worker request without living on the worker heap. Without it, wire NullMetricsRegistry instead.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 20:26:32 UTC
README
Waffle Telemetry Component
Release:
0.1.0-beta6Β |ΒCHANGELOG.mdRFC: RFC-005 (OBS-02) β contract-first, SDK-free observability
SDK-free enterprise telemetry for the Waffle Commons framework: a
Prometheus /waffle-metrics exporter fed by stateless worker-metric collectors (memory, GC, DB-pool
utilization), plus request instrumentation that defaults to the contract's no-op tracer so it costs nothing
until a real backend is wired in. The OpenTelemetry SDK bridge lives in the separate
waffle-commons/telemetry-otel package, so the vendor SDK
never enters this core perimeter.
π¦ Installation
composer require waffle-commons/telemetry
π§± Surface
| Class | Role |
|---|---|
Waffle\Commons\Telemetry\Metric\MetricsRegistry |
final readonly implementation of MetricsRegistryInterface and MetricsCollectorInterface. Records increment() / gauge() / observe() into a MetricStoreInterface and reads them back as MetricSamples for export. |
Waffle\Commons\Telemetry\Metric\ApcuMetricStore |
final readonly MetricStoreInterface backed by APCu shared memory β counters live off the worker heap so one scrape aggregates every worker on the instance. |
Waffle\Commons\Telemetry\Collector\MemoryCollector |
Stateless collector β current + peak real memory usage (memory_get_usage(true) / memory_get_peak_usage(true)). |
Waffle\Commons\Telemetry\Collector\GcCollector |
Stateless collector β cumulative GC cycle runs, objects collected, and current root-buffer size (gc_status()). |
Waffle\Commons\Telemetry\Collector\PoolUtilizationCollector |
Stateless collector β DB connection-pool active / idle / capacity from a bound PoolStatsInterface (reports zeros until a pool is wired in). |
Waffle\Commons\Telemetry\Exporter\PrometheusExporter |
final readonly β renders a set of collectors' samples into the Prometheus text exposition format (v0.0.4), no SDK required. |
Waffle\Commons\Telemetry\Middleware\MetricsMiddleware |
final readonly PSR-15 middleware serving the fail-closed /waffle-metrics scrape endpoint. |
Waffle\Commons\Telemetry\Middleware\TracingMiddleware |
final readonly PSR-15 middleware opening the per-request SpanKind::Server root span and recording request count + duration. |
Waffle\Commons\Telemetry\Cache\MeteredCache |
final readonly decorator β wraps any Contracts\Cache\CacheInterface to record hit/miss counters. |
Waffle\Commons\Telemetry\Repository\TracingRepositoryDecorator |
final readonly decorator β wraps any RFC-022 RepositoryInterface to emit a waffle.db.query client span per call. |
π Wiring
Real wiring from the template AppKernelFactory (see skeleton/src/Factory/AppKernelFactory.php):
use Waffle\Commons\Contracts\Telemetry\Metrics\MetricsCollectorInterface; use Waffle\Commons\Contracts\Telemetry\Metrics\MetricsRegistryInterface; use Waffle\Commons\Contracts\Telemetry\Metrics\NullMetricsRegistry; use Waffle\Commons\Telemetry\Collector\GcCollector; use Waffle\Commons\Telemetry\Collector\MemoryCollector; use Waffle\Commons\Telemetry\Collector\PoolUtilizationCollector; use Waffle\Commons\Telemetry\Exporter\PrometheusExporter; use Waffle\Commons\Telemetry\Metric\ApcuMetricStore; use Waffle\Commons\Telemetry\Metric\MetricsRegistry; use Waffle\Commons\Telemetry\Middleware\MetricsMiddleware; use Waffle\Commons\Telemetry\Middleware\TracingMiddleware; // Counters in APCu shared memory (never the worker heap); no-op fallback without APCu. $metricsRegistry = apcu_enabled() ? new MetricsRegistry(new ApcuMetricStore()) : new NullMetricsRegistry(); $container->set(MetricsRegistryInterface::class, $metricsRegistry); $collectors = [new MemoryCollector(), new GcCollector(), new PoolUtilizationCollector()]; if ($metricsRegistry instanceof MetricsCollectorInterface) { $collectors[] = $metricsRegistry; // the registry is itself a collector β export its own counters too } // Placed early: /waffle-metrics short-circuits before the application pipeline and applies // its own fail-closed security β localhost only by default; pass a bearer token to allow a // remote scrape. $stack->add(new MetricsMiddleware( new PrometheusExporter($collectors), $responseFactory, $streamFactory, bearerToken: null, allowedIps: ['127.0.0.1', '::1'], )); // Opens the server root span (extracts an inbound `traceparent`) and records request // count + duration; no-op cost while $tracer is the contract's NullTracer. $stack->add(new TracingMiddleware($tracer, $metricsRegistry, $tracePropagator));
$tracer defaults to Contracts\Telemetry\NullTracer and $tracePropagator to
Contracts\Telemetry\NullTextMapPropagator. Swap in waffle-commons/telemetry-otel's
OtelTracerFactory::console(...) (or an OTLP exporter) to activate real distributed tracing without touching
this package.
π‘οΈ Configuration β /waffle-metrics is fail-closed
MetricsMiddleware answers the scrape path only when the request presents the configured bearer token or
comes from an allow-listed client IP; every other request to that path gets a 404 β the endpoint's existence
is never revealed to an unauthorized caller (mirrors AXE 0 LEAK-03).
| Constructor argument | Default | Effect |
|---|---|---|
?string $bearerToken |
null |
When set, a request with Authorization: Bearer <token> (compared via hash_equals()) is authorised. |
array $allowedIps |
[] |
Exact REMOTE_ADDR values permitted to scrape (e.g. ['127.0.0.1', '::1'] for localhost-only). |
Both are opt-in allow-lists β an empty configuration means the endpoint accepts nothing, not everything.
π Collectors
| Metric(s) | Type | Source |
|---|---|---|
waffle_memory_usage_bytes, waffle_memory_peak_bytes |
Gauge | MemoryCollector β memory_get_usage(true) / memory_get_peak_usage(true). |
waffle_gc_runs_total, waffle_gc_collected_total, waffle_gc_roots |
Counter / Counter / Gauge | GcCollector β gc_status(). |
waffle_db_pool_active, waffle_db_pool_idle, waffle_db_pool_capacity |
Gauge | PoolUtilizationCollector β a bound PoolStatsInterface (zeros until a pool is wired in). |
waffle_http_requests_total, waffle_http_request_duration_seconds |
Counter / Counter (_sum/_count) |
TracingMiddleware, via MetricsRegistryInterface. |
waffle_cache_hits_total, waffle_cache_misses_total |
Counter | MeteredCache, judged with has() so the cached value is never captured into a mixed local. |
MetricsRegistry::observe() stores a summary (<name>_sum + <name>_count), so a mean is derivable
downstream without a histogram implementation.
π§ Perimeter
Depends only on waffle-commons/contracts. Every telemetry interface (Waffle\Commons\Contracts\Telemetry\*)
lands in contracts first β mago guard enforces the boundary at [guard.perimeter] in
mago.toml; a forbidden use statement fails the build, not a reviewer. Production code under
Waffle\Commons\Telemetry may depend only on itself, Contracts\**, Psr\**, and PHP core / Psl\**. Test
code under WaffleTests\Commons\Telemetry is unrestricted (@all).
Contract-first, component-agnostic by construction: components compose through waffle-commons/contracts,
never directly through one another.
π§΅ Worker safety
Every cumulative counter lives in ApcuMetricStore (APCu shared memory), never on the resident worker
heap β the only instance field on the store is its immutable key prefix. Collectors and decorators are all
final readonly and stateless per request, so the component passes the igor-php worker-mode audit with zero
findings (wfl igor 0 KO).
π§ͺ Testing
docker exec -w /waffle-commons/telemetry waffle-dev composer tests
π Documentation
Central framework docs (DiΓ‘taxis) for this component:
- Reference:
reference/telemetry.md - Explanation:
explanation/observability-telemetry.md - Full documentation tree: waffle-commons/documentation
π License
MIT β see LICENSE.md.