Last active
January 4, 2024 08:53
-
-
Save sc0ttdav3y/0d320d08a726dd0ed204a47bd8ebb78b to your computer and use it in GitHub Desktop.
OpenTelemetry + Bref + PHP + Lambda + Serverless
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use OpenTelemetry\API\Common\Signal\Signals; | |
use OpenTelemetry\API\Trace\SpanInterface; | |
use OpenTelemetry\API\Trace\SpanKind; | |
use OpenTelemetry\API\Trace\StatusCode; | |
use OpenTelemetry\API\Trace\TracerInterface; | |
use OpenTelemetry\Aws; | |
use OpenTelemetry\Aws\AwsSdkInstrumentation; | |
use OpenTelemetry\Aws\Lambda\Detector; | |
use OpenTelemetry\Context\ContextInterface; | |
use OpenTelemetry\Context\ScopeInterface; | |
use OpenTelemetry\Contrib\Grpc\GrpcTransportFactory; | |
use OpenTelemetry\Contrib\Otlp\OtlpUtil; | |
use OpenTelemetry\SDK\Common\Log\LoggerHolder; | |
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor; | |
use OpenTelemetry\SDK\Trace\TracerProvider; | |
/** | |
* OpenTelemetry wrapper to simplify AWS X-Ray tracing in Bref | |
* | |
* To use: | |
* | |
* composer require open-telemetry/contrib-aws:0.0.17 | |
* composer require open-telemetry/opentelemetry:0.0.17 | |
* composer require open-telemetry/opentelemetry-php-contrib:0.0.17 | |
* | |
* Set-up: | |
* | |
* // set things up in Bref handler | |
* OpenTelemetry::setTraceId($traceId); // get this from Bref's Context::getTraceId() method | |
* OpenTelemetry::nameRootSpan($rootName); // call this something useful, e.g. your route or function name | |
* | |
* // Initialise the stack when ready | |
* OpenTelemetry:init(); | |
* | |
* Instrumenting your code: | |
* | |
* // In important code, make a span to wrap the code like this... | |
* OpenTelemetry::startSpan(__METHOD__); | |
* try { | |
* // do stuff | |
* } catch { | |
* OpenTelemetry::recordException($e->getMessage, $e); | |
* } finally { | |
* OpenTelemetry::endSpan(); | |
* } | |
* | |
* Lambda Layers: | |
* | |
* Use the 'Go' supported Lambda layer for manual instrumentation. At time of writing, the layer is called this: | |
* `arn:aws:lambda:${aws:region}:901920570463:layer:aws-otel-collector-amd64-ver-0-68-0:1` | |
* | |
* Official resources that helped me: | |
* | |
* @see https://opentelemetry.io/docs/instrumentation/php/getting-started/ | |
* @see https://aws-otel.github.io/docs/getting-started/go-sdk/trace-manual-instr | |
* @see https://aws-otel.github.io/docs/getting-started/lambda | |
* | |
* This has been built from collaboration across a few GitHub projects: | |
* | |
* @see https://github.com/aws-observability/aws-otel-php/issues/4 | |
* @see https://github.com/open-telemetry/opentelemetry-php/issues/906 | |
* @see https://github.com/brefphp/bref/issues/921 | |
*/ | |
class OpenTelemetry | |
{ | |
/** | |
* True once init() has set up the class | |
* @var bool | |
*/ | |
private static bool $hasInitialised = false; | |
/** | |
* the OpenTelemetry tracer | |
* | |
* @var TracerInterface | |
*/ | |
private static TracerInterface $tracer; | |
/** | |
* The root span | |
* | |
* @var SpanInterface|null | |
*/ | |
private static ?SpanInterface $rootSpan = null; | |
/** | |
* The scope from the root span | |
* | |
* @var ScopeInterface | |
*/ | |
private static ScopeInterface $rootScope; | |
/** | |
* A stack of currently active spans | |
* | |
* @var SpanInterface[] | |
*/ | |
private static array $spanStack = []; | |
/** | |
* A stack of currently active scopes (aligns with spans) | |
* | |
* @var ScopeInterface[] | |
*/ | |
private static array $scopeStack = []; | |
/** | |
* An array of common span attributes to be added to every span | |
* | |
* @var array | |
*/ | |
private static array $attributes = []; | |
/** | |
* AWS headers to pass to calls to other services for XRay | |
* | |
* @var array | |
*/ | |
private static array $carrier = []; | |
/** | |
* The ASK SDK instrumation class for propagating into SDK clients | |
* | |
* @var AwsSdkInstrumentation | |
*/ | |
private static AwsSdkInstrumentation $awsSdkInstrumentation; | |
/** | |
* The context | |
* | |
* @var ContextInterface | |
*/ | |
private static ContextInterface $context; | |
/** | |
* The name to use when creating the root span | |
* | |
* @var string | |
*/ | |
private static string $rootSpanName = 'root'; | |
/** | |
* The X-Ray TraceID, if known | |
* | |
* @var string | |
*/ | |
private static string $traceId = ''; | |
/** | |
* Whether it is enabled | |
* | |
* - true will send telemetry | |
* - false will not collect telemetry | |
* | |
* @var bool|null | |
*/ | |
private static ?bool $enabled = false; | |
/** | |
* Initialise at the beginning of Bravo bootstrap | |
* | |
* @return void | |
*/ | |
public static function init() | |
{ | |
if (!self::$hasInitialised) { | |
self::setUp(); | |
register_shutdown_function([__CLASS__, 'tearDownAtShutdown']); | |
self::$hasInitialised = true; | |
} | |
} | |
/** | |
* Sets up OpenTelemetry | |
* | |
* Call this once per PHP invocation | |
* | |
* @return void | |
*/ | |
private static function setUp() | |
{ | |
// Set up to send traces to AWS XRay | |
// @see https://github.com/open-telemetry/opentelemetry-php#set-up-a-tracer | |
// Most examples on the web use the now-deleted OpenTelemetry\Contrib\OtlpGrpc\Exporter, | |
// but that's gone now and there's no real good example for AWS. This is my best guess. | |
$transport = (new GrpcTransportFactory())->create( | |
'http://127.0.0.1:4317' . OtlpUtil::method(Signals::TRACE) | |
); | |
$exporter = new OpenTelemetry\Contrib\Otlp\SpanExporter($transport); | |
$spanProcessor = new SimpleSpanProcessor($exporter); | |
$idGenerator = new Aws\Xray\IdGenerator(); | |
$xrayPropagator = new Aws\Xray\Propagator(); | |
// Use AWS lamda resource detectors | |
$detector = new Detector(); | |
$resource = $detector->getResource(); | |
// Propagate the xray header into a context | |
$headers = []; | |
try { | |
if (self::$traceId) { | |
$xrayHeader = self::$traceId; | |
} elseif (isset($_SERVER['_X_AMZN_TRACE_ID'])) { | |
$xrayHeader = $_SERVER['_X_AMZN_TRACE_ID']; | |
} else { | |
$xrayHeader = Bravo_Controller_Request_PSR15::getPsrRequest() | |
->getHeaderLine(Aws\Xray\Propagator::AWSXRAY_TRACE_ID_HEADER); | |
} | |
$headers = [ | |
Aws\Xray\Propagator::AWSXRAY_TRACE_ID_HEADER => $xrayHeader | |
]; | |
} catch (Exception $e) { | |
} | |
self::$context = $xrayPropagator->extract($headers); | |
$xrayPropagator->inject(self::$carrier, null, self::$context); | |
// set up AwsSdkInstrumentation | |
$tracerProvider = new TracerProvider($spanProcessor, null, $resource, null, $idGenerator); | |
self::$awsSdkInstrumentation = new AwsSdkInstrumentation(); | |
self::$awsSdkInstrumentation->setPropagator($xrayPropagator); | |
self::$awsSdkInstrumentation->setTracerProvider($tracerProvider); | |
self::$awsSdkInstrumentation->activate(); | |
// Get the tracer | |
self::$tracer = self::$awsSdkInstrumentation->getTracer(); | |
// Set up the root span | |
self::doCreateRootSpan(); | |
} | |
/** | |
* Names and creates a new root span, if none exists | |
* | |
* Note that it's possible to name the root by calling this method before running init(), | |
* which will actually do the creation. | |
* | |
* @param string $name | |
* | |
* @return void | |
*/ | |
public static function nameRootSpan(string $name): void | |
{ | |
// set the name for when the root span is created | |
self::$rootSpanName = $name; | |
// Do not allow second root spans; update the name instead | |
if (self::$rootSpan) { | |
self::$rootSpan->updateName($name); | |
return; | |
} | |
// Only run beyond here if initialised and enabled | |
if (!self::$enabled || !self::$hasInitialised) { | |
return; | |
} | |
self::doCreateRootSpan(); | |
} | |
/** | |
* Sets the X-Ray TraceId (e.g. from Bref Context) | |
* | |
* @param string $traceId | |
* | |
* @return void | |
*/ | |
public static function setTraceId(string $traceId): void | |
{ | |
self::$traceId = $traceId; | |
} | |
/** | |
* Returns the current root span name | |
* | |
* @return string | |
*/ | |
public static function getRootSpanName(): string | |
{ | |
return self::$rootSpanName; | |
} | |
/** | |
* Actually creates the root span | |
* | |
* @return void | |
*/ | |
private static function doCreateRootSpan(): void | |
{ | |
// Set the root span and scope, and pass the context | |
self::$rootSpan = self::$tracer | |
->spanBuilder(self::$rootSpanName) | |
->setParent(self::$context) | |
->setSpanKind(SpanKind::KIND_SERVER) | |
->startSpan(); | |
self::$rootScope = self::$rootSpan->activate(); | |
} | |
/** | |
* Enable instrumentation | |
* | |
* @return void | |
*/ | |
public static function enable() | |
{ | |
self::$enabled = true; | |
} | |
/** | |
* Disable instrumentation | |
* | |
* @return void | |
*/ | |
public static function disable() | |
{ | |
self::$enabled = false; | |
} | |
/** | |
* Adds an attribute to all spans (such as userId, etc) | |
* | |
* @param string $key | |
* @param string|int|float|null $val | |
* | |
* @return void | |
*/ | |
public static function addAttribute(string $key, $val) | |
{ | |
if ($val !== null) { | |
self::$attributes[$key] = (string)$val; | |
} else { | |
unset(self::$attributes[$key]); | |
} | |
} | |
/** | |
* Remove an attribute | |
* | |
* @param string $key | |
* | |
* @return void | |
*/ | |
public static function removeAttribute(string $key) | |
{ | |
unset(self::$attributes[$key]); | |
} | |
/** | |
* Tears down OpenTelemetary | |
* | |
* Call this in a shutdown function at the end | |
* @return void | |
*/ | |
public static function tearDownAtShutdown() | |
{ | |
if (!self::$enabled || !self::$hasInitialised) { | |
return; | |
} | |
self::$rootSpan->end(); | |
self::$rootScope->detach(); | |
} | |
/** | |
* Start a span | |
* | |
* Call this at the beginning of an interesting part of the code, and then call endSpan() at the end. | |
* | |
* @param string $classOrMethod Pass __METHOD__ or _CLASS__ based on usefulness | |
* @param string|null $name Additional data to append | |
* | |
* @return int The span stack level | |
*/ | |
public static function startSpan(string $classOrMethod, string $name = null, array $attributes = []): int | |
{ | |
if (!self::$enabled || !self::$hasInitialised) { | |
return 0; | |
} | |
$spanName = $classOrMethod; | |
if ($name) { | |
$spanName .= "/$name"; | |
} | |
$trace = debug_backtrace(); | |
$span = self::$tracer->spanBuilder($spanName)->startSpan(); | |
$span->setAttributes(array_merge(self::$attributes, ['trace' => $trace], $attributes)); | |
$scope = $span->activate(); | |
self::$spanStack[] = $span; | |
self::$scopeStack[] = $scope; | |
return count(self::$spanStack); | |
} | |
/** | |
* Ends the current span | |
* | |
* Tip: use try...finally to ensure endSpan() always fires | |
* | |
* @return int The span stack level | |
*/ | |
public static function endSpan(): int | |
{ | |
if (!self::$enabled || !self::$hasInitialised) { | |
return 0; | |
} | |
$span = array_pop(self::$spanStack); | |
$scope = array_pop(self::$scopeStack); | |
if ($span) { | |
$span->end(); | |
} | |
if ($scope) { | |
$scope->detach(); | |
} | |
return count(self::$spanStack); | |
} | |
/** | |
* Returns the current scope, if set | |
* | |
* @return \OpenTelemetry\Context\ScopeInterface|null | |
*/ | |
public static function getCurrentScope(): ?ScopeInterface | |
{ | |
return self::$scopeStack[array_key_last(self::$scopeStack)]; | |
} | |
/** | |
* Returns the current span, if set | |
* | |
* @return \OpenTelemetry\API\Trace\SpanInterface|null | |
*/ | |
public static function getCurrentSpan(): ?SpanInterface | |
{ | |
return self::$spanStack[array_key_last(self::$spanStack)]; | |
} | |
/** | |
* Records an event at the current span | |
* | |
* @return void | |
*/ | |
public static function recordEvent(string $name, array $attributes = []) | |
{ | |
if (!self::$enabled || !self::$hasInitialised) { | |
return; | |
} | |
$span = self::getCurrentSpan(); | |
if ($span) { | |
$span->addEvent($name, $attributes); | |
} | |
} | |
/** | |
* Records an exception at the current span | |
* | |
* @param Throwable $e | |
* @return void | |
*/ | |
public static function recordException(Throwable $e) | |
{ | |
if (!self::$enabled || !self::$hasInitialised) { | |
return; | |
} | |
$message = $e->getMessage(); | |
$span = self::getCurrentSpan(); | |
if ($span) { | |
$span->setStatus(StatusCode::STATUS_ERROR, $message); | |
$span->recordException($e); | |
} | |
} | |
/** | |
* Sets up the AWS client for Xray | |
* | |
* @param $client | |
* | |
* @return void | |
*/ | |
public static function instrumentAwsSdkClient($client): void | |
{ | |
if (!self::$enabled || !self::$hasInitialised) { | |
return; | |
} | |
self::$awsSdkInstrumentation->instrumentClients([$client]); | |
self::$awsSdkInstrumentation->activate(); | |
} | |
/** | |
* Returns AWS' carrier headers for propagation | |
* | |
* @return array | |
*/ | |
public static function getCarrierHeaders() | |
{ | |
return self::$carrier; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment