cascata/hyperf-opentelemetry

Drop-in OpenTelemetry integration for Hyperf 3.1+ (traces, metrics and logs via OTLP).

Maintainers

Package info

github.com/Jo1oPedro/hyperf-observability-package

pkg:composer/cascata/hyperf-opentelemetry

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-05-14 02:47 UTC

This package is auto-updated.

Last update: 2026-05-14 02:52:16 UTC


README

Drop-in OpenTelemetry integration for Hyperf 3.1+ — traces, metrics and logs via OTLP, with sensible Swoole defaults.

Out of the box you get:

  • HTTP server spans for every inbound request (SERVER kind, W3C TraceContext propagation)
  • #[Traced] attribute to wrap any service/use-case/repository method in a span
  • Correlation ID middleware (propagates X-Correlation-Id)
  • Monolog handler that ships every log via OTLP, with trace_id/span_id injected automatically
  • Coroutine-safe trace context (survives the end of individual spans, so logs emitted by exception handlers still correlate)
  • Optional Docker Compose stack with OTel Collector → Tempo / Prometheus / Loki / Grafana

Requirements

PHP >= 8.1
Hyperf ~3.1.0
Swoole >= 5.0
ext-grpc not required (uses OTLP HTTP)

Install

composer require cascata/hyperf-opentelemetry

Register the tracing middleware in config/autoload/middlewares.php:

return [
    'http' => [
        \Cascata\HyperfOpenTelemetry\Middleware\CorrelationIdMiddleware::class,
        \Cascata\HyperfOpenTelemetry\Middleware\TracingMiddleware::class,
        // ... your other middlewares
    ],
];

Add the Monolog handler to config/autoload/logger.php (your existing config):

use Cascata\HyperfOpenTelemetry\Logging\OtelLogHandler;
use Cascata\HyperfOpenTelemetry\Logging\OtelTraceContextProcessor;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;

return [
    'default' => [
        'handlers' => [
            [
                'class' => StreamHandler::class,
                'constructor' => [
                    'stream' => BASE_PATH . '/runtime/logs/hyperf.log',
                    'level'  => Logger::DEBUG,
                ],
                'formatter' => [
                    'class' => JsonFormatter::class,
                    'constructor' => [],
                ],
            ],
            [
                'class' => OtelLogHandler::class,
                'constructor' => [
                    'level' => Logger::INFO,
                ],
            ],
        ],
        'processors' => [
            ['class' => OtelTraceContextProcessor::class],
        ],
    ],
];

Set environment variables:

OTEL_ENABLED=true
OTEL_SERVICE_NAME=my-service
OTEL_SERVICE_NAMESPACE=mycompany
OTEL_SERVICE_VERSION=1.0.0
OTEL_DEPLOYMENT_ENV=dev
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_TRACES_SAMPLER_ARG=1.0

Clear the DI proxy cache and you're done:

rm -rf runtime/container

Usage

Annotate methods you want traced

use Cascata\HyperfOpenTelemetry\Tracing\Annotation\Traced;

class CreateUser
{
    #[Traced(name: 'usecase.user.create')]
    public function execute(CreateUserInput $input): CreateUserOutput
    {
        // automatic span: usecase.user.create
    }
}

Span kinds:

#[Traced(name: 'mq.welcome.publish', kind: 'producer')]    // PRODUCER (publish)
#[Traced(name: 'mq.welcome.consume', kind: 'consumer')]    // CONSUMER (read)
#[Traced(name: 'http.payment.charge', kind: 'client')]     // CLIENT (outbound)
#[Traced(name: 'usecase.foo')]                             // INTERNAL (default)

Aspects only work on methods that:

  • are public, non-static
  • belong to classes resolved by the Hyperf container (DI)

Methods called via new ClassName() won't be intercepted.

Custom metrics

use OpenTelemetry\API\Globals;

$meter   = Globals::meterProvider()->getMeter('my-service.business');
$counter = $meter->createCounter('orders_created_total');
$counter->add(1, ['tenant' => $tenantId]);

Publish optional resources

The package ships an optional config file and a complete Docker Compose stack.

# Publish opentelemetry.php to config/autoload/
php bin/hyperf.php vendor:publish cascata/hyperf-opentelemetry --id=config

# Publish Tempo + Prometheus + Loki + Grafana + OTel Collector docker stack
php bin/hyperf.php vendor:publish cascata/hyperf-opentelemetry --id=observability-stack

The published stack lives at BASE_PATH/observability/. Bring it up with:

docker network create hyperf-network    # only once
cd observability && docker compose up -d

Grafana: http://localhost:3000 (anonymous Admin).

Architecture

Application code
      │  $logger->info()    or   handler emits span
      ▼
Cascata\HyperfOpenTelemetry        ──► global TracerProvider / MeterProvider / LoggerProvider
      │
      ▼
OTLP HTTP (4318)
      │
      ▼
OTel Collector  ──►  Tempo   (traces)
                ──►  Prom    (metrics, via spanmetrics connector)
                ──►  Loki    (logs)
                          │
                          ▼
                       Grafana

The application only ever speaks OTLP to the collector. Swapping Tempo for Jaeger, or Loki for Elasticsearch, only changes the collector config — your code does not change.

Configuration reference

All values can be set via env or by publishing opentelemetry.php.

Env var Default Purpose
OTEL_ENABLED true Master switch
OTEL_SERVICE_NAME hyperf-app service.name resource attribute
OTEL_SERVICE_NAMESPACE default service.namespace
OTEL_SERVICE_VERSION 1.0.0 service.version
OTEL_DEPLOYMENT_ENV dev deployment.environment.name
OTEL_EXPORTER_OTLP_ENDPOINT http://otel-collector:4318 OTLP HTTP base URL
OTEL_TRACES_SAMPLER_ARG 1.0 TraceID-ratio sampler (0.0 – 1.0)

Troubleshooting

No spans / logs / metrics arriving in Grafana

Verify the collector is reachable from inside the app container:

docker exec <your-app> curl -s -o /dev/null -w "%{http_code}\n" \
  http://otel-collector:4318/v1/traces -X POST \
  -H "Content-Type: application/x-protobuf" --data "test"
# expected: 400 (means the endpoint is up, payload was rejected as expected)

Enable internal debug logging:

OTEL_LOG_LEVEL=debug
OTEL_PHP_INTERNAL_LOG_LEVEL=debug

Span::getCurrent() returns no-op span in exception handlers

That's why this package also stores trace_id/span_id in Hyperf\Context — logs emitted after a span closes still correlate. No action needed on your side.

Changes to #[Traced] annotations don't take effect

The Hyperf DI proxy cache must be regenerated:

rm -rf runtime/container && php bin/hyperf.php start

Batch processor doesn't flush in Swoole

This package uses SimpleSpanProcessor / SimpleLogRecordProcessor by default — they export synchronously, which is the safe choice in long-running Swoole workers. For very high-throughput services, consider implementing a periodic flush listener instead.

Versioning

Branch Hyperf OTel SDK
1.x ~3.1.0 ^1.1

Follows SemVer.

License

MIT