Skip to content

Instantly share code, notes, and snippets.

@sc0ttdav3y
Last active January 4, 2024 08:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sc0ttdav3y/0d320d08a726dd0ed204a47bd8ebb78b to your computer and use it in GitHub Desktop.
Save sc0ttdav3y/0d320d08a726dd0ed204a47bd8ebb78b to your computer and use it in GitHub Desktop.
OpenTelemetry + Bref + PHP + Lambda + Serverless
<?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