--- title: Instrumentation weight: 20 aliases: [manual] description: Manual instrumentation for OpenTelemetry PHP cSpell:ignore: guzzlehttp myapp --- {{% docs/languages/instrumentation-intro %}} ## Example app preparation {#example-app} These instructions use a modified version of the example app from [Getting Started](/docs/languages/php/getting-started/) to help you learn how to instrument your PHP code. If you want to instrument your own app or library, follow the instructions to adapt the process to your own code. ### Dependencies {#example-app-dependencies} In an empty directory, initialize a minimal `composer.json` file with the following content: ```shell composer init \ --no-interaction \ --require slim/slim:"^4" \ --require slim/psr7:"^1" \ --require monolog/monolog:"^3" composer update ``` ### Create and launch an HTTP Server To highlight the difference between instrumenting a library and a standalone app, split out the dice rolling into a library file, which then will be imported as a dependency by the app file. Create the library file named `dice.php` and add the following code to it: ```php rollOnce(); } return $result; } private function rollOnce() { $result = random_int(1, 6); return $result; } } ``` Create the app file named `index.php` and add the following code to it: ```php pushHandler(new StreamHandler('php://stdout', Level::INFO)); $app = AppFactory::create(); $dice = new Dice(); $app->get('/rolldice', function (Request $request, Response $response) use ($logger, $dice) { $params = $request->getQueryParams(); if(isset($params['rolls'])) { $result = $dice->roll($params['rolls']); if (isset($params['player'])) { $logger->info("A player is rolling the dice.", ['player' => $params['player'], 'result' => $result]); } else { $logger->info("Anonymous player is rolling the dice.", ['result' => $result]); } $response->getBody()->write(json_encode($result)); } else { $response->withStatus(400)->getBody()->write("Please enter a number of rolls"); } return $response; }); $app->run(); ``` To ensure that it is working, run the application with the following command and open in your web browser. ```shell php -S localhost:8080 ``` ## Instrumentation setup ### Dependencies Install OpenTelemetry API packages: ```shell composer require open-telemetry/api open-telemetry/sem-conv ``` ### Initialize the SDK {{% alert title="Note" color="info" %}} If you’re instrumenting a library, **skip this step**. {{% /alert %}} To use the OpenTelemetry SDK for PHP you need packages that satisfy the dependencies for `psr/http-client-implementation` and `psr/http-factory-implementation`. Here we will use Guzzle, which provides both: ```sh composer require guzzlehttp/guzzle ``` Now you can install the OpenTelemetry SDK, and OTLP exporter: ```sh composer require \ open-telemetry/sdk \ open-telemetry/exporter-otlp ``` If you are an application developer, you need to configure an instance of the `OpenTelemetry SDK` as early as possible in your application. Here we will use the `Sdk::builder()` method, and we will globally register the providers. You can build the providers by using the `TracerProvider::builder()`, `LoggerProvider::builder()`, and `MeterProvider::builder()` methods. In the case of the [example app](#example-app), create a file named `instrumentation.php` with the following content: ```php merge(ResourceInfo::create(Attributes::create([ ResourceAttributes::SERVICE_NAMESPACE => 'demo', ResourceAttributes::SERVICE_NAME => 'test-application', ResourceAttributes::SERVICE_VERSION => '0.1', ResourceAttributes::DEPLOYMENT_ENVIRONMENT => 'development', ]))); $spanExporter = new SpanExporter( (new StreamTransportFactory())->create('php://stdout', 'application/json') ); $logExporter = new LogsExporter( (new StreamTransportFactory())->create('php://stdout', 'application/json') ); $reader = new ExportingReader( new MetricExporter( (new StreamTransportFactory())->create('php://stdout', 'application/json') ) ); $meterProvider = MeterProvider::builder() ->setResource($resource) ->addReader($reader) ->build(); $tracerProvider = TracerProvider::builder() ->addSpanProcessor( new SimpleSpanProcessor($spanExporter) ) ->setResource($resource) ->setSampler(new ParentBased(new AlwaysOnSampler())) ->build(); $loggerProvider = LoggerProvider::builder() ->setResource($resource) ->addLogRecordProcessor( new SimpleLogRecordProcessor($logExporter) ) ->build(); Sdk::builder() ->setTracerProvider($tracerProvider) ->setMeterProvider($meterProvider) ->setLoggerProvider($loggerProvider) ->setPropagator(TraceContextPropagator::getInstance()) ->setAutoShutdown(true) ->buildAndRegisterGlobal(); ``` Include this code at the top of your application file `index.php`: ```php getTracer( 'instrumentation-scope-name', //name (required) 'instrumentation-scope-version', //version 'http://example.com/my-schema', //schema url ['foo' => 'bar'] //attributes ); ``` The values of `instrumentation-scope-name` and `instrumentation-scope-version` should uniquely identify the [Instrumentation Scope](/docs/concepts/instrumentation-scope/), such as the package, module or class name. While the name is required, the version is still recommended despite being optional. It's generally recommended to call `getTracer` in your app when you need it rather than exporting the `tracer` instance to the rest of your app. This helps avoid trickier application load issues when other required dependencies are involved. In the case of the [example app](#example-app), there are two places where a tracer may be acquired with an appropriate Instrumentation Scope: First, in the _application file_ `index.php`: ```php getTracer( 'dice-server', '0.1.0', 'https://opentelemetry.io/schemas/1.24.0' ); $logger = new Logger('dice-server'); $logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO)); $app = AppFactory::create(); $dice = new Dice(); $app->get('/rolldice', function (Request $request, Response $response) use ($logger, $dice, $tracer) { // ... } ``` And second, in the _library file_ `dice.php`: ```php tracer = $tracerProvider->getTracer( 'dice-lib', '0.1.0', 'https://opentelemetry.io/schemas/1.24.0' ); } public function roll($rolls) { $result = []; for ($i = 0; $i < $rolls; $i++) { $result[] = $this->rollOnce(); } return $result; } private function rollOnce() { $result = random_int(1, 6); return $result } } ``` ### Create Spans Now that you have [tracers](/docs/concepts/signals/traces/#tracer) initialized, you can create [spans](/docs/concepts/signals/traces/#spans). The code below illustrates how to create a span. ```php tracer->spanBuilder("rollTheDice")->startSpan(); $result = []; for ($i = 0; $i < $rolls; $i++) { $result[] = $this->rollOnce(); } $span->end(); return $result; } ``` Note, that it's required to `end()` the span, otherwise it will not be exported. If you followed the instructions using the [example app](#example-app) up to this point, you can copy the code above in your library file `dice.php`. You should now be able to see spans emitted from your app. Start your app as follows, and then send it requests by visiting with your browser or `curl`. ```sh php -S 8080 localhost ``` After a while, you should see the spans printed in the console by the `SpanExporter`, something like this: ```json { "resourceSpans": [ { "resource": { "attributes": [ { "key": "service.namespace", "value": { "stringValue": "demo" } }, { "key": "service.name", "value": { "stringValue": "test-application" } }, { "key": "service.version", "value": { "stringValue": "0.1" } }, { "key": "deployment.environment", "value": { "stringValue": "development" } } ] }, "scopeSpans": [ { "scope": { "name": "dice-lib", "version": "0.1.0" }, "spans": [ { "traceId": "007a1e7a89f21f98b600d288b7d65390", "spanId": "c32797fc72c252d2", "flags": 1, "name": "rollTheDice", "kind": 1, "startTimeUnixNano": "1706111239077485365", "endTimeUnixNano": "1706111239077735657", "status": {} } ], "schemaUrl": "https://opentelemetry.io/schemas/1.24.0" } ] } ] } ``` ### Create nested Spans Nested [spans](/docs/concepts/signals/traces/#spans) let you track work that's nested in nature. For example, the `rollOnce()` function below represents a nested operation. The following sample creates a nested span that tracks `rollOnce()`: ```php private function rollOnce() { $parent = OpenTelemetry\API\Trace\Span::getCurrent(); $scope = $parent->activate(); try { $span = $this->tracer->spanBuilder("rollTheDice")->startSpan(); $result = random_int(1, 6); $span->end(); } finally { $scope->detach(); } return $result; } ``` You _must_ `detach` the active scope if you have activated it. ### Get the current span In the example above, we retrieved the current span, using the following method: ```php $span = OpenTelemetry\API\Trace\Span::getCurrent(); ``` ### Get a span from context It can also be helpful to get the [span](/docs/concepts/signals/traces/#spans) from a given context that isn't necessarily the active span. ```php $span = OpenTelemetry\API\Trace\Span::fromContext($context); ``` ### Span Attributes [Attributes](/docs/concepts/signals/traces/#attributes) let you attach key/value pairs to a [`Span`](/docs/concepts/signals/traces/#spans) so it carries more information about the current operation that it's tracking. ```php private function rollOnce() { $parent = OpenTelemetry\API\Trace\Span::getCurrent(); $scope = $parent->activate(); try { $span = $this->tracer->spanBuilder("rollTheDice")->startSpan(); $result = random_int(1, 6); $span->setAttribute('dicelib.rolled', $result); $span->end(); } finally { $scope->detach(); } return $result; } ``` #### Semantic Attributes There are semantic conventions for spans representing operations in well-known protocols like HTTP or database calls. Semantic conventions for these spans are defined in the specification at [Trace Semantic Conventions](/docs/specs/semconv/general/trace/). In the simple example of this guide the source code attributes can be used. First add the semantic conventions as a dependency to your application: ```shell composer require open-telemetry/sem-conv ``` Add the following to the top of your file: ```php use OpenTelemetry\SemConv\TraceAttributes; use OpenTelemetry\SemConv\TraceAttributeValues; ``` Finally, you can update your file to include semantic attributes: ```php $span->setAttribute(TraceAttributes::CODE_FUNCTION, 'rollOnce'); $span->setAttribute(TraceAttributes::CODE_FILEPATH, __FILE__); ``` ### Create Spans with events [Spans](/docs/concepts/signals/traces/#spans) can be annotated with named events (called [Span Events](/docs/concepts/signals/traces/#span-events)) that can carry zero or more [Span Attributes](#span-attributes), each of which itself is a key:value map paired automatically with a timestamp. ```php $span->addEvent("Init"); ... $span->addEvent("End"); ``` ```php $eventAttributes = Attributes::create([ "operation" => "calculate-pi", "result" => 3.14159, ]); $span->addEvent("End Computation", $eventAttributes); ``` ### Create Spans with links A [Span](/docs/concepts/signals/traces/#spans) may be linked to zero or more other Spans that are causally related via a [Span Link](/docs/concepts/signals/traces/#span-links). Links can be used to represent batched operations where a Span was initiated by multiple initiating Spans, each representing a single incoming item being processed in the batch. ```php $span = $tracer->spanBuilder("span-with-links") ->addLink($parentSpan1->getContext()) ->addLink($parentSpan2->getContext()) ->addLink($parentSpan3->getContext()) ->addLink($remoteSpanContext) ->startSpan(); ``` For more details how to read context from remote processes, see [Context Propagation](../propagation/). ### Set span status and record exceptions {{% docs/languages/span-status-preamble %}} It can be a good idea to record exceptions when they happen. It's recommended to do this in conjunction with [setting span status](/docs/specs/otel/trace/api/#set-status). The status can be set at any time before the span is finished: ```php $span = $tracer->spanBuilder("my-span")->startSpan(); try { // do something that could fail throw new \Exception('uh-oh'); } catch (\Throwable $t) { $span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR, "Something bad happened!"); //This will capture things like the current stack trace in the span. $span->recordException($t, ['exception.escaped' => true]); throw $t; } finally { $span->end(); } ``` ### Span Processor Different Span processors are offered by OpenTelemetry. The `SimpleSpanProcessor` immediately forwards ended spans to the exporter, while the `BatchSpanProcessor` batches them and sends them periodically. ```php $tracerProvider = TracerProvider::builder() ->addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporterFactory()->create())) ->build(); ``` ### Transports All exporters require a `Transport`, which is responsible for the sending of telemetry data: - `PsrTransport` - uses a PSR-18 client to send data over HTTP - `StreamTransport` - uses a stream to send data (eg to file or `stdout`) - `GrpcTransport` - uses gRPC to send protobuf-encoded data ### Exporter See [Exporters](/docs/languages/php/exporters) ## Metrics OpenTelemetry can be used to measure and record different types of metrics from an application, which can then be [pushed](/docs/specs/otel/metrics/sdk/#push-metric-exporter) to a metrics service such as the OpenTelemetry collector: - counter - async counter - histogram - async gauge - up/down counter - async up/down counter Meter types and usage are explained in the [metrics concepts](/docs/concepts/signals/metrics/) documentation. ### Setup First, create a `MeterProvider`: ```php create()); $meterProvider = MeterProvider::builder() ->addReader($reader) ->build(); ``` ### Synchronous meters A synchronous meter must be manually adjusted as data changes: ```php $up_down = $meterProvider ->getMeter('demo_meter') ->createUpDownCounter('queued', 'jobs', 'The number of jobs enqueued'); //jobs come in $up_down->add(5); //job completed $up_down->add(-1); //more jobs come in $up_down->add(2); $meterProvider->forceFlush(); ``` Synchronous metrics are exported when `forceFlush()` or `shutdown()` are called on the meter provider.
View output ```json { "resourceMetrics": [ { "resource": {}, "scopeMetrics": [ { "scope": { "name": "demo_meter" }, "metrics": [ { "name": "queued", "description": "The number of jobs enqueued", "unit": "jobs", "sum": { "dataPoints": [ { "startTimeUnixNano": "1687332126443709851", "timeUnixNano": "1687332126445544432", "asInt": "6" } ], "aggregationTemporality": "AGGREGATION_TEMPORALITY_DELTA" } } ] } ] } ] } ```
### Asynchronous meters Async meters are `observable`, eg `ObservableGauge`. When registering an observable/async meter, you provide one or more callback functions. The callback functions will be called by a [periodic exporting metric reader](/docs/specs/otel/metrics/sdk/#periodic-exporting-metricreader) whenever its `collect()` method is called, for example based on an event-loop timer. The callback(s) are responsible for returning the current data for the meter. In this example, the callbacks are executed every time that `$reader->collect()` is executed: ```php $queue = [ 'job1', 'job2', 'job3', ]; $reader = $meterProvider ->getMeter('demo_meter') ->createObservableGauge('queued', 'jobs', 'The number of jobs enqueued') ->observe(static function (ObserverInterface $observer) use (&$queue): void { $observer->observe(count($queue)); }); $reader->collect(); array_pop($queue); $reader->collect(); ```
View output ```json {"resourceMetrics":[{"resource":{},"scopeMetrics":[{"scope":{"name":"demo_meter"},"metrics":[{"name":"queued","description":"The number of jobs enqueued","unit":"jobs","gauge":{"dataPoints":[{"startTimeUnixNano":"1687331630161510994","timeUnixNano":"1687331630162989144","asInt":"3"}]}}]}]}]} {"resourceMetrics":[{"resource":{},"scopeMetrics":[{"scope":{"name":"demo_meter"},"metrics":[{"name":"queued","description":"The number of jobs enqueued","unit":"jobs","gauge":{"dataPoints":[{"startTimeUnixNano":"1687331630161510994","timeUnixNano":"1687331631164759171","asInt":"2"}]}}]}]}]} ```
## Logs As logging is a mature and well-established function, the [OpenTelemetry approach](/docs/concepts/signals/logs/) is a little different for this signal. The OpenTelemetry logger is not designed to be used directly, but rather to be integrated into existing logging libraries. In this way, you can choose to have some or all of your application logs sent to an OpenTelemetry-compatible service such as the [collector](/docs/collector/). ### Setup First, we create a `LoggerProvider`: ```php create('php://stdout', 'application/json') ); $loggerProvider = LoggerProvider::builder() ->addLogRecordProcessor(new SimpleLogRecordProcessor($exporter)) ->setResource(ResourceInfoFactory::emptyResource()) ->build(); ``` ### Logging events An `EventLogger` can use a `Logger` to emit log events: ```php $logger = $loggerProvider->getLogger('demo', '1.0', 'http://schema.url', [/*attributes*/]); $eventLogger = new EventLogger($logger, 'my-domain'); $record = (new LogRecord('hello world')) ->setSeverityText('INFO') ->setAttributes([/*attributes*/]); $eventLogger->logEvent('foo', $record); ```
View sample output ```json { "resourceLogs": [ { "resource": {}, "scopeLogs": [ { "scope": { "name": "demo", "version": "1.0" }, "logRecords": [ { "observedTimeUnixNano": "1687496730010009088", "severityText": "INFO", "body": { "stringValue": "hello world" }, "attributes": [ { "key": "event.name", "value": { "stringValue": "foo" } }, { "key": "event.domain", "value": { "stringValue": "my-domain" } } ] } ] } ] } ] } ```
### Integrations for third-party logging libraries #### Monolog You can use the [monolog handler](https://packagist.org/packages/open-telemetry/opentelemetry-logger-monolog) to send monolog logs to an OpenTelemetry-capable receiver. First, install the monolog library and a handler: ```shell composer require \ monolog/monolog \ open-telemetry/opentelemetry-logger-monolog ``` Following on from the logging example above: ```php $handler = new \OpenTelemetry\Contrib\Logs\Monolog\Handler( $loggerProvider, \Psr\Log\LogLevel::ERROR, ); $monolog = new \Monolog\Logger('example', [$handler]); $monolog->info('hello, world'); $monolog->error('oh no', [ 'foo' => 'bar', 'exception' => new \Exception('something went wrong'), ]); ```
View sample output ```json { "resourceLogs": [ { "resource": {}, "scopeLogs": [ { "scope": { "name": "monolog" }, "logRecords": [ { "timeUnixNano": "1687496945597429000", "observedTimeUnixNano": "1687496945598242048", "severityNumber": "SEVERITY_NUMBER_ERROR", "severityText": "ERROR", "body": { "stringValue": "oh no" }, "attributes": [ { "key": "channel", "value": { "stringValue": "example" } }, { "key": "context", "value": { "arrayValue": { "values": [ { "stringValue": "bar" }, { "arrayValue": { "values": [ { "stringValue": "Exception" }, { "stringValue": "something went wrong" }, { "intValue": "0" }, { "stringValue": "/usr/src/myapp/logging.php:31" } ] } } ] } } } ] } ] } ] } ] } ```
## Error Handling By default, OpenTelemetry will log errors and warnings via PHP's [`error_log`](https://www.php.net/manual/en/function.error-log.php) function. The verbosity can be controlled or disabled via the `OTEL_LOG_LEVEL` setting. The `OTEL_PHP_LOG_DESTINATION` variable can be used to control log destination or disable error logging completely. Valid values are `default`, `error_log`, `stderr`, `stdout`, `psr3`, or `none`. `default` (or if the variable is not set), will use `error_log` unless a PSR-3 logger is configured: ```php $logger = new \Example\Psr3Logger(LogLevel::INFO); \OpenTelemetry\API\LoggerHolder::set($logger); ``` For more fine-grained control and special case handling, custom handlers and filters can be applied to the PSR-3 logger (if the logger offers this ability). ## Next steps You'll also want to configure an appropriate exporter to [export your telemetry data](/docs/languages/php/exporters) to one or more telemetry backends.