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

Fix W3C test service #686

Merged
merged 24 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
09d0d5e
Fix W3C test service (1)
yuktea May 26, 2022
e5c8867
Fix codestyle
yuktea May 26, 2022
0eeada3
Fix symfony-setup script for W3C test service
yuktea May 27, 2022
8e92442
Fix issues in W3C service controller.
yuktea May 28, 2022
7ae1646
Fix context generation
yuktea May 28, 2022
dff26c7
Fix syntax wart in the symfony-setup script
yuktea May 28, 2022
4cfc68c
fix: Inject context from request into response headers
yuktea May 31, 2022
562b597
Update API usage
yuktea Jun 1, 2022
77eb918
chore: clean up controller
yuktea Jun 1, 2022
5aaea25
chore: Improve error reporting in setup script.
yuktea Jun 1, 2022
32f9ed9
fix: sanitize server concats for the tracestate header
yuktea Jun 2, 2022
3d59965
fix: Created span propagation
yuktea Jun 2, 2022
30b9fa5
fix: drop tracestate on invalid members
yuktea Jun 3, 2022
5749582
fix: Add tolerance for future traceparent versions
yuktea Jun 3, 2022
8b0bf8a
fix: enhance version validation regex
yuktea Jun 3, 2022
9f4ff77
test: update unit tests
yuktea Jun 3, 2022
afb5b29
refactor: trace version validation
yuktea Jun 3, 2022
deb13c7
test: add unit tests for trace version validation
yuktea Jun 3, 2022
8db1644
test: another trace validation unit test
yuktea Jun 3, 2022
ad98cdf
Apply cleanup suggestions
yuktea Jun 9, 2022
89e2de5
Remove unused import
yuktea Jun 9, 2022
3fc6908
fix codestyle
yuktea Jun 9, 2022
5139f91
Extract header sanitization to propagation getter decorator
Nevay Jun 10, 2022
6796211
Extract header sanitization logic to propagation-getter decorator
yuktea Jun 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions src/API/Trace/Propagation/TraceContextPropagator.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,32 @@ private static function extractImpl($carrier, PropagationGetterInterface $getter
return SpanContext::getInvalid();
}

// Traceparent = {version}-{trace-id}-{parent-id}-{trace-flags}
// traceParent = {version}-{trace-id}-{parent-id}-{trace-flags}
$pieces = explode('-', $traceparent);

// Unable to extract traceparent. Expected 4 values
if (count($pieces) !== 4) {
// If the header does not have at least 4 pieces, it is invalid -- restart the trace.
if (count($pieces) < 4) {
return SpanContext::getInvalid();
}

[$version, $traceId, $spanId, $traceFlags] = $pieces;

// Validates the version, traceId, spanId and traceFlags
// Returns an invalid spanContext if any of the checks fail
if ($version !== self::VERSION || !SpanContext::isValidTraceId($traceId) ||
!SpanContext::isValidSpanId($spanId) || !SpanContext::isValidTraceFlag($traceFlags)) {
/**
* Return invalid if:
* - Version is invalid (not 2 char hex or 'ff')
* - Trace version, trace ID, span ID or trace flag are invalid
*/
if (!SpanContext::isValidTraceVersion($version)
|| !SpanContext::isValidTraceId($traceId)
|| !SpanContext::isValidSpanId($spanId)
|| !SpanContext::isValidTraceFlag($traceFlags)
) {
return SpanContext::getInvalid();
}

// Return invalid if the trace version is not a future version but still has > 4 pieces.
$versionIsFuture = hexdec($version) > hexdec(self::VERSION);
if (count($pieces) > 4 && !$versionIsFuture) {
return SpanContext::getInvalid();
}

Expand Down
10 changes: 10 additions & 0 deletions src/API/Trace/SpanContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
final class SpanContext implements API\SpanContextInterface
{
public const INVALID_TRACE = '00000000000000000000000000000000';
public const TRACE_VERSION_REGEX = '/^(?!ff)[\da-f]{2}$/';
public const VALID_TRACE = '/^[0-9a-f]{32}$/';
public const TRACE_LENGTH = 32;
public const INVALID_SPAN = '0000000000000000';
Expand Down Expand Up @@ -56,6 +57,15 @@ public static function getInvalid(): API\SpanContextInterface
return self::$invalidContext;
}

/**
* @param string $traceVersion
* @return bool Returns a value that indicates whether a trace version is valid.
*/
public static function isValidTraceVersion(string $traceVersion): bool
{
return 1 === preg_match(self::TRACE_VERSION_REGEX, $traceVersion);
}

/**
* @return bool Returns a value that indicates whether a trace id is valid
*/
Expand Down
20 changes: 15 additions & 5 deletions src/API/Trace/TraceState.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use function array_reverse;
use function array_walk;
use function implode;
use function strlen;

class TraceState implements TraceStateInterface
{
Expand Down Expand Up @@ -134,16 +135,16 @@ private function parse(string $rawTracestate): array
{
$parsedTracestate = [];

if (\strlen($rawTracestate) <= self::MAX_TRACESTATE_LENGTH) {
if (strlen($rawTracestate) <= self::MAX_TRACESTATE_LENGTH) {
$listMembers = explode(self::LIST_MEMBERS_SEPARATOR, $rawTracestate);

// Discard tracestate if entries exceed max length.
if (count($listMembers) > self::MAX_TRACESTATE_LIST_MEMBERS) {

// Truncate the tracestate if it exceeds the maximum list-members allowed
// TODO: Log a message when truncation occurs
$listMembers = array_slice($listMembers, 0, self::MAX_TRACESTATE_LIST_MEMBERS);
// TODO: Log a message when we discard.
return [];
}

$invalid = false;
foreach ($listMembers as $listMember) {
$vendor = explode(self::LIST_MEMBER_KEY_VALUE_SPLITTER, trim($listMember));

Expand All @@ -153,8 +154,17 @@ private function parse(string $rawTracestate): array
// TODO: Log if we can't validate the key and value
if ($this->validateKey($vendor[0]) && $this->validateValue($vendor[1])) {
$parsedTracestate[$vendor[0]] = $vendor[1];

continue;
}
}
$invalid = true;

break;
}
// Discard tracestate if any member is invalid.
if ($invalid) {
return [];
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Context\Propagation;

use function preg_replace;

/**
* Some servers concatenate multiple headers with ';' -- we need to replace these with ','
* This is still a workaround and doesn't get around the problem fully, specifically it doesn't
* handle edge cases where the header has a trailing ';' or an empty trace state.
* We also need to trim trailing separators from the header, found when a header is empty.
*/
final class SanitizeCombinedHeadersPropagationGetter implements PropagationGetterInterface
{
private const LIST_MEMBERS_SEPARATOR = ',';
private const SERVER_CONCAT_HEADERS_REGEX = '/;(?=[^,=;]*=|$)/';
private const TRAILING_LEADING_SEPARATOR_REGEX = '/^' . self::LIST_MEMBERS_SEPARATOR . '+|' . self::LIST_MEMBERS_SEPARATOR . '+$/';

private PropagationGetterInterface $getter;

public function __construct(PropagationGetterInterface $getter)
{
$this->getter = $getter;
}

public function keys($carrier): array
{
return $this->getter->keys($carrier);
}

public function get($carrier, string $key): ?string
{
$value = $this->getter->get($carrier, $key);
if ($value === null) {
return null;
}

return preg_replace(
[self::SERVER_CONCAT_HEADERS_REGEX, self::TRAILING_LEADING_SEPARATOR_REGEX],
[self::LIST_MEMBERS_SEPARATOR],
$value,
);
}
}
57 changes: 37 additions & 20 deletions tests/TraceContext/W3CTestService/TestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

namespace App\Controller;

use GuzzleHttp\Client;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\API\Trace\SpanContext;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\Propagation\ArrayAccessGetterSetter;
use OpenTelemetry\Context\Propagation\SanitizeCombinedHeadersPropagationGetter;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Expand All @@ -15,39 +18,53 @@ class TestController
{
/**
* @Route("/test")
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
public function index(Request $request): Response
{
global $tracer;
if (!$tracer) {
return new Response('internal error', Response::HTTP_INTERNAL_SERVER_ERROR);
}

$traceCtxPropagator = TraceContextPropagator::getInstance();
$propagationGetter = new SanitizeCombinedHeadersPropagationGetter(new ArrayAccessGetterSetter());

try {
$context = $traceCtxPropagator->extract($request->headers->all(), $propagationGetter);
} catch (\InvalidArgumentException $th) {
$context = new Context();
}

$array = $request->request->all();
$body = json_decode($request->getContent(), true);
if (!$body) {
return new Response('Invalid JSON', Response::HTTP_BAD_REQUEST);
}

foreach ($body as $case) {
if ($tracer) {
$headers = ['content-type' => 'application/json'];
$url = $case['url'];
$arguments = $case['arguments'];
$headers = ['content-type' => 'application/json'];
$url = $case['url'];
$arguments = $case['arguments'];

try {
$context = TraceContextPropagator::extract($request->headers->all());
} catch (\InvalidArgumentException $th) {
$context = SpanContext::generate();
}
$span = $tracer->spanBuilder($url)->setParent($context)->startSpan();
$context = $span->storeInContext($context);
$scope = $context->activate();

$span = $tracer->startAndActivateSpanFromContext($url, $context, true);
TraceContextPropagator::inject($carrier, null, $context);
$traceCtxPropagator->inject($headers, null, $context);

$client = new Client([
$client = new Psr18Client(HttpClient::create(
[
'base_uri' => $url,
'timeout' => 2.0,
]);
]
));

$testServiceRequest = new \Nyholm\Psr7\Request('POST', $url, $headers, json_encode($arguments));

$testServiceRequest = new \GuzzleHttp\Psr7\Request('POST', $url, $headers, json_encode($arguments));
$response = $client->sendRequest($testServiceRequest);
$client->sendRequest($testServiceRequest);

$span->end();
}
$span->end();
$scope->detach();
}

return new Response(
Expand Down
45 changes: 18 additions & 27 deletions tests/TraceContext/W3CTestService/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,37 @@
require_once __DIR__ . '/../vendor/autoload.php';

use App\Kernel;
use OpenTelemetry\API\Trace as API;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Contrib\Jaeger\Exporter as JaegerExporter;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
use OpenTelemetry\SDK\Trace\SamplingResult;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Component\HttpFoundation\Request;

(new Dotenv())->bootEnv(dirname(__DIR__) . '/.env');

$sampler = new AlwaysOnSampler();
$samplingResult = $sampler->shouldSample(
Context::getCurrent(),
md5((string) microtime(true)),
'io.opentelemetry.example',
API\SpanKind::KIND_INTERNAL
);
$httpClient = new Psr18Client();

$exporter = new JaegerExporter(
'W3C Trace-Context Test Service',
'http://localhost:9412/api/v2/spans'
);
$tracer = (new TracerProvider([
new BatchSpanProcessor(
new JaegerExporter(
'W3C Trace-Context Test Service',
'http://localhost:9412/api/v2/spans',
$httpClient,
$httpClient,
$httpClient,
)
),
]))->getTracer('W3C Trace-Context Test Service');

if (SamplingResult::RECORD_AND_SAMPLE === $samplingResult->getDecision()) {
$tracer = (new TracerProvider())
->addSpanProcessor(new BatchSpanProcessor($exporter, ClockFactory::getDefault()))
->getTracer('io.opentelemetry.contrib.php');

$request = Request::createFromGlobals();
$span = $tracer->startAndActivateSpan($request->getUri());
}
$request = Request::createFromGlobals();
$span = $tracer->spanBuilder($request->getUri())->startSpan();
$spanScope = $span->activate();

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

if (SamplingResult::RECORD_AND_SAMPLED === $samplingResult->getDecision()) {
$span->end();
}
$span->end();
$spanScope->detach();
15 changes: 9 additions & 6 deletions tests/TraceContext/W3CTestService/symfony-setup
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,28 @@ curl -sS https://get.symfony.com/cli/installer | bash
mv /root/.symfony/bin/symfony /usr/local/bin/symfony

# Set up Symfony test application
cd tests/TraceContext/W3CTestService
cd tests/TraceContext/W3CTestService || { printf "Something went wrong: could not cd into W3CTestService directory."; exit 1; }
rm -rf test_app
symfony new test_app --version=5.2

# Move our files into the test application
cp index.php test_app/public/index.php
cp TestController.php test_app/src/Controller/TestController.php
cd test_app

composer require annotations
cd test_app || { printf "Something went wrong: could not cd into test_app."; exit 1; }

# Link the local opentelemetry changes to vendor/open-telemetry
composer require open-telemetry/opentelemetry "*"
composer config repositories.local \
'{"type": "path", "url": "/usr/src/open-telemetry/", "options": {"symLink": true }}' \
--file composer.json
composer config minimum-stability dev --file composer.json
composer require php-http/mock-client "*"

composer require symfony/http-client
composer require annotations
composer require nyholm/psr7 "*"
composer require open-telemetry/opentelemetry "*"
composer require php-http/mock-client "*"

rm composer.lock
composer clearcache
composer install
Expand All @@ -41,7 +44,7 @@ cd ..
rm -rf trace-context
git clone https://github.com/w3c/trace-context.git

cd trace-context/test
cd trace-context/test || { printf "Failed to cd to trace-context/test" ; exit; }

# Run the test
python3 test.py http://127.0.0.1:8001/test
Loading