prod/php/ElasticOTel/PhpPartFacade.php (242 lines of code) (raw):
<?php
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/** @noinspection PhpIllegalPsrClassPathInspection */
declare(strict_types=1);
namespace Elastic\OTel;
use Elastic\OTel\HttpTransport\ElasticHttpTransportFactory;
use Elastic\OTel\InferredSpans\InferredSpans;
use Elastic\OTel\Log\ElasticLogWriter;
use Elastic\OTel\Util\HiddenConstructorTrait;
use OpenTelemetry\API\Globals;
use OpenTelemetry\SDK\Registry;
use OpenTelemetry\SDK\SdkAutoloader;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SemConv\TraceAttributes;
use OpenTelemetry\SemConv\Version;
use RuntimeException;
use Throwable;
use function elastic_otel_get_config_option_by_name;
/**
* Code in this file is part of implementation internals, and thus it is not covered by the backward compatibility.
*
* @internal
*
* Called by the extension
*/
final class PhpPartFacade
{
/**
* Constructor is hidden because instance() should be used instead
*/
use HiddenConstructorTrait;
public static bool $wasBootstrapCalled = false;
private static ?self $singletonInstance = null;
private static bool $rootSpanEnded = false;
private ?InferredSpans $inferredSpans = null;
public const CONFIG_ENV_VAR_NAME_DEV_INTERNAL_MODE_IS_DEV = 'ELASTIC_OTEL_PHP_DEV_INTERNAL_MODE_IS_DEV';
/**
* We need to use TELEMETRY_DISTRO_NAME and TELEMETRY_DISTRO_VERSION attribute names before OTel SDK is loaded by composer
* so we copy those values to local constants
*
* @see \OpenTelemetry\SemConv\TraceAttributes::TELEMETRY_DISTRO_NAME
* @see \OpenTelemetry\SemConv\TraceAttributes::TELEMETRY_DISTRO_VERSION
*
* @noinspection PhpUnnecessaryFullyQualifiedNameInspection
*/
public const OTEL_ATTR_NAME_TELEMETRY_DISTRO_NAME = 'telemetry.distro.name';
public const OTEL_ATTR_NAME_TELEMETRY_DISTRO_VERSION = 'telemetry.distro.version';
/**
* Called by the extension
*
* @param string $elasticOTelNativePartVersion
* @param int $maxEnabledLogLevel
* @param float $requestInitStartTime
*
* @return bool
*/
public static function bootstrap(string $elasticOTelNativePartVersion, int $maxEnabledLogLevel, float $requestInitStartTime): bool
{
self::$wasBootstrapCalled = true;
require __DIR__ . DIRECTORY_SEPARATOR . 'BootstrapStageLogger.php';
BootstrapStageLogger::configure($maxEnabledLogLevel, __DIR__, __NAMESPACE__);
BootstrapStageLogger::logDebug(
'Starting bootstrap sequence...'
. "; elasticOTelNativePartVersion: $elasticOTelNativePartVersion" . "; maxEnabledLogLevel: $maxEnabledLogLevel" . "; requestInitStartTime: $requestInitStartTime",
__FILE__,
__LINE__,
__CLASS__,
__FUNCTION__
);
if (self::$singletonInstance !== null) {
BootstrapStageLogger::logCritical(
'bootstrap() is called even though singleton instance is already created (probably bootstrap() is called more than once)',
__FILE__,
__LINE__,
__CLASS__,
__FUNCTION__
);
return false;
}
try {
require __DIR__ . DIRECTORY_SEPARATOR . 'Autoloader.php';
Autoloader::register(__DIR__);
InstrumentationBridge::singletonInstance()->bootstrap();
self::prepareEnvForOTelSdk($elasticOTelNativePartVersion);
self::registerAutoloader();
self::registerNativeOtlpSerializer();
self::registerAsyncTransportFactory();
self::registerOtelLogWriter();
/** @noinspection PhpInternalEntityUsedInspection */
if (SdkAutoloader::isExcludedUrl()) {
BootstrapStageLogger::logDebug('Url is excluded', __FILE__, __LINE__, __CLASS__, __FUNCTION__);
return false;
}
Traces\ElasticRootSpan::startRootSpan(function () {
PhpPartFacade::$rootSpanEnded = true;
if (PhpPartFacade::$singletonInstance && PhpPartFacade::$singletonInstance->inferredSpans) {
PhpPartFacade::$singletonInstance->inferredSpans->shutdown();
}
});
self::$singletonInstance = new self();
if (elastic_otel_get_config_option_by_name('inferred_spans_enabled')) {
self::$singletonInstance->inferredSpans = new InferredSpans(
(bool)elastic_otel_get_config_option_by_name('inferred_spans_reduction_enabled'),
(bool)elastic_otel_get_config_option_by_name('inferred_spans_stacktrace_enabled'),
elastic_otel_get_config_option_by_name('inferred_spans_min_duration') // @phpstan-ignore argument.type
);
}
} catch (Throwable $throwable) {
BootstrapStageLogger::logCriticalThrowable($throwable, 'One of the steps in bootstrap sequence has thrown', __FILE__, __LINE__, __CLASS__, __FUNCTION__);
return false;
}
BootstrapStageLogger::logDebug('Successfully completed bootstrap sequence', __FILE__, __LINE__, __CLASS__, __FUNCTION__);
return true;
}
/**
* Called by the extension
*
* @noinspection PhpUnused
*/
public static function inferredSpans(int $durationMs, bool $internalFunction): bool
{
if (self::$singletonInstance === null) {
BootstrapStageLogger::logDebug('Missing facade', __FILE__, __LINE__, __CLASS__, __FUNCTION__);
return true;
}
if (self::$singletonInstance->inferredSpans === null) {
BootstrapStageLogger::logDebug('Missing inferred spans instance', __FILE__, __LINE__, __CLASS__, __FUNCTION__);
return true;
}
self::$singletonInstance->inferredSpans->captureStackTrace($durationMs, $internalFunction);
return true;
}
private static function buildElasticOTelVersion(string $nativePartVersion): string
{
if ($nativePartVersion === PhpPartVersion::VALUE) {
return $nativePartVersion;
}
BootstrapStageLogger::logWarning(
'Native part and PHP part versions do not match. native part version: ' . $nativePartVersion . '; PHP part version: ' . PhpPartVersion::VALUE,
__FILE__,
__LINE__,
__CLASS__,
__FUNCTION__
);
return $nativePartVersion . '/' . PhpPartVersion::VALUE;
}
private static function isInDevMode(): bool
{
$modeIsDevEnvVarVal = getenv(self::CONFIG_ENV_VAR_NAME_DEV_INTERNAL_MODE_IS_DEV);
if (is_string($modeIsDevEnvVarVal)) {
/**
* @var string[] $trueStringValues
* @noinspection PhpRedundantVariableDocTypeInspection
*/
static $trueStringValues = ['true', 'yes', 'on', '1'];
foreach ($trueStringValues as $trueStringValue) {
if (strcasecmp($modeIsDevEnvVarVal, $trueStringValue) === 0) {
return true;
}
}
}
return false;
}
private static function prepareEnvForOTelAttributes(string $elasticOTelNativePartVersion): void
{
$envVarName = 'OTEL_RESOURCE_ATTRIBUTES';
$envVarValueOnEntry = getenv($envVarName);
$envVarValue = (is_string($envVarValueOnEntry) && strlen($envVarValueOnEntry) !== 0) ? ($envVarValueOnEntry . ',') : '';
// https://opentelemetry.io/docs/specs/semconv/resource/#telemetry-distribution-experimental
$envVarValue .=
self::OTEL_ATTR_NAME_TELEMETRY_DISTRO_NAME . '=elastic'
. ','
. self::OTEL_ATTR_NAME_TELEMETRY_DISTRO_VERSION . '=' . self::buildElasticOTelVersion($elasticOTelNativePartVersion);
self::setEnvVar($envVarName, $envVarValue);
}
/**
* @param non-empty-string $envVarName
*/
private static function setEnvVar(string $envVarName, string $envVarValue): void
{
if (!putenv($envVarName . '=' . $envVarValue)) {
throw new RuntimeException('putenv returned false; $envVarName: ' . $envVarName . '; envVarValue: ' . $envVarValue);
}
}
private static function prepareEnvForOTelSdk(string $elasticOTelNativePartVersion): void
{
self::setEnvVar('OTEL_PHP_AUTOLOAD_ENABLED', 'true');
self::prepareEnvForOTelAttributes($elasticOTelNativePartVersion);
}
private static function registerAutoloader(): void
{
$vendorDir = ProdPhpDir::$fullPath . DIRECTORY_SEPARATOR . (
self::isInDevMode()
? ('..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'vendor')
: ('vendor_' . PHP_MAJOR_VERSION . PHP_MINOR_VERSION)
);
$vendorAutoloadPhp = $vendorDir . '/autoload.php';
if (!file_exists($vendorAutoloadPhp)) {
throw new RuntimeException("File $vendorAutoloadPhp does not exist");
}
BootstrapStageLogger::logDebug('About to require ' . $vendorAutoloadPhp, __FILE__, __LINE__, __CLASS__, __FUNCTION__);
require $vendorAutoloadPhp;
BootstrapStageLogger::logDebug('Finished successfully', __FILE__, __LINE__, __CLASS__, __FUNCTION__);
}
private static function registerAsyncTransportFactory(): void
{
if (elastic_otel_get_config_option_by_name('async_transport') === false) {
BootstrapStageLogger::logDebug('ELASTIC_OTEL_ASYNC_TRANSPORT set to false', __FILE__, __LINE__, __CLASS__, __FUNCTION__);
return;
}
Registry::registerTransportFactory('http', ElasticHttpTransportFactory::class, true);
}
private static function registerOtelLogWriter(): void
{
ElasticLogWriter::enableLogWriter();
}
private static function registerNativeOtlpSerializer(): void
{
if (elastic_otel_get_config_option_by_name('native_otlp_serializer_enabled') === false) {
BootstrapStageLogger::logDebug('ELASTIC_OTEL_NATIVE_OTLP_SERIALIZER_ENABLED set to false', __FILE__, __LINE__, __CLASS__, __FUNCTION__);
} else {
// Load classes such as \OpenTelemetry\Contrib\Otlp\SpanExporter to shadow the ones in SDK
$otelOtlpDir = ProdPhpDir::$fullPath . DIRECTORY_SEPARATOR . 'OpenTelemetry' . DIRECTORY_SEPARATOR . 'Contrib' . DIRECTORY_SEPARATOR . 'Otlp';
foreach (['SpanExporter', 'LogsExporter', 'MetricExporter'] as $exporter) {
require $otelOtlpDir . DIRECTORY_SEPARATOR . $exporter . '.php';
}
}
}
/**
* Called by the extension
*
* @noinspection PhpUnused
*/
public static function handleError(int $type, string $errorFilename, int $errorLineno, string $message): void
{
BootstrapStageLogger::logDebug(
"Called with arguments: type: $type, errorFilename: $errorFilename, errorLineno: $errorLineno, message: $message",
__FILE__,
__LINE__,
__CLASS__,
__FUNCTION__
);
}
/**
* Called by the extension
*
* @noinspection PhpUnused
*/
public static function shutdown(): void
{
self::$singletonInstance = null;
}
/**
* Called by the extension
*
* @param array<mixed> $params
*
* @noinspection PhpUnused, PhpUnusedParameterInspection
*/
public static function debugPreHook(mixed $object, array $params, ?string $class, string $function, ?string $filename, ?int $lineno): void
{
if (self::$rootSpanEnded) {
return;
}
$tracer = Globals::tracerProvider()->getTracer(
'co.elastic.edot.php.debug',
null,
Version::VERSION_1_25_0->url(),
);
$parent = Context::getCurrent();
/** @noinspection PhpDeprecationInspection */
$span = $tracer->spanBuilder($class ? $class . "::" . $function : $function) // @phpstan-ignore argument.type
->setSpanKind(SpanKind::KIND_CLIENT)
->setParent($parent)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
->setAttribute(TraceAttributes::CODE_FILE_PATH, $filename)
->setAttribute(TraceAttributes::CODE_LINENO, $lineno)
->setAttribute('call.arguments', print_r($params, true))
->startSpan();
$context = $span->storeInContext($parent);
Context::storage()->attach($context);
}
/**
* Called by the extension
*
* @param array<mixed> $params
*
* @noinspection PhpUnused, PhpUnusedParameterInspection
*/
public static function debugPostHook(mixed $object, array $params, mixed $retval, ?Throwable $exception): void
{
if (self::$rootSpanEnded) {
return;
}
$scope = Context::storage()->scope();
if (!$scope) {
return;
}
$scope->detach();
$span = Span::fromContext($scope->context());
$span->setAttribute('call.return_value', print_r($retval, true));
if ($exception) {
/** @noinspection PhpDeprecationInspection */
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
}
$span->end();
}
}