As a developer, you might be wondering how OpenTelemetry could be beneficial to you. Without practical examples, the usefulness of distributed tracing can be difficult to grasp for persons without a cloud or site reliability engineering background. This user guide shows how OpenTelemetry could be useful to gain insights into exceptions happening within an application. This example uses the OpenTelemtry PHP library integrated into a Symfony application, bundled with Jaeger and Zipkin, for visualizing data.
To follow this guide you will need:
- PHP Installed, this example uses PHP 7.4.
- Composer for dependency management.
- Symfony CLI for managing your Symfony application.
- Docker for bundling our visualization tools. We have setup instructions for docker on this project's readme.
This example uses Symfony version 5.2 .
Create a Symfony application by running the command symfony new my_project_name
. We are calling this example otel-php-symfony-basic-example
, so the command is as follows;
symfony new otel-php-symfony-basic-example
.
To define our routes within our controller methods, let's require the Doctrine annotation library by running the command composer require doctrine/annotations
.
We can test that routes defined within Controllers work by creating a HelloController.php
file within the src\Controller
folder as follows:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class HelloController extends AbstractController
{
/**
* @Route("/hello", name="hello")
*/
public function index(): Response
{
return new Response('Hello World');
}
}
To check out the routes available in our current project run php bin/console debug:router
.
Let's confirm that our application works by running the command symfony server:start
.
You can navigate to http://127.0.0.1:8000/hello
route to see the Hello world
response returned from the HelloController index method above.
For this step, we require the OpenTelemetry PHP Library by running the command composer require open-telemetry/opentelemetry
. It is worthy of note that this command pulls in the last stable release for the library.
To visualize traces from our application, we have to bundle open source tracing tools Zipkin and Jaeger into our application using docker.
Let's add a docker-compose.yaml
file in the root of our project with the content as follows:
version: '3.7'
services:
zipkin:
image: openzipkin/zipkin-slim
ports:
- 9411:9411
jaeger:
image: jaegertracing/all-in-one
environment:
COLLECTOR_ZIPKIN_HOST_PORT: 9412 # Before version 1.22.0 use COLLECTOR_ZIPKIN_HTTP_PORT
ports:
- 9412:9412
- 16686:16686
To confirm that docker is installed and running on our system, we can run the hello world docker example using the command docker run -it --rm hello-world
. If everything works well, run docker-compose up -d
to pull in Zipkin and Jaeger. This might take some time, depending on your internet connection speed.
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.
Now it is time to utilize our OpenTelemetry PHP Library to export traces to both Zipkin and Jaeger.
The entry point for all Symfony applications is the index.php
file located in the public
folder. Let's navigate to public\index.php
to see what is happening. It is worthy of note that resources(namespaces, classes, variables) created within the index.php
file are available within the entire application, by default the index file imports all auto loaded classes within the vendor folder. It also imports contents of the .env
file. The other parts of the index.php
file enable debugging as well as support request and response resolution using the application kernel.
To use open-telemetry specific classes we have to import them at the top of our index file, using the use
keyword. This is what our imports look like:
use App\Kernel;
use OpenTelemetry\Contrib\Jaeger\Exporter as JaegerExporter;
use OpenTelemetry\Contrib\Zipkin\Exporter as ZipkinExporter;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
use OpenTelemetry\SDK\Trace\SamplingResult;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\API\Trace as API;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;
Next, we create a sample recording trace using the AlwaysOnSampler class, just before the Kernel instance is created like below:
$sampler = new AlwaysOnSampler();
$samplingResult = $sampler->shouldSample(
null,
md5((string) microtime(true)),
substr(md5((string) microtime(true)), 16),
'io.opentelemetry.example',
API\SpanKind::KIND_INTERNAL
);
Since we are looking to export traces to both Zipkin and Jaeger we have to make use of their individual exporters;
$jaegerExporter = new JaegerExporter(
'Hello World Web Server Jaeger',
'http://localhost:9412/api/v2/spans'
);
$zipkinExporter = new ZipkinExporter(
'Hello World Web Server Zipkin',
'http://localhost:9411/api/v2/spans'
);
Next we create a trace, and 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;
if (SamplingResult::RECORD_AND_SAMPLED === $samplingResult->getDecision()) {
$jaegerTracer = (new TracerProvider(null, $sampler))
->addSpanProcessor(new BatchSpanProcessor($jaegerExporter, Clock::get()))
->getTracer('io.opentelemetry.contrib.php');
$zipkinTracer = (new TracerProvider(null, $sampler))
->addSpanProcessor(new BatchSpanProcessor($zipkinExporter, Clock::get()))
->getTracer('io.opentelemetry.contrib.php');
$request = Request::createFromGlobals();
$jaegerSpan = $jaegerTracer->startAndActivateSpan($request->getUri());
$zipkinSpan = $zipkinTracer->startAndActivateSpan($request->getUri());
}
Finally we end the active spans if sampling is complete, by adding the following block at the end of the index.php
file;
if (SamplingResult::RECORD_AND_SAMPLED === $samplingResult->getDecision()) {
$zipkinSpan->end();
$jaegerSpan->end();
}
lets 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/hello
or any other route on our symfony server;
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 symfony server and docker instance are running for this step.
For Jaeger under service, you should see a Hello World Web Server Jaeger
service, go ahead and click find traces to see exported traces.
Once we click on Find Traces
you should be able to see traces like below:
We can click on a trace to get more information about the trace.
For Zipkin, we can visualize our trace by clicking on Run Query
Since resources in Symfony's public\index.php
file are available to the entire application, we can use any of the already instantiated tracers within HelloController
. In addition to the tracers, we can also utilize associated properties, methods and events.
Lets try using the addEvent
method, to capture errors within our controller as follows:
global $zipkinTracer;
if ($zipkinTracer) {
/** @var Span $span */
$span = $zipkinTracer->getActiveSpan();
$span->setAttribute('foo', 'bar');
$span->updateName('New name');
$zipkinTracer->startAndActivateSpan('Child span');
try {
throw new \Exception('Exception Example');
} catch (\Exception $exception) {
$span->setSpanStatus($exception->getCode(), $exception->getMessage());
}
$span->end();
}
In the above snippet we change the span name and attributes for our Zipkin trace, we also add an exception event to the span.
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
With the above example we have been able to instrument a Symfony application using the OpenTelemetry php library. You can fork the example project here. You can also checkout the original test application here.