The OpenFGA PHP SDK includes comprehensive OpenTelemetry support for observability, providing distributed tracing, metrics collection, and telemetry data to help you monitor, debug, and optimize your authorization workflows. Whether you’re troubleshooting performance issues or gaining insights into your application’s authorization patterns, the SDK’s telemetry features give you the visibility you need.
New to OpenTelemetry? It’s an open-source observability framework that helps you collect, process, and export telemetry data (metrics, logs, and traces) from your applications. Think of it as a way to understand what your application is doing under the hood.
Already using OpenTelemetry? The SDK integrates seamlessly with your existing setup - just configure your telemetry provider and start getting insights into your OpenFGA operations automatically.
The SDK automatically instruments and provides telemetry for:
check()
, listObjects()
, writeTuples()
, etc.All examples in this guide assume the following setup:
Requirements:
composer require open-telemetry/api open-telemetry/sdk
Common imports and setup code:
<?php
require_once __DIR__ . '/vendor/autoload.php';
// OpenFGA SDK imports
use OpenFGA\Client;
use OpenFGA\Observability\TelemetryFactory;
// OpenTelemetry imports (when using full OpenTelemetry setup)
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
// Event-driven telemetry imports
use OpenFGA\Events\{
EventDispatcher,
HttpRequestSentEvent,
HttpResponseReceivedEvent,
OperationCompletedEvent,
OperationStartedEvent
};
// Helper functions for common operations
use function OpenFGA\{allowed, dsl, model, store, tuple, tuples, write};
// Basic client configuration (customize for your environment)
$apiUrl = $_ENV['FGA_API_URL'] ?? 'http://localhost:8080';
$storeId = 'your-store-id';
$modelId = 'your-model-id';
The simplest way to get started is with the built-in telemetry that works without any external dependencies:
// Create a telemetry provider
$telemetry = TelemetryFactory::create(
serviceName: 'my-authorization-service',
serviceVersion: '1.0.0'
);
// Configure the client with telemetry
$client = new Client(
url: $apiUrl,
telemetry: $telemetry
);
// Your authorization operations are now automatically instrumented!
$result = $client->check(
store: $storeId,
model: $modelId,
tupleKey: tuple(user: 'user:anne', relation: 'viewer', object: 'document:readme')
);
For production use with a telemetry backend, install the OpenTelemetry packages and configure them:
composer require open-telemetry/api open-telemetry/sdk
// Configure OpenTelemetry (this is a basic example)
$tracerProvider = new TracerProvider([
new SimpleSpanProcessor(
new SpanExporter($_ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? 'http://localhost:4317')
)
]);
Globals::registerInitializer(function () use ($tracerProvider) {
return \OpenTelemetry\SDK\Registry::get()->tracerProvider($tracerProvider);
});
// Create telemetry with your service information
$telemetry = TelemetryFactory::create(
serviceName: 'my-authorization-service',
serviceVersion: '1.2.3'
);
// Configure client
$client = new Client(
url: $apiUrl,
telemetry: $telemetry
);
// Operations are now traced and exported to your backend
$result = $client->listObjects(
store: $storeId,
model: $modelId,
user: 'user:anne',
relation: 'viewer',
type: 'document'
);
Every HTTP request to the OpenFGA API is automatically instrumented:
Traces (Spans):
HTTP {METHOD}
(for example HTTP POST
)Metrics:
openfga.http.requests.total
- Counter of HTTP requests by method, status code, and success/failureExample span attributes:
http.method: POST
http.url: https://api.fga.example/stores/123/check
http.scheme: https
http.host: api.fga.example
http.status_code: 200
http.response.size: 1024
openfga.sdk.name: openfga-php
openfga.sdk.version: 1.0.0
Business-level operations provide higher-level observability:
Traces (Spans):
openfga.{operation}
(for example openfga.check
, openfga.write_tuples
)Metrics:
openfga.operations.total
- Counter of operations by type, store, success/failureopenfga.operations.duration
- Histogram of operation durationsExample operation span:
openfga.operation: check
openfga.store_id: store_01H1234567890ABCDEF
openfga.model_id: model_01H1234567890ABCDEF
openfga.sdk.name: openfga-php
openfga.sdk.version: 1.0.0
The SDK automatically tracks retry attempts and circuit breaker behavior:
Retry Metrics:
openfga.retries.total
- Counter of retry attempts by endpoint and outcomeopenfga.retries.delay
- Histogram of retry delays in millisecondsCircuit Breaker Metrics:
openfga.circuit_breaker.state_changes.total
- Counter of state changes (open/closed)Authentication Telemetry:
openfga.auth.events.total
- Counter of authentication eventsopenfga.auth.duration
- Histogram of authentication operation durationsConfigure your service information for better observability:
$telemetry = TelemetryFactory::create(
serviceName: 'user-management-api', // Your service name
serviceVersion: '2.1.0' // Your service version
);
You can provide your own configured OpenTelemetry tracer and meter:
// Get your configured tracer and meter
$tracer = Globals::tracerProvider()->getTracer('my-service', '1.0.0');
$meter = Globals::meterProvider()->getMeter('my-service', '1.0.0');
// Create telemetry with custom providers
$telemetry = TelemetryFactory::createWithCustomProviders($tracer, $meter);
$client = new Client(
url: $apiUrl,
telemetry: $telemetry
);
For testing or when you want to disable telemetry:
// Explicitly disable telemetry
$telemetry = TelemetryFactory::createNoOp(); // Returns null
$client = new Client(
url: $apiUrl,
telemetry: $telemetry
);
// Or simply pass null directly
$client = new Client(
url: $apiUrl,
telemetry: null // No telemetry
);
For local development with Jaeger:
# Start Jaeger with Docker
docker run -d --name jaeger \
-p 16686:16686 \
-p 14250:14250 \
jaegertracing/all-in-one:latest
use OpenTelemetry\Contrib\Jaeger\Exporter as JaegerExporter;
$tracerProvider = new TracerProvider([
new SimpleSpanProcessor(
new JaegerExporter(
'my-service',
'http://localhost:14268/api/traces'
)
)
]);
Globals::registerInitializer(function () use ($tracerProvider) {
return \OpenTelemetry\SDK\Registry::get()->tracerProvider($tracerProvider);
});
$telemetry = TelemetryFactory::create('my-service', '1.0.0');
For cloud-based observability services:
// AWS X-Ray, Google Cloud Trace, Azure Monitor, etc.
$exporter = new SpanExporter($_ENV['OTEL_EXPORTER_OTLP_ENDPOINT']);
// Configure with your cloud provider's specific settings
If you already have OpenTelemetry configured in your application:
// The SDK will automatically use your existing global configuration
$telemetry = TelemetryFactory::create('my-authorization-service');
$client = new Client(
url: $apiUrl,
telemetry: $telemetry
);
// Traces will be included in your existing observability setup
Here’s a complete example showing how telemetry works throughout an authorization workflow:
// Configure telemetry (assumes OpenTelemetry is set up)
$telemetry = TelemetryFactory::create(
serviceName: 'document-service',
serviceVersion: '1.0.0'
);
$client = new Client(
url: $apiUrl,
telemetry: $telemetry
);
try {
// Each operation creates its own span with timing and metadata
// 1. Create store - traced as "openfga.create_store"
$store = $client->createStore(name: 'document-service-store')
->unwrap();
// 2. Create model - traced as "openfga.create_authorization_model"
$model = $client->createAuthorizationModel(
store: $store->getId(),
typeDefinitions: $authModel->getTypeDefinitions()
)->unwrap();
// 3. Write relationships - traced as "openfga.write_tuples"
$client->writeTuples(
store: $store->getId(),
model: $model->getId(),
writes: tuples(
tuple(user: 'user:anne', relation: 'viewer', object: 'document:readme'),
tuple(user: 'user:bob', relation: 'editor', object: 'document:readme')
)
)->unwrap();
// 4. Check authorization - traced as "openfga.check"
$allowed = $client->check(
store: $store->getId(),
model: $model->getId(),
tupleKey: tuple(user: 'user:anne', relation: 'viewer', object: 'document:readme')
)->unwrap();
// 5. List accessible objects - traced as "openfga.list_objects"
$documents = $client->listObjects(
store: $store->getId(),
model: $model->getId(),
user: 'user:anne',
relation: 'viewer',
type: 'document'
)->unwrap();
echo "Authorization check complete. Anne can view document: " .
($allowed->getAllowed() ? 'Yes' : 'No') . "\n";
echo "Documents Anne can view: " . count($documents->getObjects()) . "\n";
} catch (Throwable $e) {
// Errors are automatically recorded in spans
echo "Authorization failed: " . $e->getMessage() . "\n";
}
Performance Analysis:
Error Investigation:
Usage Patterns:
Check if OpenTelemetry is properly installed:
composer show | grep open-telemetry
Verify your exporter configuration:
// Add debug output
$telemetry = TelemetryFactory::create('test-service');
if ($telemetry instanceof \OpenFGA\Observability\OpenTelemetryProvider) {
echo "Using OpenTelemetry provider\n";
} elseif ($telemetry === null) {
echo "No telemetry configured\n";
}
Check your backend connectivity:
The telemetry overhead is minimal in production:
Common OpenTelemetry environment variables that work with the SDK:
# Service identification
export OTEL_SERVICE_NAME="my-authorization-service"
export OTEL_SERVICE_VERSION="1.0.0"
# Exporter configuration
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
export OTEL_EXPORTER_OTLP_HEADERS="api-key=your-api-key"
# Sampling (to reduce overhead in high-traffic scenarios)
export OTEL_TRACES_SAMPLER="traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.1" # Sample 10% of traces
The SDK provides a powerful event-driven telemetry system that allows you to create custom observability solutions without tight coupling to the main client functionality. This approach lets you build specialized listeners for different concerns like logging, metrics collection, alerting, or custom analytics.
The SDK emits events at key points during operation execution:
OperationStartedEvent
- When an OpenFGA operation begins (check, write, etc.)OperationCompletedEvent
- When an operation finishes (success or failure)HttpRequestSentEvent
- When HTTP requests are sent to the OpenFGA APIHttpResponseReceivedEvent
- When HTTP responses are receivedHere’s how to create and register custom event listeners:
// Create a logging listener
// Note: This is an example helper class and not part of the SDK.
final class LoggingEventListener
{
public function onHttpRequestSent(HttpRequestSentEvent $event): void
{
echo "[{$event->getOperation()}] HTTP Request: {$event->getRequest()->getMethod()} {$event->getRequest()->getUri()}\n";
}
public function onHttpResponseReceived(HttpResponseReceivedEvent $event): void
{
$status = $event->getResponse() ? $event->getResponse()->getStatusCode() : 'N/A';
$success = $event->isSuccessful() ? '✅' : '❌';
echo "[{$event->getOperation()}] HTTP Response: {$success} {$status}\n";
}
public function onOperationStarted(OperationStartedEvent $event): void
{
echo "[{$event->getOperation()}] Started - Store: {$event->getStoreId()}\n";
}
public function onOperationCompleted(OperationCompletedEvent $event): void
{
$success = $event->isSuccessful() ? '✅' : '❌';
echo "[{$event->getOperation()}] Completed: {$success}\n";
}
}
// Create a metrics listener
// Note: This is an example helper class and not part of the SDK.
final class MetricsEventListener
{
private array $operationTimes = [];
private array $requestCounts = [];
public function onOperationStarted(OperationStartedEvent $event): void
{
$this->operationTimes[$event->getEventId()] = microtime(true);
}
public function onOperationCompleted(OperationCompletedEvent $event): void
{
$operation = $event->getOperation();
// Count operations
$this->requestCounts[$operation] = ($this->requestCounts[$operation] ?? 0) + 1;
// Track timing
if (isset($this->operationTimes[$event->getEventId()])) {
$duration = microtime(true) - $this->operationTimes[$event->getEventId()];
echo "[{$operation}] completed in " . round($duration * 1000, 2) . "ms\n";
unset($this->operationTimes[$event->getEventId()]);
}
}
public function getMetrics(): array
{
return [
'request_counts' => $this->requestCounts,
'active_operations' => count($this->operationTimes),
];
}
}
Register your listeners with the event dispatcher:
// Create event dispatcher and listeners
$eventDispatcher = new EventDispatcher();
$loggingListener = new LoggingEventListener();
$metricsListener = new MetricsEventListener();
// Register listeners for different events
$eventDispatcher->addListener(HttpRequestSentEvent::class, [$loggingListener, 'onHttpRequestSent']);
$eventDispatcher->addListener(HttpResponseReceivedEvent::class, [$loggingListener, 'onHttpResponseReceived']);
$eventDispatcher->addListener(OperationStartedEvent::class, [$loggingListener, 'onOperationStarted']);
$eventDispatcher->addListener(OperationCompletedEvent::class, [$loggingListener, 'onOperationCompleted']);
// Register metrics listener
$eventDispatcher->addListener(OperationStartedEvent::class, [$metricsListener, 'onOperationStarted']);
$eventDispatcher->addListener(OperationCompletedEvent::class, [$metricsListener, 'onOperationCompleted']);
// Note: In production, you would configure the event dispatcher through dependency injection
// The above example shows the concept for educational purposes
Here’s a complete example showing event-driven telemetry in action:
// Your custom listeners (defined above)
$eventDispatcher = new EventDispatcher();
$loggingListener = new LoggingEventListener();
$metricsListener = new MetricsEventListener();
// Register all listeners
$eventDispatcher->addListener(HttpRequestSentEvent::class, [$loggingListener, 'onHttpRequestSent']);
$eventDispatcher->addListener(HttpResponseReceivedEvent::class, [$loggingListener, 'onHttpResponseReceived']);
$eventDispatcher->addListener(OperationStartedEvent::class, [$loggingListener, 'onOperationStarted']);
$eventDispatcher->addListener(OperationCompletedEvent::class, [$loggingListener, 'onOperationCompleted']);
$eventDispatcher->addListener(OperationStartedEvent::class, [$metricsListener, 'onOperationStarted']);
$eventDispatcher->addListener(OperationCompletedEvent::class, [$metricsListener, 'onOperationCompleted']);
$client = new Client(
url: $apiUrl,
eventDispatcher: $eventDispatcher,
);
// Perform operations - events will be triggered automatically
$storeId = store($client, 'telemetry-demo');
$authModel = dsl($client, '
model
schema 1.1
type user
type document
relations
define viewer: [user]
');
$modelId = model($client, $storeId, $authModel);
write($client, $storeId, $modelId, tuple('user:alice', 'viewer', 'document:report'));
$canView = allowed($client, $storeId, $modelId, tuple('user:alice', 'viewer', 'document:report'));
// View collected metrics
echo "Collected Metrics:\n";
print_r($metricsListener->getMetrics());
Custom Alerting:
// Note: This is an example helper class and not part of the SDK.
final class AlertingEventListener
{
public function onOperationCompleted(OperationCompletedEvent $event): void
{
if (!$event->isSuccessful()) {
// Send alert to your monitoring system
$this->sendAlert([
'operation' => $event->getOperation(),
'store_id' => $event->getStoreId(),
'error' => $event->getException()?->getMessage(),
]);
}
}
}
Security Monitoring:
// Note: This is an example helper class and not part of the SDK.
final class SecurityEventListener
{
public function onOperationStarted(OperationStartedEvent $event): void
{
if ($event->getOperation() === 'check') {
// Log authorization attempts for security analysis
$this->logSecurityEvent([
'timestamp' => time(),
'operation' => $event->getOperation(),
'store_id' => $event->getStoreId(),
'user_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
]);
}
}
}
Performance Analytics:
// Note: This is an example helper class and not part of the SDK.
final class PerformanceEventListener
{
private array $operationTimings = [];
public function onOperationCompleted(OperationCompletedEvent $event): void
{
$timing = $this->calculateTiming($event);
// Export to your analytics platform
$this->exportToAnalytics([
'operation' => $event->getOperation(),
'duration_ms' => $timing,
'store_id' => $event->getStoreId(),
'success' => $event->isSuccessful(),
]);
}
}
In production applications, register listeners through your DI container:
// In your service provider or DI configuration
$container->singleton(EventDispatcher::class, function () {
$dispatcher = new EventDispatcher();
// Register all your listeners
$dispatcher->addListener(OperationStartedEvent::class, [LoggingEventListener::class, 'onOperationStarted']);
$dispatcher->addListener(OperationCompletedEvent::class, [MetricsEventListener::class, 'onOperationCompleted']);
// ... more listeners
return $dispatcher;
});
// Configure the client to use the dispatcher
$container->singleton(Client::class, function ($container) {
return new Client(
url: $apiUrl,
eventDispatcher: $container->get(EventDispatcher::class),
);
});
Add custom context to your authorization operations:
// The SDK automatically includes relevant attributes, but you can add more context
// when configuring your service or through OpenTelemetry's context propagation
// Add custom attributes to the current span
$span = Span::getCurrent();
$span->setAttribute('user.department', 'engineering');
$span->setAttribute('request.source', 'mobile-app');
// Now perform your authorization check
$result = $client->check(
store: $storeId,
model: $modelId,
tupleKey: tuple(user: 'user:anne', relation: 'viewer', object: 'document:readme')
);
The SDK integrates with your application’s existing traces:
// If you have an existing span (for example from a web request)
$parentSpan = $yourFramework->getCurrentSpan();
// OpenFGA operations will automatically become child spans
$result = $client->check(
store: $storeId,
model: $modelId,
tupleKey: tuple(user: 'user:anne', relation: 'viewer', object: 'document:readme')
); // This becomes a child of $parentSpan
If you only want metrics without distributed tracing:
// Configure OpenTelemetry with metrics only
use OpenTelemetry\SDK\Metrics\MeterProvider;
$meterProvider = new MeterProvider(/* your exporters */);
// Don't configure a tracer provider
$telemetry = TelemetryFactory::create('my-service');
Getting Started:
Production Setup:
Integration:
Examples:
For more details on the OpenTelemetry ecosystem, visit the official OpenTelemetry documentation.