Search code examples
composer-phpopen-telemetryobservabilityopen-telemetry-collectorotel

Dependencies do not show with OpenTelemetry automatic instrumentation for PHP


I have a very simple stack of PHP + Otel auto instrumetation + Elastic APM + Prometheus.

My stack gets tracing and metrics information from my php app. My tracings are forwarded to the elastic cloud, where I use APM. My metrics go to Prometheus.

I see that my tracing is partially working as I can't see the dependencies. Which in this case would be my external api.

enter image description here enter image description here

The base of my stack: (I didn't attach the php.ini and .htaccess files as I don't think it's relevant.)

index.php

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use DI\Container;
use GuzzleHttp\Client;

require __DIR__ . '/vendor/autoload.php';

$container = new Container();

AppFactory::setContainer($container);
$app = AppFactory::create();

$app->get('/', function (Request $request, Response $response) {
    $response->getBody()->write('<h3>RANDOM API</h3>
    <p>O que você gostaria de ver?</p>
    <ul>
        <li>Random dogs? <a href="/dogs">Clique</li>
    </ul>');

    return $response;
});

$app->get('/healthcheck', function (Request $request, Response $response) {
    return $response->withStatus(204);
});

$app->get('/dogs', function (Request $request, Response $response) {
    $client = new Client();

    try {
        $apiResponse = $client->get('https://dog.ceo/api/breeds/image/random');
        $dogData = json_decode($apiResponse->getBody());

        $response->getBody()->write('<img src="' . $dogData->message . '" alt="random dog" style="max-width: 500px"/>');

    } catch (\Exception $e) {
        $response->getBody()->write($e->getMessage());
        return $response->withStatus(500);
    }

    return $response;
});

$app->run();

Dockerfile

FROM php:8.2

RUN set -xe; \
    apt-get update; \
    apt-get -y install g++ zlib1g-dev build-essential zlib1g-dev gcc make autoconf m4 perl git; \
    curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \
    pecl install grpc && docker-php-ext-enable grpc; \
    pecl install opentelemetry-beta && docker-php-ext-enable opentelemetry

ENV COMPOSER_ALLOW_SUPERUSER 1
ENV OTEL_PHP_AUTOLOAD_ENABLED=true
ENV OTEL_SERVICE_NAME=your-service-name
ENV OTEL_TRACES_EXPORTER=console
ENV OTEL_METRICS_EXPORTER=otlp
ENV OTEL_EXPORTER_OTLP_PROTOCOL=grpc
ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317
ENV OTEL_PROPAGATORS=baggage,tracecontext

WORKDIR /usr/src/myapp

COPY composer.json composer.lock* /usr/src/myapp/

RUN composer install --no-dev --no-scripts --no-autoloader

COPY . /usr/src/myapp

RUN composer dump-autoload --optimize --classmap-authoritative; \
    composer require slim/slim:^4 slim/psr7:^1; \
    composer require open-telemetry/exporter-otlp php-http/guzzle7-adapter; \
    composer require open-telemetry/transport-grpc; \
    composer config allow-plugins.php-http/discovery false; \
    composer require open-telemetry/sdk open-telemetry/opentelemetry-auto-slim; \
    php --ri opentelemetry

COPY php.ini /usr/local/etc/php/php.ini

CMD [ "php", "-S", "0.0.0.0:8080" ]

docker-compose.yml

services:
  otel_collector:
    networks:
      - backend
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - "./otel-collector-config.yml:/etc/otelcol/otel-collector-config.yml"
    command: --config /etc/otelcol/otel-collector-config.yml
    ports:
      - "14278:14278"
      - "65535:65535"
      - "55677:55677"
      - "12345:12345"
      - "4318:4318"
      - "4317:4317"    
      - "8889:8889"  
  prometheus:
    networks:
      - backend
    image: prom/prometheus:latest
    volumes:
      - "./prometheus.yml:/etc/prometheus/prometheus.yml"
    ports:
      - "9090:9090"

  app-php:
    build: ./php
    image: app-php
    ports:
      - 8083:8080
    container_name: app-php
    environment:
      OTEL_EXPORTER_OTLP_ENDPOINT: http://otel_collector:4317
      OTEL_SERVICE_NAME: app-php
      OTEL_TRACES_EXPORTER: otlp
    networks:
      - backend
  
networks:
  backend:

composer.json

{
    "require": {
        "slim/slim": "4.*",
        "php-di/php-di": "^6.3",
        "guzzlehttp/guzzle": "^7.3",
        "open-telemetry/api": "^1.0.0",
        "open-telemetry/sdk": "^1.0.0"
    },
    "minimum-stability": "dev"
}

otel-collector-config.yml

receivers:
  otlp:
    protocols:
      grpc: {}
      http: {}

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"


  logging:
    verbosity: detailed
   

  otlp/elastic: 
    endpoint: "xxx-xxx-xxx"  
    headers:
      Authorization: "xxx-xxx-xxx"

 
processors:
  batch: {}

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus, logging]
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/elastic, logging]

prometheus.yml

global:
  scrape_interval:     15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: aggregated-trace-metrics
    static_configs:
    - targets: ['otel_collector:8889']

Solution

  • It looks like your routes for the Slim Framework application are being traced and are visible in elastic, but you are missing spans for the http calls to https://dog.ceo

    The slim framework auto-instrumentation does not instrument outgoing http calls. If the $client that you are using is a PSR-18 http client, then it can be auto-instrumented by installing the open-telemetry/opentelemetry-auto-psr18 package. Do note that $client->get() is not part of the PSR-18 spec, only sendRequest() is. If the http client you are using proxies get() through to sendRequest(), then it should work. Otherwise, create the request and send it using the PSR-18 method:

    $request = new Request('GET', 'https://dog.ceo/api/breeds/image/random');
    $client->sendRequest($request);