|
| 1 | +# Integrating Opentelemetry PHP into Laravel Applications |
| 2 | + |
| 3 | +## Introduction |
| 4 | +Distributed tracing helps developers and management gain insights into how well applications perform in terms of traces, metrics, and logs. This guide shows how developers can integrate OpenTelemetry PHP into their Laravel applications for the above benefits. Our example application visualizes exceptions from a Laravel application using both Jaeger and Zipkin. |
| 5 | + |
| 6 | +To follow this guide you will need: |
| 7 | + |
| 8 | +* PHP Installed; this example uses PHP 7.4. |
| 9 | +* [Composer](https://getcomposer.org/download/ ) for dependency management. |
| 10 | +* [Docker](https://docs.docker.com/get-docker/) for bundling our visualization tools. We have setup instructions for docker on this project's [readme](https://github.com/open-telemetry/opentelemetry-php#development). |
| 11 | + |
| 12 | +This example uses Laravel version 8.0 . |
| 13 | + |
| 14 | +## Step 1 - Creating a Laravel Application |
| 15 | + |
| 16 | +The Laravel framework supports creating applications using composer. To do that, run `composer create-project <project-name>` . We are naming our project `otel-php-laravel-basic-example`, so the command is as follows: |
| 17 | + |
| 18 | +`composer create-project laravel/laravel otel-php-laravel-basic-example` |
| 19 | + |
| 20 | +To confirm that our application works, we can move to the application directory using `cd otel-php-laravel-basic-example` , then serve the application with `php artisan serve` . |
| 21 | + |
| 22 | + |
| 23 | + |
| 24 | +Let's navigate to `http://127.0.0.1:8000` on our browser to see the default Laravel welcome page. |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +## Step 2 - Require OpenTelemetry PHP Package |
| 29 | + |
| 30 | +Laravel comes with most packages needed for development out of the box, so for this example, we will only require the open-telemetry PHP package. Let's run `composer require open-telemetry/opentelemetry` to pull that in. |
| 31 | + |
| 32 | +** Notes ** |
| 33 | +As of the time of writing this, Laravel ships with Guzzle version `^7.0.1`, but our open-telemetry PHP package uses Guzzle version `^6.2.0`, so pulling in open-telemetry PHP could lead to errors around unresolved packages. To fix the errors run `composer require guzzlehttp/guzzle:^6.2.0` to downgrade Guzzle first. Then run `composer require open-telemetry/opentelemetry` to pull in the open-telemetry package. |
| 34 | + |
| 35 | +## Step 3 - Bundle Zipkin and Jaeger into the Application |
| 36 | + |
| 37 | +To visualize traces exported from our application, we need to integrate open source tracing tools [Zipkin](https://zipkin.io/) and [Jaeger](https://www.jaegertracing.io/) into our setup using docker. |
| 38 | + |
| 39 | +First, we create a `docker-compose.yaml` file in the root of our project, with content as follows: |
| 40 | + |
| 41 | +```yaml |
| 42 | +version: '3.7' |
| 43 | +services: |
| 44 | + zipkin: |
| 45 | + image: openzipkin/zipkin-slim |
| 46 | + ports: |
| 47 | + - 9411:9411 |
| 48 | + jaeger: |
| 49 | + image: jaegertracing/all-in-one |
| 50 | + environment: |
| 51 | + COLLECTOR_ZIPKIN_HTTP_PORT: 9412 |
| 52 | + ports: |
| 53 | + - 9412:9412 |
| 54 | + - 16686:16686 |
| 55 | +``` |
| 56 | +
|
| 57 | +Next, we pull in Zipkin and Jaeger by running `docker-compose up -d`. This might take some time, depending on your internet connection speed. |
| 58 | + |
| 59 | + |
| 60 | + |
| 61 | +We can confirm that Zipkin is up by navigating to `http://localhost:9411/` on our browser. For Jaeger, navigating to `http://localhost:16686/` on our browser should display the Jaeger home page. |
| 62 | + |
| 63 | + |
| 64 | + |
| 65 | + |
| 66 | + |
| 67 | +## Step 5 - Instrument Laravel Application |
| 68 | + |
| 69 | +For this step, we will utilize our OpenTelemetry PHP Library to export traces to both Zipkin and Jaeger. |
| 70 | + |
| 71 | +The default entry point for Laravel applications is the `index.php` file located in the `public` folder. If we navigate to `public\index.php` we can see that the index file autoloads classes from packages within our vendor folder, making them easily useable within our application. |
| 72 | + |
| 73 | +```php |
| 74 | +require __DIR__.'/../vendor/autoload.php'; |
| 75 | +``` |
| 76 | + |
| 77 | +The other parts of the `index.php` file enable request and response resolution using the application kernel. |
| 78 | + |
| 79 | +```php |
| 80 | +$app = require_once __DIR__.'/../bootstrap/app.php'; |
| 81 | +
|
| 82 | +$kernel = $app->make(Kernel::class); |
| 83 | +
|
| 84 | +$response = tap($kernel->handle( |
| 85 | + $request = Request::capture() |
| 86 | +))->send(); |
| 87 | +
|
| 88 | +$kernel->terminate($request, $response); |
| 89 | +``` |
| 90 | + It is worthy of note that resources(namespaces, classes, variables) created within the `index.php` file are available within the entire application. |
| 91 | + |
| 92 | + To use open-telemetry specific classes within our application we have to import them at the top of our index file, using the `use` keyword. This is what our imports look like: |
| 93 | + |
| 94 | + ```php |
| 95 | + use OpenTelemetry\Contrib\Jaeger\Exporter as JaegerExporter; |
| 96 | +use OpenTelemetry\Contrib\Zipkin\Exporter as ZipkinExporter; |
| 97 | +use OpenTelemetry\Sdk\Trace\Clock; |
| 98 | +use OpenTelemetry\Sdk\Trace\Sampler\AlwaysOnSampler; |
| 99 | +use OpenTelemetry\Sdk\Trace\SamplingResult; |
| 100 | +use OpenTelemetry\Sdk\Trace\SpanProcessor\BatchSpanProcessor; |
| 101 | +use OpenTelemetry\Sdk\Trace\TracerProvider; |
| 102 | +use OpenTelemetry\Trace as API; |
| 103 | +``` |
| 104 | + |
| 105 | +Next, we create a sample recording trace using the [AlwaysOnSampler](https://github.com/open-telemetry/opentelemetry-php/blob/main/sdk/Trace/Sampler/AlwaysOnSampler.php) class, just before the Kernel instance is created like below: |
| 106 | + |
| 107 | +```php |
| 108 | +$sampler = new AlwaysOnSampler(); |
| 109 | +$samplingResult = $sampler->shouldSample( |
| 110 | + null, |
| 111 | + md5((string) microtime(true)), |
| 112 | + substr(md5((string) microtime(true)), 16), |
| 113 | + 'io.opentelemetry.example', |
| 114 | + API\SpanKind::KIND_INTERNAL |
| 115 | +); |
| 116 | +``` |
| 117 | + |
| 118 | +Since we are looking to export traces to both Zipkin and Jaeger we have to make use of their exporters; |
| 119 | + |
| 120 | +```php |
| 121 | +$jaegerExporter = new JaegerExporter( |
| 122 | + 'Hello World Web Server Jaeger', |
| 123 | + 'http://localhost:9412/api/v2/spans' |
| 124 | +); |
| 125 | +
|
| 126 | +$zipkinExporter = new ZipkinExporter( |
| 127 | + 'Hello World Web Server Zipkin', |
| 128 | + 'http://localhost:9411/api/v2/spans' |
| 129 | +); |
| 130 | +``` |
| 131 | + |
| 132 | +Next, we create a trace then add processors for each trace(One for Jaeger and another for Zipkin). Then we proceed to start and activate a span for each trace. We create a trace only if the RECORD AND SAMPLED sampling result condition passes as follows; |
| 133 | + |
| 134 | +```php |
| 135 | +if (SamplingResult::RECORD_AND_SAMPLED === $samplingResult->getDecision()) { |
| 136 | +
|
| 137 | + $jaegerTracer = (new TracerProvider()) |
| 138 | + ->addSpanProcessor(new BatchSpanProcessor($jaegerExporter, Clock::get())) |
| 139 | + ->getTracer('io.opentelemetry.contrib.php'); |
| 140 | +
|
| 141 | + $zipkinTracer = (new TracerProvider()) |
| 142 | + ->addSpanProcessor(new BatchSpanProcessor($zipkinExporter, Clock::get())) |
| 143 | + ->getTracer('io.opentelemetry.contrib.php'); |
| 144 | +
|
| 145 | + $request = Request::createFromGlobals(); |
| 146 | + $jaegerSpan = $jaegerTracer->startAndActivateSpan($request->getUri()); |
| 147 | + $zipkinSpan = $zipkinTracer->startAndActivateSpan($request->getUri()); |
| 148 | +
|
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +Finally, we end the active spans if sampling is complete, by adding the following block at the end of the `index.php` file; |
| 153 | + |
| 154 | +```php |
| 155 | +if (SamplingResult::RECORD_AND_SAMPLED === $samplingResult->getDecision()) { |
| 156 | + $zipkinTracer->endActiveSpan(); |
| 157 | + $jaegerTracer->endActiveSpan(); |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +Let's confirm that we can see exported traces on both Zipkin and Jaeger. To do that, we need to reload `http://127.0.0.1:8000` on our browser; |
| 162 | + |
| 163 | +We also need reload both Zipkin and Jaeger on our browser, using the URLs `http://localhost:9411/` and `http://localhost:16686/`. Do ensure that both your Laravel server and docker instance are running for this step. |
| 164 | + |
| 165 | +For Jaeger under service, you should see a `Hello World Web Server Jaeger` service. Go ahead and click find traces to see exported traces. |
| 166 | + |
| 167 | + |
| 168 | + |
| 169 | + |
| 170 | +Once we click on `Find Traces`, you should be able to see traces like below: |
| 171 | + |
| 172 | + |
| 173 | + |
| 174 | + |
| 175 | + |
| 176 | + |
| 177 | +We can click on a trace to get more information about the trace. |
| 178 | + |
| 179 | + |
| 180 | + |
| 181 | + |
| 182 | +For Zipkin, we can visualize our trace by clicking on `Run Query` |
| 183 | + |
| 184 | + |
| 185 | + |
| 186 | +Since resources in Laravel's `public\index.php` file are available to the entire application, we can use any of the already instantiated tracers to further instrument controllers or any other parts of our application. |
| 187 | + |
| 188 | +Let's create a `Hello` controller to check this out. Run the command `php artisan make:controller HelloController` |
| 189 | + |
| 190 | + |
| 191 | + |
| 192 | +Next we need to add a route for accessing the controller. To do this we need to utilize the `HelloController` class within our web routes file located in the `routes\web.php` as follows: |
| 193 | + |
| 194 | +```php |
| 195 | +use App\Http\Controllers\HelloController; |
| 196 | +``` |
| 197 | +Next we need to add a route and method for the controller. |
| 198 | + |
| 199 | +```php |
| 200 | +Route::get('/hello', [HelloController::class, 'index']); |
| 201 | +``` |
| 202 | +The above snippet routes every GET request from the `/hello` route on the browser to an index method within the `HelloController` class. For now, this method does not exist, so we have to add it to our controller as follows |
| 203 | + |
| 204 | +```php |
| 205 | +public function index(){ |
| 206 | + return "hello"; |
| 207 | +} |
| 208 | +``` |
| 209 | +Let's confirm that everything works well by visiting the `/hello` route on our browser. |
| 210 | + |
| 211 | + |
| 212 | + |
| 213 | +Now that we have the `index` method working, we can simulate adding an exception event to our Zipkin trace as follows: |
| 214 | + |
| 215 | +```php |
| 216 | +global $zipkinTracer; |
| 217 | +if ($zipkinTracer) { |
| 218 | + /** @var Span $span */ |
| 219 | + $span = $zipkinTracer->getActiveSpan(); |
| 220 | + |
| 221 | + $span->setAttribute('foo', 'bar'); |
| 222 | + $span->updateName('New name'); |
| 223 | +
|
| 224 | + $zipkinTracer->startAndActivateSpan('Child span'); |
| 225 | + try { |
| 226 | + throw new \Exception('Exception Example'); |
| 227 | + } catch (\Exception $exception) { |
| 228 | + $span->setSpanStatus($exception->getCode(), $exception->getMessage()); |
| 229 | + } |
| 230 | + $zipkinTracer->endActiveSpan(); |
| 231 | +} |
| 232 | +``` |
| 233 | +In the above snippet we change the span name and attributes for our Zipkin trace, we also add an exception event to the span. |
| 234 | + |
| 235 | +We need to reload our `http://127.0.0.1:8000/hello` route, then navigate to Zipkin like before, to see that our span name gets updated to `new name` and our `Exception Example` is visible. |
| 236 | + |
| 237 | + |
| 238 | + |
| 239 | +## Summary |
| 240 | +With the above example we have been able to instrument a Laravel application using the OpenTelemetry PHP library. You can fork the example project [here](https://github.com/prondubuisi/otel-php-laravel-basic-example). |
0 commit comments