Observability
Traces, metrics, and logs in one call.
Nexus emits OpenTelemetry spans, RED metrics, and structured logs across
every actor boundary — automatically. One
withObservability() call and your entire
actor graph is observable. No instrumentation code inside handlers.
No overhead when disabled.
<?php
// One call wires traces, metrics, and logs into every actor.
use Monadial\Nexus\App\NexusApp;
use Monadial\Nexus\Observability\Config\ObservabilityConfig;
use Monadial\Nexus\Observability\Otel\ObservabilityFactory;
NexusApp::create('payments')
->withObservability(ObservabilityFactory::fromConfig(
ObservabilityConfig::fromEnv($_SERVER),
))
->actor('payments', Props::fromBehavior($paymentBehavior))
->run(new SwooleRuntime());
// No call to withObservability()? Defaults to a no-op provider — zero overhead.W3C trace propagation across every actor boundary.
Every tell() and ask()
carries the current W3C traceparent in the message
envelope. When the handler runs, Nexus opens a child span automatically — no
manual context propagation, no thread-local hacks.
The full call chain is visible in one trace: HTTP request → actor handler → child actor → persistence write. Dead letters and supervision restarts appear as span events, not silent gaps.
- → W3C
traceparent/tracestatein every envelope - → Spans nest: HTTP → actor → child actors → persistence
- → Dead letters and restarts surface as span events, not silent gaps
- → Worker pool cross-thread hops visible as linked spans
<?php
// W3C traceparent propagates through every tell() and ask().
// Spans nest automatically: HTTP request → actor handler → persistence write.
$behavior = Behavior::receive(static function (ActorContext $ctx, object $msg): Behavior {
if ($msg instanceof ProcessOrder) {
// Both calls carry the incoming trace context — no manual wiring.
$inventoryRef->tell(new ReserveStock($msg->orderId));
$persistenceRef->tell(new SaveOrder($msg));
}
return Behavior::same();
});
// Resulting trace:
// POST /orders [200ms]
// └─ payments.handle [180ms]
// ├─ inventory.tell [40ms]
// └─ persistence.tell [60ms]<?php
// RED metrics emitted per actor — no instrumentation code in handlers.
// Export to any OTLP collector: Prometheus, Tempo, Honeycomb, Datadog.
// Emitted automatically as OpenTelemetry instruments:
// nexus.actor.messages.processed {nexus.message.type} // counter
// nexus.actor.message.processing.duration // histogram
// http.server.request.duration {method, status} // histogram
// http.server.active_requests // up/down counter
// Cross-worker transport + Doctrine pools:
// nexus.worker_pool.messages.sent {nexus.worker.target}
// nexus.dbal.pool.acquire.wait {pool.name} // histogram
// Swoole + actor-system observable gauges:
// swoole.coroutine.count, swoole.server.connections
// nexus.actor_system.live_actors, nexus.actor_system.dead_lettersRED metrics per actor, no handler code.
Rate, error rate, and duration histograms are emitted for every actor automatically. Mailbox depth gauges let you spot backpressure before it becomes a 503. Worker pool metrics give per-thread visibility under Swoole.
All metrics are exported via OTLP — point the collector at Prometheus, Tempo, Honeycomb, or Datadog with a single environment variable. Swoole server and coroutine stats are exposed as OpenTelemetry observable gauges alongside the actor metrics.
Full metrics reference →Logs correlated to traces by default.
$ctx->log() injects the current
trace_id and span_id
into every log record — no extra adapter, no middleware. The PSR-3 logger you
already pass to ActorSystem::create() receives
the enriched context automatically.
In Grafana, every log line becomes a clickable link to the corresponding trace. Debugging a slow actor means one click from the log panel to the full distributed trace — not a manual correlation of log timestamps and trace IDs.
Logs correlation guide →<?php
// Add one RecordProcessor; every log record then carries the active span's ids.
use Monadial\Nexus\Observability\Logger\TraceCorrelationProcessor;
// register new TraceCorrelationProcessor($observability) in your nexus-logger pipeline
$behavior = Behavior::receive(static function (ActorContext $ctx, object $msg): Behavior {
$ctx->log()->info('Processing payment', ['order_id' => $msg->orderId]);
// → record now includes trace_id + span_id from the active span
return Behavior::same();
});
// Result: every log line is clickable in Grafana — jump straight to the trace.Everything included. Nothing required.
The nexus-observability-otel bridge uses the
OpenTelemetry SDK — the same SDK your other services already export. No Nexus-specific
agent, no sidecar, no proprietary format. Disabled by default: if you don't call
withObservability(), Nexus uses a no-op provider
with zero allocations on the hot path.
Zero overhead when disabled
No call to withObservability()? Nexus uses a no-op
provider. No span allocations, no context propagation, no performance
cost on the message hot path.
OTLP to any collector
Export spans and metrics to the OpenTelemetry Collector, Tempo, Jaeger, Honeycomb, or Datadog Agent via OTLP/HTTP. One environment variable controls the endpoint.
Supervision events in spans
Actor restarts, escalations, and dead-letter deliveries appear as span events on the relevant trace — not silent gaps in your observability data.
Wire any OTLP collector in minutes.
ObservabilityFactory::fromConfig() reads
the standard OpenTelemetry env vars and builds an OTLP-backed provider. Set your
endpoint once, pass the provider to withObservability(),
and Nexus handles the rest.
Uses the same open-telemetry/sdk and
open-telemetry/exporter-otlp packages your other
PHP services already depend on. No Nexus-specific exporter or agent.
- → gRPC and HTTP OTLP exporters both supported
- → Batch span processor for low-overhead export
- → Works with OpenTelemetry Collector, Tempo, Jaeger, Honeycomb, Datadog
- →
OTEL_EXPORTER_OTLP_ENDPOINTenvironment variable controls the endpoint
<?php
// Point Nexus at any OTLP endpoint via standard OpenTelemetry env vars.
// Works with the OpenTelemetry Collector, Tempo, Jaeger, Honeycomb, Datadog.
use Monadial\Nexus\Observability\Config\ObservabilityConfig;
use Monadial\Nexus\Observability\Otel\ObservabilityFactory;
// OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318
// OTEL_SERVICE_NAME=payments
// OTEL_TRACES_SAMPLER=parentbased_always_on
$observability = ObservabilityFactory::fromConfig(
ObservabilityConfig::fromEnv($_SERVER),
);
// Disabled by default: unset OTEL vars ⇒ a no-op provider (zero overhead).Observability that covers the whole system.
Actor systems can be opaque: a message enters, something happens inside a mailbox, a reply comes out — or doesn't. Nexus makes every step visible. Span nesting shows exactly which actor handled a message and how long it took. Mailbox depth gauges show where backpressure is building before requests time out. Supervision events in traces show when and why an actor restarted.
This works across the worker pool too. A cross-thread message hop via
WorkerActorRef appears as a linked span in the
same trace — the trace context crosses thread boundaries inside the envelope,
just like it does with local actor refs.
Add observability to your actor system.
The getting-started guide wires traces, metrics, and logs into a running Nexus app in under ten minutes.
composer require nexus-actors/nexus