Skip to main content

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.

bootstrap.php
<?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 / tracestate in 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
src/Actor/OrderProcessor.php
<?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]
metrics exported automatically
<?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_letters

RED 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 →
src/Actor/PaymentActor.php
<?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_ENDPOINT environment variable controls the endpoint
bootstrap.php — OTLP setup
<?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