Skip to content

Instantly share code, notes, and snippets.

@breda
Created December 6, 2024 13:45
Show Gist options
  • Save breda/79c8f38af5fac7b1c0d3bb15bff2c314 to your computer and use it in GitHub Desktop.
Save breda/79c8f38af5fac7b1c0d3bb15bff2c314 to your computer and use it in GitHub Desktop.
Simple class to add OpenTelemetry instrumentation for a class/method
<?php
/*
* Deps:
* composer require open-telemetry/sem-conv
* composer require open-telemetry/api
* composer require open-telemetry/context
*
* ext-opentelemetry
*/
declare(strict_types=1);
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SemConv\TraceAttributes;
use Throwable;
use function array_map;
use function implode;
use function is_object;
use function OpenTelemetry\Instrumentation\hook;
use function sprintf;
use function strrpos;
use function substr;
/**
* ClassInstrumentation is a re-usable helper class to register OpenTelemetry instrumentation for a specific class and method.
*
* It will create a span for each call to the method.
* The span will have the following attributes (at least):
* - code.function: the name of the function
* - code.namespace: the namespace of the class
* - code.filepath: the file path of the class
* - code.lineno: the line number of the function
* - code.params: the parameters of the function
*
*
* Example usage:
* ```php
* ClassInstrumentation::register('BookRepository', 'findById');
* ```
*/
class ClassInstrumentation
{
/* CHANGE ME */
public const string NAME = 'name';
public static function register(string $className, string $methodName): void
{
hook(
class: $className,
function: $methodName,
pre: static function (
object $instance,
array $params,
string $class,
string $function,
?string $filename,
?int $lineno,
): void {
$instrumentation = new CachedInstrumentation('opentelemetry.' . self::NAME);
$params = array_map(static function ($param) {
return is_object($param) ? $param::class : '"' . $param . '"';
}, $params);
$params = sprintf('[%s]', implode(', ', $params));
$namespace = substr($class, 0, (int) strrpos($class, '\\'));
$className = substr($class, (int) strrpos($class, '\\') + 1);
$builder = $instrumentation->tracer()
->spanBuilder(sprintf('%s::%s', $className, $function))
->setSpanKind(SpanKind::KIND_CLIENT)
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $namespace)
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
->setAttribute(TraceAttributes::CODE_LINENO, $lineno)
->setAttribute('code.params', $params);
$parent = Context::getCurrent();
$span = $builder->startSpan();
Context::storage()->attach($span->storeInContext($parent));
},
post: static function (object $instance, array $params, mixed $statement, ?Throwable $exception): void {
$scope = Context::storage()->scope();
if ($scope === null) {
return;
}
$scope->detach();
$span = Span::fromContext($scope->context());
if ($exception !== null) {
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
}
$span->end();
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment