Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added OTLP/HTTP JSON Exporter #210

Merged
merged 13 commits into from
Nov 10, 2020
163 changes: 163 additions & 0 deletions contrib/Otlp/Exporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Otlp;

use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use Http\Adapter\Guzzle6\Client;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use OpenTelemetry\Sdk\Trace;
use OpenTelemetry\Trace as API;


class Exporter implements Trace\Exporter
{
/**
* @var string
*/
private $endpointURL;

/**
* @var string
*/
private $protocol;

/**
* @var string
*/
private $insecure;

/**
* @var string
*/
private $certificateFile;

/**
* @var array
*/
private $headers;

/**
* @var string
*/
private $compression;

/**
* @var int
*/
private $timeout;
/**
* @var SpanConverter
*/
private $spanConverter;

/**
* @var bool
*/
private $running = true;

/**
* @var ClientInterface
*/

private $client;

/**
* Exporter constructor.
* @param string $serviceName
*/
public function __construct(
$serviceName,
ClientInterface $client=null
)
{

// Set default values based on presence of env variable
$this->endpointURL = getenv("OTEL_EXPORTER_OTLP_ENDPOINT") ?: "localhost:55680";
$this->protocol = getenv("OTEL_EXPORTER_OTLP_PROTOCOL") ?: "json";
$this->insecure = getenv("OTEL_EXPORTER_OTLP_INSECURE") ?: "false";
$this->certificateFile = getenv("OTEL_EXPORTER_OTLP_CERTIFICATE") ?: "none";
$this->headers[] = getenv("OTEL_EXPORTER_OTLP_HEADERS") ?: "none";
$this->compression = getenv("OTEL_EXPORTER_OTLP_COMPRESSION") ?: "none";
$this->timeout =(int)getenv("OTEL_EXPORTER_OTLP_TIMEOUT") ?: 10;


$this->client = $client ?? $this->createDefaultClient();
$this->spanConverter = new SpanConverter($serviceName);
}

/**
* Exports the provided Span data via the OTLP protocol
*
* @param iterable<API\Span> $spans Array of Spans
* @return int return code, defined on the Exporter interface
*/
public function export(iterable $spans): int
{
if (!$this->running) {
return Exporter::FAILED_NOT_RETRYABLE;
}

if (empty($spans)) {
return Trace\Exporter::SUCCESS;
}

$convertedSpans = [];
foreach ($spans as $span) {
array_push($convertedSpans, $this->spanConverter->convert($span));
}

try {
$json = json_encode($convertedSpans);

$this->headers[] = '';

if($this->protocol == "json") {
$headers = ['content-type' => 'application/json', "Content-Encoding" => "gzip"];

}

$request = new Request('POST', $this->endpointURL, $this->headers, $json);
$response = $this->client->sendRequest($request);
} catch (RequestExceptionInterface $e) {
return Trace\Exporter::FAILED_NOT_RETRYABLE;
} catch (NetworkExceptionInterface | ClientExceptionInterface $e) {
return Trace\Exporter::FAILED_RETRYABLE;
}

if ($response->getStatusCode() >= 400 && $response->getStatusCode() < 500) {
return Trace\Exporter::FAILED_NOT_RETRYABLE;
}

if ($response->getStatusCode() >= 500 && $response->getStatusCode() < 600) {
return Trace\Exporter::FAILED_RETRYABLE;
}

return Trace\Exporter::SUCCESS;
}

public function shutdown(): void
{
$this->running = false;
}

protected function createDefaultClient(): ClientInterface
{
$container = [];
$history = Middleware::history($container);
$stack = HandlerStack::create();
// Add the history middleware to the handler stack.
$stack->push($history);

return Client::createWithConfig([
'handler' => $stack,
'timeout' => 30,
]);
}
}
77 changes: 77 additions & 0 deletions contrib/Otlp/SpanConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Otlp;

use OpenTelemetry\Trace\Span;

class SpanConverter
{
/**
* @var string
*/
private $serviceName;

public function __construct(string $serviceName)
{
$this->serviceName = $serviceName;
}

private function sanitiseTagValue($value)
{
// Casting false to string makes an empty string
if (is_bool($value)) {
return $value ? 'true' : 'false';
}

// OTLP tags must be strings, but opentelemetry
// accepts strings, booleans, numbers, and lists of each.
if (is_array($value)) {
return join(',', array_map([$this, 'sanitiseTagValue'], $value));
}

// Floats will lose precision if their string representation
// is >=14 or >=17 digits, depending on PHP settings.
// Can also throw E_RECOVERABLE_ERROR if $value is an object
// without a __toString() method.
// This is possible because OpenTelemetry\Trace\Span does not verify
// setAttribute() $value input.
return (string) $value;
}

public function convert(Span $span)
{
$spanParent = $span->getParent();
$row = [
'id' => $span->getContext()->getSpanId(),
'traceId' => $span->getContext()->getTraceId(),
'parentId' => $spanParent ? $spanParent->getSpanId() : null,
'localEndpoint' => [
'serviceName' => $this->serviceName,
],
'name' => $span->getSpanName(),
'timestamp' => (int) ($span->getStartEpochTimestamp() / 1e3), // RealtimeClock in microseconds
'duration' => (int) (($span->getEnd() - $span->getStart()) / 1e3), // Diff in microseconds
];

foreach ($span->getAttributes() as $k => $v) {
if (!array_key_exists('tags', $row)) {
$row['tags'] = [];
}
$row['tags'][$k] = $this->sanitiseTagValue($v->getValue());
}

foreach ($span->getEvents() as $event) {
if (!array_key_exists('annotations', $row)) {
$row['annotations'] = [];
}
$row['annotations'][] = [
'timestamp' => (int) ($event->getTimestamp() / 1e3), // RealtimeClock in microseconds
'value' => $event->getName(),
];
}

return $row;
}
}
65 changes: 65 additions & 0 deletions examples/AlwaysOnOTLPExample.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';


use OpenTelemetry\Contrib\Otlp\Exporter as OTLPExporter;
use OpenTelemetry\Sdk\Trace\Attributes;
use OpenTelemetry\Sdk\Trace\Clock;
use OpenTelemetry\Sdk\Trace\Sampler\AlwaysOnSampler;
use OpenTelemetry\Sdk\Trace\SamplingResult;
use OpenTelemetry\Sdk\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\Sdk\Trace\TracerProvider;
use OpenTelemetry\Trace as API;

$sampler = new AlwaysOnSampler();
$samplingResult = $sampler->shouldSample(
null,
md5((string) microtime(true)),
substr(md5((string) microtime(true)), 16),
'io.opentelemetry.example',
API\SpanKind::KIND_INTERNAL
);
$Exporter = new OTLPExporter(
'OTLP Example Service');

if (SamplingResult::RECORD_AND_SAMPLED === $samplingResult->getDecision()) {
echo 'Starting OTLPExample';
$tracer = (new TracerProvider())
->addSpanProcessor(new SimpleSpanProcessor($Exporter))
->getTracer('io.opentelemetry.contrib.php');


for ($i = 0; $i < 5; $i++) {
// start a span, register some events
$timestamp = Clock::get()->timestamp();
$span = $tracer->startAndActivateSpan('session.generate.span.' . microtime(true));

$spanParent = $span->getParent();
echo sprintf(
PHP_EOL . 'Exporting Trace: %s, Parent: %s, Span: %s',
$span->getContext()->getTraceId(),
$spanParent ? $spanParent->getSpanId() : 'None',
$span->getContext()->getSpanId()
);

$span->setAttribute('remote_ip', '1.2.3.4')
->setAttribute('country', 'USA');

$span->addEvent('found_login' . $i, $timestamp, new Attributes([
'id' => $i,
'username' => 'otuser' . $i,
]));
$span->addEvent('generated_session', $timestamp, new Attributes([
'id' => md5((string) microtime(true)),
]));

$tracer->endActiveSpan();
}
echo PHP_EOL . 'OTLPExample complete! ';
} else {
echo PHP_EOL . 'OTLPExample tracing is not enabled';
}

echo PHP_EOL;
Loading