# Observability

Emit OpenTelemetry traces and metrics from version 2 of the PHP SDK, with runtime-aware presets, a configurable provider factory, and vendor-neutral tracing and metrics seams.

Version 2 of the PHP SDK can emit a trace span and metrics for every RPC it sends. Tracing and metrics are exposed as vendor-neutral seams, in the same way logging is exposed through PSR-3: the SDK depends only on a `Tracer` and a `Meter` contract, and concrete backends plug in behind them.

Both seams default to a no-op, so the SDK never depends on a telemetry vendor unless you opt in. You enable them by setting a tracer and a meter, either through a [runtime preset](#runtime-presets) or directly on the [`DriverOptions`](/docs/reference/php/v2/api/utilities.md#driveroptions). When either is a real implementation, the SDK adds a telemetry step to its [middleware](/docs/reference/php/v2/concepts/middleware.md) pipeline automatically and instruments every RPC.

## Runtime presets {#runtime-presets}

The simplest way to enable OpenTelemetry is to pass an `ObservabilityOptions` to a [runtime preset](/docs/reference/php/v2/concepts/runtimes.md). The preset builds the OpenTelemetry providers, configures an OTLP exporter, and picks the export strategy that fits the runtime, then wires the tracer and meter onto the `DriverOptions` for you.

The presets need the OpenTelemetry SDK and the OTLP exporter.

```bash
composer require open-telemetry/sdk open-telemetry/exporter-otlp
```

```php
use SurrealDB\SDK\Surreal;
use SurrealDB\SDK\Runtime\Runtime;
use SurrealDB\SDK\Telemetry\OpenTelemetry\ObservabilityOptions;

$observability = new ObservabilityOptions(
    endpoint: 'http://localhost:4318', // OTLP collector
    serviceName: 'my-app',
);

// PHP-FPM / CLI: buffer spans, flush after the request.
$db = new Surreal(Runtime::sync(observability: $observability));

// OpenSwoole: direct, non-blocking export.
$db = new Surreal(Runtime::swoole(observability: $observability));

// FrankenPHP / Amp: direct export over a non-blocking client.
$db = new Surreal(Runtime::amp(observability: $observability));
```

Each preset chooses an export strategy suited to how the runtime handles long work.

| Preset | Strategy | Behaviour |
|--------|----------|-----------|
| `Runtime::sync()` | Batched | Buffers spans in memory and flushes once the request finishes, after `fastcgi_finish_request()` under PHP-FPM, so export stays off the user-visible request path |
| `Runtime::swoole()` | Direct | Exports each span as it ends; OpenSwoole's runtime hooks make that export non-blocking |
| `Runtime::amp()` | Direct | Exports each span as it ends over a non-blocking Amp PSR-18 client, so export yields on the event loop |

The Amp preset additionally needs `amphp/http-client-psr7` for its non-blocking exporter.

```bash
composer require amphp/http-client-psr7
```

## Configuring `ObservabilityOptions`

`ObservabilityOptions` is a value object in the `SurrealDB\SDK\Telemetry\OpenTelemetry` namespace. Every field has a default, so `new ObservabilityOptions()` works against a local collector.

| Option | Default | Description |
|--------|---------|-------------|
| `endpoint` | env, then `http://localhost:4318` | Base OTLP endpoint, with no signal path |
| `contentType` | `application/x-protobuf` | OTLP payload encoding, or `application/json` |
| `headers` | `[]` | Extra headers sent on every export, such as an ingest token |
| `serviceName` | `surrealdb-php` | The `service.name` resource attribute |
| `samplerRatio` | `null` | Head sampling probability in `[0, 1]`; `null` records every span |
| `traces` | `true` | Emit spans |
| `metrics` | `true` | Emit metrics |
| `finishRequestBeforeFlush` | `true` | Batched strategy: call `fastcgi_finish_request()` before draining the buffer |
| `maxQueueSize`, `scheduledDelayMillis`, `maxExportBatchSize` | `null` | Batched strategy tuning; `null` uses the OpenTelemetry defaults |

When `endpoint` is empty, the SDK falls back to the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable, then `http://localhost:4318`. The `/v1/traces` and `/v1/metrics` paths are appended automatically.

```php
$observability = new ObservabilityOptions(
    serviceName: 'checkout-api',
    headers: ['Authorization' => 'Bearer ' . $token],
    samplerRatio: 0.1, // sample 10% of traces
);
```

## Flushing in a framework

The `Runtime::sync()` preset registers a shutdown hook to flush buffered telemetry. When a framework owns the response lifecycle, such as Laravel, build the providers yourself with `OtelObservability` and flush in a terminable hook instead.

```php
use SurrealDB\SDK\Surreal;
use SurrealDB\SDK\Connection\DriverOptions;
use SurrealDB\SDK\Telemetry\OpenTelemetry\ObservabilityOptions;
use SurrealDB\SDK\Telemetry\OpenTelemetry\OtelObservability;

$telemetry = OtelObservability::batched(new ObservabilityOptions(serviceName: 'my-app'));

$db = new Surreal(new DriverOptions(
    tracer: $telemetry->tracer(),
    meter: $telemetry->meter(),
));

// Laravel: flush after the response has been sent to the client.
app()->terminating(static fn () => $telemetry->forceFlush());
```

`OtelObservability` is the factory the presets use. Build it with `batched()` for the FPM/CLI strategy or `direct()` for async runtimes, pass `ampHttpClient()` as the `direct()` client on Amp, and call `forceFlush()` to drain buffered telemetry or `shutdown()` to flush and tear the providers down. Both factories accept optional span and metric exporters, mainly for tests and custom backends.

## Bridging an existing OpenTelemetry setup

If your application already configures the OpenTelemetry SDK, bridge its providers onto the SDK seams rather than building new ones. This needs only `open-telemetry/api`.

```bash
composer require open-telemetry/api
```

Use `fromGlobals()` to read the globally registered provider, so SDK spans nest under whatever ambient span is active in the calling code. You manage the exporters and flushing through your own provider.

```php
use SurrealDB\SDK\Surreal;
use SurrealDB\SDK\Connection\DriverOptions;
use SurrealDB\SDK\Telemetry\OpenTelemetry\OtelTracer;
use SurrealDB\SDK\Telemetry\OpenTelemetry\OtelMeter;

$db = new Surreal(new DriverOptions(
    tracer: OtelTracer::fromGlobals(),
    meter: OtelMeter::fromGlobals(),
));
```

To bridge a provider you manage explicitly, pass its tracer or meter to the constructor.

```php
$db = new Surreal(new DriverOptions(
    tracer: new OtelTracer($tracerProvider->getTracer('surrealdb/surrealdb.php')),
    meter: new OtelMeter($meterProvider->getMeter('surrealdb/surrealdb.php')),
));
```

## PSR-3 logs

When you do not want an OpenTelemetry dependency, the PSR-3 adapter records spans and measurements as structured log lines. It is zero-dependency and useful for getting span timing into logs you already collect.

```php
use SurrealDB\SDK\Surreal;
use SurrealDB\SDK\Connection\DriverOptions;
use SurrealDB\SDK\Telemetry\Psr\Psr3Tracer;
use SurrealDB\SDK\Telemetry\Psr\Psr3Meter;

$db = new Surreal(new DriverOptions(
    tracer: new Psr3Tracer($logger),
    meter: new Psr3Meter($logger),
));
```

Both PSR-3 adapters log at `debug` level by default. Pass a [PSR-3 log level](https://www.php-fig.org/psr/psr-3/#5-psrlogloglevel) as the second argument to change it.

## What is emitted

The telemetry step follows the [OpenTelemetry database semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/). For each RPC it opens a span named `surrealdb.<method>` (for example `surrealdb.query`) with the `Client` span kind, then sets its status to `Ok` or `Error` and records any thrown exception.

| Attribute | Description |
|-----------|-------------|
| `db.system.name` | Always `surrealdb` |
| `db.operation.name` | The RPC method, such as `query` or `signin` |
| `db.surrealdb.session` | The session id, when the call targets a non-default session |
| `db.surrealdb.transaction` | The transaction id, when the call runs inside one |
| `db.query.text` | The SurrealQL text on `query` spans, only when query-text capture is enabled |
| `error.type` | The error kind or exception class, on failures |

Two metrics are recorded per call:

| Metric | Type | Unit | Description |
|--------|------|------|-------------|
| `db.client.operation.duration` | Histogram | `s` | How long each operation took |
| `db.client.operation.count` | Counter | `{operation}` | Number of operations, dimensioned by `outcome` |

> [!NOTE]
> SurrealQL text is not captured by default, because queries can carry sensitive data. The `db.query.text` attribute is only added when you construct a [`TelemetryMiddleware`](/docs/reference/php/v2/concepts/middleware.md) with `recordQueryText: true` and register it yourself.

## Custom backends

To send telemetry somewhere the bundled adapters do not cover, implement the contracts in the `SurrealDB\SDK\Contracts` namespace. A backend can implement tracing, metrics, or both; the two seams are independent.

| Contract | Methods |
|----------|---------|
| `Tracer` | `startSpan(string $name, SpanKind $kind = SpanKind::Client, array $attributes = []): Span` |
| `Span` | `setAttribute()`, `recordException()`, `setStatus()`, `end()` |
| `Meter` | `counter(string $name, ?string $unit, ?string $description): Counter`, `histogram(...): Histogram` |
| `Counter` | `add(int\|float $value = 1, array $attributes = []): void` |
| `Histogram` | `record(int\|float $value, array $attributes = []): void` |

Spans use two enums from `SurrealDB\SDK\Enum`. `SpanKind` has `Internal`, `Client`, `Server`, `Producer`, and `Consumer`. `SpanStatus` has `Unset`, `Ok`, and `Error`.

```php
use SurrealDB\SDK\Contracts\Tracer;
use SurrealDB\SDK\Contracts\Span;
use SurrealDB\SDK\Enum\SpanKind;

final class CustomTracer implements Tracer
{
    public function startSpan(string $name, SpanKind $kind = SpanKind::Client, array $attributes = []): Span
    {
        // return your own Span implementation
    }
}
```

## Logging

Telemetry is for traces and metrics. To log each RPC instead, pass a PSR-3 `logger` to `DriverOptions`. The SDK then adds its logging step to the pipeline automatically, recording every request, its outcome, and its timing. See [Middleware](/docs/reference/php/v2/concepts/middleware.md) for the built-in logging step.

```php
$db = new Surreal(new DriverOptions(logger: $logger));
```

## Learn more

- [Runtimes and workers](/docs/reference/php/v2/concepts/runtimes.md) for the presets that wire observability per runtime
- [Middleware](/docs/reference/php/v2/concepts/middleware.md) for the pipeline the telemetry step runs in
- [Events](/docs/reference/php/v2/concepts/events.md) for PSR-14 events you can observe per request
- [Utilities](/docs/reference/php/v2/api/utilities.md#driveroptions) for the `DriverOptions` fields
