cascata / hyperf-opentelemetry
Drop-in OpenTelemetry integration for Hyperf 3.1+ (traces, metrics and logs via OTLP).
Package info
github.com/Jo1oPedro/hyperf-observability-package
pkg:composer/cascata/hyperf-opentelemetry
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.8
- hyperf/context: ~3.1.0
- hyperf/di: ~3.1.0
- hyperf/event: ~3.1.0
- hyperf/framework: ~3.1.0
- hyperf/http-server: ~3.1.0
- hyperf/logger: ~3.1.0
- monolog/monolog: ^3.0
- open-telemetry/api: ^1.1
- open-telemetry/exporter-otlp: ^1.1
- open-telemetry/sdk: ^1.1
- open-telemetry/sem-conv: ^1.27
- php-http/guzzle7-adapter: ^1.0
- ramsey/uuid: ^4.7
Requires (Dev)
- mockery/mockery: ^1.6
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
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 (
SERVERkind, 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_idinjected 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