Skip to content

kaz29/cakephp-otel-plugin

Repository files navigation

CakePHP OpenTelemetry Plugin

Tests

日本語版🇯🇵

A CakePHP 5 plugin that adds OpenTelemetry instrumentation to your application. It uses ext-opentelemetry's zend_observer hooks to automatically generate spans for Controller and Table operations without any code changes.

Requirements

  • PHP 8.3+
  • CakePHP 5.x
  • ext-opentelemetry PECL extension

Installation

composer require kaz29/cakephp-otel-plugin

Load the plugin in config/bootstrap.php or Application::bootstrap():

$this->addPlugin('OtelInstrumentation');

Instrumented Targets

Target Span name example
Controller::invokeAction App\Controller\UsersController::index
Table::find Users.find(all)
Table::save Users.save
Table::delete Users.delete

Excluding Instrumentation

Some endpoints — health checks called every few seconds by load balancers and orchestrators, for example — generate high volumes of low-value spans that bloat your telemetry backend. You can opt out specific Controller/action pairs from instrumentation:

// config/bootstrap.php or config/app_local.php
use Cake\Core\Configure;

Configure::write('OtelInstrumentation.exclude', [
    // Skip every action on HealthController (health/readiness/liveness probes)
    ['controller' => \App\Controller\HealthController::class, 'action' => '*'],

    // Skip a specific action while leaving others instrumented
    ['controller' => \App\Controller\PostsController::class, 'action' => 'ping'],
]);

Matching is exactcontroller must be the fully-qualified class name and action must match the action name verbatim. The single exception is 'action' => '*', which matches every action of that controller. '*' is not allowed on controller.

While an excluded action is executing, child Table::find/save/delete calls and custom-hook spans are also suppressed, so a single rule cleans up the whole subtree. Once the action returns, instrumentation resumes for subsequent requests.

This setting only affects HTTP requests dispatched through Controller::invokeAction. CLI commands are not instrumented in the first place.

Custom Instrumentation

You can instrument any class method by registering custom hooks. The plugin uses the same \OpenTelemetry\Instrumentation\hook() mechanism as the built-in Controller/Table instrumentation.

Note: Controller::invokeAction, Table::find, Table::save, and Table::delete are already instrumented by the plugin. Do not register them as custom hooks — doing so would create duplicate spans.

Via Configure (simple)

// config/bootstrap.php or config/app_local.php
use Cake\Core\Configure;
use OpenTelemetry\API\Trace\SpanKind;

Configure::write('OtelInstrumentation.hooks', [
    // Minimal — span name auto-generated as "App\Service\PaymentService::charge"
    ['class' => \App\Service\PaymentService::class, 'method' => 'charge'],

    // With options
    [
        'class' => \App\Service\PaymentService::class,
        'method' => 'refund',
        'spanName' => 'payment.refund',
        'kind' => SpanKind::KIND_CLIENT,
        'attributes' => ['payment.provider' => 'stripe'],
    ],
]);

Via static registration (advanced)

Use CustomInstrumentation::register() when you need dynamic attributes via callback:

// In Application::bootstrap(), before $this->addPlugin('OtelInstrumentation')
use OtelInstrumentation\Instrumentation\CustomInstrumentation;
use OpenTelemetry\API\Trace\SpanKind;

CustomInstrumentation::register(
    \App\Service\PaymentService::class,
    'charge',
    spanName: 'payment.charge',
    kind: SpanKind::KIND_CLIENT,
    attributes: ['payment.provider' => 'stripe'],
    attributeCallback: fn($instance, $params, $class, $function) => [
        'payment.amount' => $params[0] ?? null,
    ],
);

Options

Option Type Default Description
class string (required) Fully qualified class name
method string (required) Method name to hook
spanName string|null FQCN::method Custom span name
kind int KIND_INTERNAL SpanKind constant
attributes array [] Static span attributes
attributeCallback Closure|null null fn($instance, $params, $class, $function): array — use register() instead of Configure for this option

Environment Variables

OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_SERVICE_NAME=my-cakephp-app
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

OtelErrorLoggingMiddleware

A PSR-15 middleware that catches 500-level exceptions and emits them as OpenTelemetry log records. The log is automatically associated with the current span, so you can view related errors directly in your trace backend (Jaeger, Grafana Tempo, etc.).

  • HttpException with status code >= 500: logged
  • HttpException with status code < 500 (e.g. 404): not logged
  • Non-HttpException (unexpected errors): logged as 500

Setup

Add it after ErrorHandlerMiddleware in your Application::middleware():

use OtelInstrumentation\Middleware\OtelErrorLoggingMiddleware;

public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
        ->add(new ErrorHandlerMiddleware())
        ->add(new OtelErrorLoggingMiddleware())
        // ...
    ;
}

TraceAwareLogger

A PSR-3 LoggerInterface decorator that automatically injects trace_id / span_id into log context.

$logger = new \OtelInstrumentation\Log\TraceAwareLogger($existingPsr3Logger);

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors