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

Implement GCP X-Cloud-Trace-Context Propagator #1132

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitsplit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ splits:
target: "https://${GH_TOKEN}@github.com/opentelemetry-php/exporter-zipkin.git"
- prefix: "src/Extension/Propagator/B3"
target: "https://${GH_TOKEN}@github.com/opentelemetry-php/extension-propagator-b3.git"
- prefix: "src/Extension/Propagator/XCloudTrace"
target: "https://${GH_TOKEN}@github.com/opentelemetry-php/extension-propagator-cloudtrace.git"

# List of references to split (defined as regexp)
origins:
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"src/Contrib/Grpc/_register.php",
"src/Contrib/Zipkin/_register.php",
"src/Extension/Propagator/B3/_register.php",
"src/Extension/Propagator/XCloudTrace/_register.php",
"src/SDK/Logs/Exporter/_register.php",
"src/SDK/Metrics/MetricExporter/_register.php",
"src/SDK/Propagation/_register.php",
Expand Down
27 changes: 27 additions & 0 deletions src/Extension/Propagator/XCloudTrace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/extension-propagator-cloudtrace/releases)
[![Source](https://img.shields.io/badge/source-extension--propagator--xcloudtrace-green)](https://github.com/open-telemetry/opentelemetry-php/tree/main/src/Extension/Propagator/XCloudTrace)
[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php:extension--propagator--xcloudtrace-blue)](https://github.com/opentelemetry-php/extension-propagator-cloudtrace)
[![Latest Version](http://poser.pugx.org/open-telemetry/extension-propagator-cloudtrace/v/unstable)](https://packagist.org/packages/open-telemetry/extension-propagator-cloudtrace/)
[![Stable](http://poser.pugx.org/open-telemetry/extension-propagator-cloudtrace/v/stable)](https://packagist.org/packages/open-telemetry/extension-propagator-cloudtrace/)

# OpenTelemetry Extension
### XCloudTrace Propagator

XCloudTrace is a propagator that supports the specification for the header "x-cloud-trace-context" used for trace context propagation across
service boundaries. (https://cloud.google.com/trace/docs/setup#force-trace). OpenTelemetry PHP XCloudTrace Propagator Extension provides
option to use it bi-directionally or one-way. One-way does not inject the header for downstream consumption, it only processes the incoming headers
and returns the correct span context. It only attaches to existing X-Cloud-Trace-Context traces and does not create downstream ones.
For one-way XCloudTrace:
```text
XCloudTracePropagator::getOneWayInstance()
```

For bi-directional XCloudTrace:
```text
XCloudTracePropagator::getInstance()
```

## Contributing

This repository is a read-only git subtree split.
To contribute, please see the main [OpenTelemetry PHP monorepo](https://github.com/open-telemetry/opentelemetry-php).
123 changes: 123 additions & 0 deletions src/Extension/Propagator/XCloudTrace/Utils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Extension\Propagator\XCloudTrace;

/**
* This class contains utilities that are used by the XCloudTracePropagator.
* This class mostly contains numerical handling functions to work with
* trace and span IDs.
*/
final class Utils
{

/**
* Pads the string with zero string characters on left hand side, to max total string size.
*
* @param string $str The string to pad.
* @param int $amount Total String size, default is 16.
* @return string The padded string
*/
public static function leftZeroPad(string $str, int $amount = 16) : string
{
return str_pad($str, $amount, '0', STR_PAD_LEFT);
}

/**
* Converts a decimal number in string format to a hex number in string format.
* The returned number will not start with 0x.
*
* @param string $num The number to convert.
* @return string The converted number.
*/
public static function decToHex(string $num) : string
{
$int = (int) $num;
if (self::isBigNum($int)) {
return self::baseConvert($num, 10, 16);
}

return dechex($int);
}

/**
* Converts a hex number in string format to a decimal number in string format.
* The given number does not have to start with 0x.
*
* @param string $num The number to convert.
* @return string The converted number.
*/
public static function hexToDec(string $num) : string
{
$dec = hexdec($num);
if (self::isBigNum($dec)) {
return self::baseConvert($num, 16, 10);
}

return (string) $dec;
}

/**
* Tests whether the given number is larger than the maximum integer of the installed PHP's build.
* On 32-bit system it's 2147483647 and on 64-bit it's 9223372036854775807.
* We are comparing with >= and no >, because this function is used in context of what method to use
* to convert to some base (in our case hex to octal and vice versa).
* So it's ok if we use >=, because it means that only for MAX_INT we will use the slower baseConvert
* method.
*
* @param int|float $number The number to test.
* @return bool Whether it was bigger or not than the max.
*/
public static function isBigNum($number) : bool
{
return $number >= PHP_INT_MAX;
}

/**
* Custom function to convert a number in string format from one base to another.
* Built-in functions, specifically for hex, do not work well in PHP under
* all versions (32/64-bit) or if the number only fits into an unsigned long.
* PHP does not have unsigned longs, so this function is necessary.
*
* @param string $num The number to convert (in some base).
* @param int $fromBase The base to convert from.
* @param int $toBase The base to convert to.
* @return string Converted number in the new base.
*/
public static function baseConvert(string $num, int $fromBase, int $toBase) : string
{
$num = strtolower($num);
$chars = '0123456789abcdefghijklmnopqrstuvwxyz';
$newstring = substr($chars, 0, $toBase);

$length = strlen($num);
$result = '';

$number = [];
for ($i = 0; $i < $length; $i++) {
$number[$i] = strpos($chars, $num[$i]);
}

do {
$divide = 0;
$newlen = 0;
for ($i = 0; $i < $length; $i++) {
if (!isset($number[$i]) || $number[$i] === false) {
return '';
}
$divide = $divide * $fromBase + $number[$i];
if ($divide >= $toBase) {
$number[$newlen++] = (int) ($divide / $toBase);
$divide %= $toBase;
} elseif ($newlen > 0) {
$number[$newlen++] = 0;
}
}
$length = $newlen;
$result = $newstring[$divide] . $result;
} while ($newlen != 0);

return $result;
}
}
63 changes: 63 additions & 0 deletions src/Extension/Propagator/XCloudTrace/XCloudTraceFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Extension\Propagator\XCloudTrace;

use OpenTelemetry\API\Trace\SpanContext;
use OpenTelemetry\API\Trace\SpanContextInterface;

/**
* This format using a human readable string encoding to propagate SpanContext.
* The current format of the header is `<trace-id>[/<span-id>][;o=<options>]`.
* The options are a bitmask of options. Currently the only option is the
* least significant bit which signals whether the request was traced or not
* (1 = traced, 0 = not traced).
*/
final class XCloudTraceFormatter
{
const CONTEXT_HEADER_FORMAT = '/([0-9a-fA-F]{32})(?:\/(\d+))?(?:;o=(\d+))?/';

/**
* Generate a SpanContext object from the Trace Context header
*
* @param string $header
* @return SpanContextInterface
*/
public static function deserialize(string $header) : SpanContextInterface
{
$matched = preg_match(self::CONTEXT_HEADER_FORMAT, $header, $matches);

if (!$matched) {
return SpanContext::getInvalid();
}
if (!array_key_exists(2, $matches) || empty($matches[2])) {
return SpanContext::getInvalid();
}
if (!array_key_exists(3, $matches)) {
return SpanContext::getInvalid();
}

return SpanContext::createFromRemoteParent(
strtolower($matches[1]),
Utils::leftZeroPad(Utils::decToHex($matches[2])),
(int) ($matches[3] == '1')
);
}

/**
* Convert a SpanContextInterface to header string
*
* @param SpanContextInterface $context
* @return string
*/
public static function serialize(SpanContextInterface $context) : string
{
$ret = $context->getTraceId();
if ($context->getSpanId()) {
$ret .= '/' . Utils::hexToDec($context->getSpanId());
}

return $ret . (';o=' . ($context->isSampled() ? '1' : '0'));
}
}
99 changes: 99 additions & 0 deletions src/Extension/Propagator/XCloudTrace/XCloudTracePropagator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Extension\Propagator\XCloudTrace;

use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\Context\Propagation\ArrayAccessGetterSetter;
use OpenTelemetry\Context\Propagation\PropagationGetterInterface;
use OpenTelemetry\Context\Propagation\PropagationSetterInterface;
use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface;

/**
* XCloudTracePropagator is a propagator that supports the specification for the X-Cloud-Trace-Context
* header used for trace context propagation across service boundaries.
* (https://cloud.google.com/trace/docs/setup#force-trace)
*/
final class XCloudTracePropagator implements TextMapPropagatorInterface
{
private static ?TextMapPropagatorInterface $oneWayInstance = null;
private static ?TextMapPropagatorInterface $instance = null;

public static function getOneWayInstance(): TextMapPropagatorInterface
{
if (self::$oneWayInstance === null) {
self::$oneWayInstance = new XCloudTracePropagator(true);
}

return self::$oneWayInstance;
}

public static function getInstance(): TextMapPropagatorInterface
{
if (self::$instance === null) {
self::$instance = new XCloudTracePropagator(false);
}

return self::$instance;
}

private const XCLOUD = 'x-cloud-trace-context';

private const FIELDS = [
self::XCLOUD,
];

private bool $oneWay;

private function __construct(bool $oneWay)
{
$this->oneWay = $oneWay;
}

/** {@inheritdoc} */
public function fields(): array
{
return self::FIELDS;
}

/** {@inheritdoc} */
public function inject(&$carrier, PropagationSetterInterface $setter = null, ContextInterface $context = null): void
{
if ($this->oneWay) {
return;
}

$setter ??= ArrayAccessGetterSetter::getInstance();
$context ??= Context::getCurrent();
$spanContext = Span::fromContext($context)->getContext();

if (!$spanContext->isValid()) {
return;
}

$headerValue = XCloudTraceFormatter::serialize($spanContext);
$setter->set($carrier, self::XCLOUD, $headerValue);
}

/** {@inheritdoc} */
public function extract($carrier, PropagationGetterInterface $getter = null, ContextInterface $context = null): ContextInterface
{
$getter ??= ArrayAccessGetterSetter::getInstance();
$context ??= Context::getCurrent();

$headerValue = $getter->get($carrier, self::XCLOUD);
if ($headerValue === null) {
return $context;
}

$spanContext = XCloudTraceFormatter::deserialize($headerValue);
if (!$spanContext->isValid()) {
return $context;
}

return $context->withContextValue(Span::wrap($spanContext));
}
}
12 changes: 12 additions & 0 deletions src/Extension/Propagator/XCloudTrace/_register.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

use OpenTelemetry\Extension\Propagator\XCloudTrace\XCloudTracePropagator;
use OpenTelemetry\SDK\Common\Configuration\KnownValues;
use OpenTelemetry\SDK\Registry;

Registry::registerTextMapPropagator(
KnownValues::VALUE_XCLOUD_TRACE,
XCloudTracePropagator::getInstance()
);
37 changes: 37 additions & 0 deletions src/Extension/Propagator/XCloudTrace/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "open-telemetry/extension-propagator-xcloudtrace",
"description": "XCloudTraceContext propagator extension for OpenTelemetry PHP.",
"keywords": ["opentelemetry", "otel", "tracing", "apm", "extension", "propagator", "xcloudtrace"],
"type": "library",
"support": {
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php",
"docs": "https://opentelemetry.io/docs/php",
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V"
},
"license": "Apache-2.0",
"authors": [
{
"name": "opentelemetry-php contributors",
"homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors"
}
],
"require": {
"php": "^7.4 || ^8.0",
"open-telemetry/api": "^1.0",
"open-telemetry/context": "^1.0"
},
"autoload": {
"psr-4": {
"OpenTelemetry\\Extension\\Propagator\\XCloudTrace\\": "."
},
"files": [
"_register.php"
]
},
"extra": {
"branch-alias": {
"dev-main": "1.0.x-dev"
}
}
}
2 changes: 2 additions & 0 deletions src/SDK/Common/Configuration/KnownValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface KnownValues
public const VALUE_BAGGAGE = 'baggage';
public const VALUE_B3 = 'b3';
public const VALUE_B3_MULTI = 'b3multi';
public const VALUE_XCLOUD_TRACE = 'xcloudtrace';
public const VALUE_XRAY = 'xray';
public const VALUE_OTTRACE = 'ottrace';
public const VALUE_ALWAYS_ON = 'always_on';
Expand Down Expand Up @@ -105,6 +106,7 @@ interface KnownValues
self::VALUE_BAGGAGE, // W3C Baggage
self::VALUE_B3, // B3 Single
self::VALUE_B3_MULTI, // B3 Multi
self::VALUE_XCLOUD_TRACE, // GCP XCloudTraceContext
self::VALUE_XRAY, // AWS X-Ray (third party)
self::VALUE_OTTRACE, // OT Trace (third party)
self::VALUE_NONE, // No automatically configured propagator.
Expand Down
Loading