Skip to content

Commit

Permalink
Implement GCP X-Cloud-Trace-Context Propagator (#1132)
Browse files Browse the repository at this point in the history
  • Loading branch information
ynikitin-etsy authored Nov 13, 2023
0 parents commit 7864488
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 0 deletions.
27 changes: 27 additions & 0 deletions 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 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 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 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 _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 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"
}
}
}

0 comments on commit 7864488

Please sign in to comment.