From 9d616c9537d768adb6138e5530f7e38f185746c0 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Tue, 12 Nov 2024 10:54:49 +0200 Subject: [PATCH 01/29] Added Config for tests --- composer.json | 2 +- .../Util/Config/AllOptionsMetadata.php | 128 ++++++ .../Util/Config/BoolOptionMetadata.php | 39 ++ .../Util/Config/BoolOptionParser.php | 71 +++ .../Util/Config/CompositeRawSnapshot.php | 53 +++ .../Config/CompositeRawSnapshotSource.php | 53 +++ .../Util/Config/DevInternalSubOptionNames.php | 41 ++ .../Util/Config/DurationOptionMetadata.php | 46 ++ .../Util/Config/DurationOptionParser.php | 144 ++++++ .../Util/Config/DurationUnits.php | 57 +++ .../Util/Config/EnumOptionParser.php | 130 ++++++ .../Util/Config/EnvVarsRawSnapshotSource.php | 65 +++ .../Util/Config/FloatOptionMetadata.php | 39 ++ .../Util/Config/FloatOptionParser.php | 52 +++ .../Util/Config/IniRawSnapshotSource.php | 84 ++++ .../Util/Config/IntOptionMetadata.php | 39 ++ .../Util/Config/IntOptionParser.php | 52 +++ .../Util/Config/KeyValuePairsOptionParser.php | 72 +++ .../Util/Config/LabelsOptionParser.php | 82 ++++ .../Util/Config/LogLevelOptionMetadata.php | 39 ++ .../Util/Config/LogLevelOptionParser.php | 41 ++ .../Config/NullableBoolOptionMetadata.php | 39 ++ .../Util/Config/NullableIntOptionMetadata.php | 39 ++ .../Config/NullableLabelsOptionMetadata.php | 41 ++ .../Config/NullableLogLevelOptionMetadata.php | 39 ++ .../Util/Config/NullableOptionMetadata.php | 63 +++ .../Config/NullableStringOptionMetadata.php | 39 ++ .../NullableWildcardListOptionMetadata.php | 41 ++ .../Util/Config/NumericOptionParser.php | 121 +++++ .../Util/Config/OptionDefaultValues.php | 40 ++ .../Util/Config/OptionMetadata.php | 49 ++ .../Util/Config/OptionNames.php | 77 ++++ .../Util/Config/OptionParser.php | 47 ++ .../Config/OptionWithDefaultValueMetadata.php | 76 ++++ .../Util/Config/ParseException.php | 39 ++ tests/ElasticOTelTests/Util/Config/Parser.php | 112 +++++ .../Util/Config/RawSnapshotFromArray.php | 50 +++ .../Util/Config/RawSnapshotInterface.php | 34 ++ .../Config/RawSnapshotSourceInterface.php | 41 ++ .../ElasticOTelTests/Util/Config/Snapshot.php | 417 ++++++++++++++++++ .../Util/Config/SnapshotDevInternal.php | 94 ++++ .../Util/Config/SnapshotTrait.php | 76 ++++ .../Util/Config/StringOptionMetadata.php | 41 ++ .../Util/Config/StringOptionParser.php | 39 ++ .../Config/WildcardListOptionMetadata.php | 41 ++ .../Util/Config/WildcardListOptionParser.php | 56 +++ 46 files changed, 3079 insertions(+), 1 deletion(-) create mode 100644 tests/ElasticOTelTests/Util/Config/AllOptionsMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/BoolOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/BoolOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/CompositeRawSnapshot.php create mode 100644 tests/ElasticOTelTests/Util/Config/CompositeRawSnapshotSource.php create mode 100644 tests/ElasticOTelTests/Util/Config/DevInternalSubOptionNames.php create mode 100644 tests/ElasticOTelTests/Util/Config/DurationOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/DurationOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/DurationUnits.php create mode 100644 tests/ElasticOTelTests/Util/Config/EnumOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/EnvVarsRawSnapshotSource.php create mode 100644 tests/ElasticOTelTests/Util/Config/FloatOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/FloatOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/IniRawSnapshotSource.php create mode 100644 tests/ElasticOTelTests/Util/Config/IntOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/IntOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/KeyValuePairsOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/LabelsOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/LogLevelOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/LogLevelOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/NullableBoolOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/NullableIntOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/NullableLabelsOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/NullableLogLevelOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/NullableOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/NullableStringOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/NullableWildcardListOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/NumericOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/OptionDefaultValues.php create mode 100644 tests/ElasticOTelTests/Util/Config/OptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/OptionNames.php create mode 100644 tests/ElasticOTelTests/Util/Config/OptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/OptionWithDefaultValueMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/ParseException.php create mode 100644 tests/ElasticOTelTests/Util/Config/Parser.php create mode 100644 tests/ElasticOTelTests/Util/Config/RawSnapshotFromArray.php create mode 100644 tests/ElasticOTelTests/Util/Config/RawSnapshotInterface.php create mode 100644 tests/ElasticOTelTests/Util/Config/RawSnapshotSourceInterface.php create mode 100644 tests/ElasticOTelTests/Util/Config/Snapshot.php create mode 100644 tests/ElasticOTelTests/Util/Config/SnapshotDevInternal.php create mode 100644 tests/ElasticOTelTests/Util/Config/SnapshotTrait.php create mode 100644 tests/ElasticOTelTests/Util/Config/StringOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/StringOptionParser.php create mode 100644 tests/ElasticOTelTests/Util/Config/WildcardListOptionMetadata.php create mode 100644 tests/ElasticOTelTests/Util/Config/WildcardListOptionParser.php diff --git a/composer.json b/composer.json index 7207b82..aec65e8 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "php-parallel-lint/php-parallel-lint": "1.3.2", "phpstan/phpstan": "1.11.4", "phpstan/phpstan-phpunit": "^1.3.15", - "phpunit/phpunit": "^9.6||^10.5", + "phpunit/phpunit": "^9.6", "slevomat/coding-standard": "8.14.1", "squizlabs/php_codesniffer": "3.8.1" }, diff --git a/tests/ElasticOTelTests/Util/Config/AllOptionsMetadata.php b/tests/ElasticOTelTests/Util/Config/AllOptionsMetadata.php new file mode 100644 index 0000000..cf9dbbe --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/AllOptionsMetadata.php @@ -0,0 +1,128 @@ +> + */ + private static $vaLue = null; + + private static function buildDurationMetadataInMillisecondsWithMin(int $min, int $default): DurationOptionMetadata + { + return new DurationOptionMetadata(floatval($min), /* max */ null, DurationUnits::MILLISECONDS, floatval($default)); + } + + private static function buildDurationMetadataInMillisecondsNoMin(int $default): DurationOptionMetadata + { + return new DurationOptionMetadata(/* min */ null, /* max */ null, DurationUnits::MILLISECONDS, floatval($default)); + } + + private static function buildDurationMetadataInMilliseconds(int $default): DurationOptionMetadata + { + return self::buildDurationMetadataInMillisecondsWithMin(/* min */ 0, $default); + } + + private static function buildDurationMetadataInSeconds(int $defaultInSeconds): DurationOptionMetadata + { + return new DurationOptionMetadata(/* min */ 0.0, /* max */ null, DurationUnits::SECONDS, floatval($defaultInSeconds * TimeUtil::NUMBER_OF_MILLISECONDS_IN_SECOND)); + } + + private static function buildPositiveOrZeroIntMetadata(int $default): IntOptionMetadata + { + return new IntOptionMetadata(/* min */ 0, /* max */ null, $default); + } + + /** + * @return array> Option name to metadata + */ + public static function get(): array + { + if (self::$vaLue !== null) { + return self::$vaLue; + } + + /** @var array> $value */ + $value = [ + OptionNames::API_KEY => new NullableStringOptionMetadata(), + OptionNames::AST_PROCESS_ENABLED => new BoolOptionMetadata(/* defaultValue: */ false), + OptionNames::AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE + => new BoolOptionMetadata(/* defaultValue: */ true), + OptionNames::AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX => new NullableStringOptionMetadata(), + OptionNames::AST_PROCESS_DEBUG_DUMP_OUT_DIR => new NullableStringOptionMetadata(), + OptionNames::ASYNC_BACKEND_COMM => new BoolOptionMetadata(/* default */ true), + OptionNames::BREAKDOWN_METRICS => new BoolOptionMetadata(/* default */ true), + OptionNames::CAPTURE_ERRORS => new BoolOptionMetadata(/* default */ true), + OptionNames::DEV_INTERNAL => new NullableWildcardListOptionMetadata(), + OptionNames::DISABLE_INSTRUMENTATIONS => new NullableWildcardListOptionMetadata(), + OptionNames::DISABLE_SEND => new BoolOptionMetadata(/* default */ false), + OptionNames::ENABLED => new BoolOptionMetadata(/* default */ true), + OptionNames::ENVIRONMENT => new NullableStringOptionMetadata(), + OptionNames::GLOBAL_LABELS => new NullableLabelsOptionMetadata(), + OptionNames::HOSTNAME => new NullableStringOptionMetadata(), + OptionNames::LOG_LEVEL => new NullableLogLevelOptionMetadata(), + OptionNames::LOG_LEVEL_STDERR => new NullableLogLevelOptionMetadata(), + OptionNames::LOG_LEVEL_SYSLOG => new NullableLogLevelOptionMetadata(), + OptionNames::NON_KEYWORD_STRING_MAX_LENGTH => self::buildPositiveOrZeroIntMetadata(/* default */ 10 * 1024), + OptionNames::PROFILING_INFERRED_SPANS_ENABLED => new BoolOptionMetadata(/* default */ false), + OptionNames::PROFILING_INFERRED_SPANS_MIN_DURATION => self::buildDurationMetadataInMilliseconds(/* default */ 0), + OptionNames::PROFILING_INFERRED_SPANS_SAMPLING_INTERVAL => self::buildDurationMetadataInMillisecondsWithMin(/* min */ 1, /* default */ 50), + OptionNames::SANITIZE_FIELD_NAMES => new WildcardListOptionMetadata(WildcardListOptionParser::staticParse(self::SANITIZE_FIELD_NAMES_DEFAULT)), + OptionNames::SECRET_TOKEN => new NullableStringOptionMetadata(), + OptionNames::SERVER_TIMEOUT => self::buildDurationMetadataInSeconds(/* default */ 30), + OptionNames::SERVICE_NAME => new NullableStringOptionMetadata(), + OptionNames::SERVICE_NODE_NAME => new NullableStringOptionMetadata(), + OptionNames::SERVICE_VERSION => new NullableStringOptionMetadata(), + OptionNames::SPAN_COMPRESSION_ENABLED => new BoolOptionMetadata(/* default */ true), + OptionNames::SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION => self::buildDurationMetadataInMilliseconds(/* default */ 50), + OptionNames::SPAN_COMPRESSION_SAME_KIND_MAX_DURATION => self::buildDurationMetadataInMilliseconds(/* default */ 0), + OptionNames::SPAN_STACK_TRACE_MIN_DURATION => self::buildDurationMetadataInMillisecondsNoMin(/* default */ OptionDefaultValues::SPAN_STACK_TRACE_MIN_DURATION), + OptionNames::STACK_TRACE_LIMIT => new IntOptionMetadata(/* min */ null, /* max */ null, /* default */ OptionDefaultValues::STACK_TRACE_LIMIT), + OptionNames::TRANSACTION_IGNORE_URLS => new NullableWildcardListOptionMetadata(), + OptionNames::TRANSACTION_MAX_SPANS => self::buildPositiveOrZeroIntMetadata(OptionDefaultValues::TRANSACTION_MAX_SPANS), + OptionNames::TRANSACTION_SAMPLE_RATE => new FloatOptionMetadata(/* min */ 0.0, /* max */ 1.0, /* default */ 1.0), + OptionNames::URL_GROUPS => new NullableWildcardListOptionMetadata(), + OptionNames::VERIFY_SERVER_CERT => new BoolOptionMetadata(/* default */ true), + ]; + + self::$vaLue = $value; + return self::$vaLue; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/BoolOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/BoolOptionMetadata.php new file mode 100644 index 0000000..c13e761 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/BoolOptionMetadata.php @@ -0,0 +1,39 @@ + + */ +final class BoolOptionMetadata extends OptionWithDefaultValueMetadata +{ + public function __construct(bool $defaultValue) + { + parent::__construct(new BoolOptionParser(), $defaultValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/BoolOptionParser.php b/tests/ElasticOTelTests/Util/Config/BoolOptionParser.php new file mode 100644 index 0000000..7a59045 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/BoolOptionParser.php @@ -0,0 +1,71 @@ + + */ +final class BoolOptionParser extends EnumOptionParser +{ + /** @var array */ + public static $trueRawValues = ['true', 'yes', 'on', '1']; + + /** @var array */ + public static $falseRawValues = ['false', 'no', 'off', '0']; + + /** @var ?array */ + private static $boolNameToValue = null; + + public function __construct() + { + if (self::$boolNameToValue === null) { + self::$boolNameToValue = []; + foreach (self::$trueRawValues as $trueRawValue) { + self::$boolNameToValue[] = [$trueRawValue, true]; + } + foreach (self::$falseRawValues as $falseRawValue) { + self::$boolNameToValue[] = [$falseRawValue, false]; + } + } + + parent::__construct( + 'bool' /* <- dbgEnumDesc */, + self::$boolNameToValue /* <- nameToValue */, + false /* <- isCaseSensitive */, + false /* <- isUnambiguousPrefixAllowed */ + ); + } + + /** @inheritDoc */ + public function parse(string $rawValue) + { + return TextUtil::isEmptyString($rawValue) ? false : parent::parse($rawValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/CompositeRawSnapshot.php b/tests/ElasticOTelTests/Util/Config/CompositeRawSnapshot.php new file mode 100644 index 0000000..ca6e9d3 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/CompositeRawSnapshot.php @@ -0,0 +1,53 @@ + */ + private $subSnapshots; + + /** + * @param array $subSnapshots + */ + public function __construct(array $subSnapshots) + { + $this->subSnapshots = $subSnapshots; + } + + public function valueFor(string $optionName): ?string + { + foreach ($this->subSnapshots as $subSnapshot) { + if (($value = $subSnapshot->valueFor($optionName)) !== null) { + return $value; + } + } + return null; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/CompositeRawSnapshotSource.php b/tests/ElasticOTelTests/Util/Config/CompositeRawSnapshotSource.php new file mode 100644 index 0000000..e5bce4a --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/CompositeRawSnapshotSource.php @@ -0,0 +1,53 @@ + */ + private $subSources; + + /** + * @param array $subSources + */ + public function __construct(array $subSources) + { + $this->subSources = $subSources; + } + + public function currentSnapshot(array $optionNameToMeta): RawSnapshotInterface + { + /** @var array */ + $subSnapshots = []; + foreach ($this->subSources as $subSource) { + $subSnapshots[] = $subSource->currentSnapshot($optionNameToMeta); + } + return new CompositeRawSnapshot($subSnapshots); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/DevInternalSubOptionNames.php b/tests/ElasticOTelTests/Util/Config/DevInternalSubOptionNames.php new file mode 100644 index 0000000..da2e0ab --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/DevInternalSubOptionNames.php @@ -0,0 +1,41 @@ + + */ +final class DurationOptionMetadata extends OptionWithDefaultValueMetadata +{ + public function __construct( + ?float $minValidValueInMilliseconds, + ?float $maxValidValueInMilliseconds, + int $defaultUnits, + float $defaultValueInMilliseconds + ) { + parent::__construct( + new DurationOptionParser($minValidValueInMilliseconds, $maxValidValueInMilliseconds, $defaultUnits), + $defaultValueInMilliseconds + ); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/DurationOptionParser.php b/tests/ElasticOTelTests/Util/Config/DurationOptionParser.php new file mode 100644 index 0000000..a3d85ad --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/DurationOptionParser.php @@ -0,0 +1,144 @@ + + */ +final class DurationOptionParser extends OptionParser +{ + /** @var ?float */ + private $minValidValueInMilliseconds; + + /** @var ?float */ + private $maxValidValueInMilliseconds; + + /** @var int */ + private $defaultUnits; + + public function __construct( + ?float $minValidValueInMilliseconds, + ?float $maxValidValueInMilliseconds, + int $defaultUnits + ) { + $this->minValidValueInMilliseconds = $minValidValueInMilliseconds; + $this->maxValidValueInMilliseconds = $maxValidValueInMilliseconds; + $this->defaultUnits = $defaultUnits; + } + + /** @inheritDoc */ + public function parse(string $rawValue): float + { + $partWithoutSuffix = ''; + $units = $this->defaultUnits; + self::splitToValueAndUnits($rawValue, /* ref */ $partWithoutSuffix, /* ref */ $units); + + $auxFloatOptionParser = new FloatOptionParser(null /* minValidValue */, null /* maxValidValue */); + $parsedValueInMilliseconds + = self::convertToMilliseconds($auxFloatOptionParser->parse($partWithoutSuffix), $units); + + if ( + ( + ($this->minValidValueInMilliseconds !== null) + && ($parsedValueInMilliseconds < $this->minValidValueInMilliseconds) + ) + || ( + ($this->maxValidValueInMilliseconds !== null) + && ($parsedValueInMilliseconds > $this->maxValidValueInMilliseconds) + ) + ) { + throw new ParseException( + 'Value is not in range between the valid minimum and maximum values.' + . ' Raw option value: `' . $rawValue . "'." + . ' Parsed option value (in milliseconds): ' . $parsedValueInMilliseconds . '.' + . ' The valid minimum value (in milliseconds): ' . $this->minValidValueInMilliseconds . '.' + . ' The valid maximum value (in milliseconds): ' . $this->maxValidValueInMilliseconds . '.' + ); + } + + return $parsedValueInMilliseconds; + } + + public function defaultUnits(): int + { + return $this->defaultUnits; + } + + public function minValidValueInMilliseconds(): ?float + { + return $this->minValidValueInMilliseconds; + } + + public function maxValidValueInMilliseconds(): ?float + { + return $this->maxValidValueInMilliseconds; + } + + private static function splitToValueAndUnits(string $rawValue, string &$partWithoutSuffix, int &$units): void + { + foreach (DurationUnits::$suffixAndIdPairs as $suffixAndIdPair) { + $suffix = $suffixAndIdPair[0]; + if (TextUtil::isSuffixOf($suffix, $rawValue, /* isCaseSensitive */ false)) { + $partWithoutSuffix = trim(substr($rawValue, 0, -strlen($suffix))); + $units = $suffixAndIdPair[1]; + return; + } + } + $partWithoutSuffix = $rawValue; + } + + public static function convertToMilliseconds(float $srcValue, int $srcValueUnits): float + { + switch ($srcValueUnits) { + case DurationUnits::MILLISECONDS: + return $srcValue; + + case DurationUnits::SECONDS: + return $srcValue * 1000; + + case DurationUnits::MINUTES: + return $srcValue * 60 * 1000; + + default: + throw new ParseException( + ExceptionUtil::buildMessage( + 'Not a valid time duration units ID', + /* context */ + [ + 'srcValueUnits' => $srcValueUnits, + 'srcValue' => $srcValue, + 'valid time duration units' => DurationUnits::$suffixAndIdPairs, + ] + ) + ); + } + } +} diff --git a/tests/ElasticOTelTests/Util/Config/DurationUnits.php b/tests/ElasticOTelTests/Util/Config/DurationUnits.php new file mode 100644 index 0000000..1a2aa73 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/DurationUnits.php @@ -0,0 +1,57 @@ + Array should be in descending order of suffix length + * + * @see TimeDurationUnitsTest::testSuffixAndIdIsInDescendingOrderOfSuffixLength + */ + public static $suffixAndIdPairs + = [ + [self::MILLISECONDS_SUFFIX, self::MILLISECONDS], + [self::SECONDS_SUFFIX, self::SECONDS], + [self::MINUTES_SUFFIX, self::MINUTES], + ]; +} diff --git a/tests/ElasticOTelTests/Util/Config/EnumOptionParser.php b/tests/ElasticOTelTests/Util/Config/EnumOptionParser.php new file mode 100644 index 0000000..748bc3d --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/EnumOptionParser.php @@ -0,0 +1,130 @@ + + */ +class EnumOptionParser extends OptionParser +{ + /** @var string */ + private $dbgEnumDesc; + + /** + * We are forced to use list-array of pairs instead of regular associative array + * because in an associative array if the key is numeric string it's automatically converted to int + * (see https://www.php.net/manual/en/language.types.array.php) + * + * @var array> + * @phpstan-var array + */ + private $nameValuePairs; + + /** @var bool */ + private $isCaseSensitive; + + /** @var bool */ + private $isUnambiguousPrefixAllowed; + + /** + * @param string $dbgEnumDesc + * @param array $nameValuePairs + * @param bool $isCaseSensitive + * @param bool $isUnambiguousPrefixAllowed + */ + public function __construct( + string $dbgEnumDesc, + array $nameValuePairs, + bool $isCaseSensitive, + bool $isUnambiguousPrefixAllowed + ) { + $this->dbgEnumDesc = $dbgEnumDesc; + $this->nameValuePairs = $nameValuePairs; + $this->isCaseSensitive = $isCaseSensitive; + $this->isUnambiguousPrefixAllowed = $isUnambiguousPrefixAllowed; + } + + /** + * @return array> + * @phpstan-return array + */ + public function nameValuePairs(): array + { + return $this->nameValuePairs; + } + + public function isCaseSensitive(): bool + { + return $this->isCaseSensitive; + } + + public function isUnambiguousPrefixAllowed(): bool + { + return $this->isUnambiguousPrefixAllowed; + } + + /** + * @param string $rawValue + * + * @return mixed + * + * @phpstan-return T + */ + public function parse(string $rawValue) + { + foreach ($this->nameValuePairs as $enumEntryNameValuePair) { + if (TextUtil::isPrefixOf($rawValue, $enumEntryNameValuePair[0], $this->isCaseSensitive)) { + if (strlen($enumEntryNameValuePair[0]) === strlen($rawValue)) { + return $enumEntryNameValuePair[1]; + } + + if (!$this->isUnambiguousPrefixAllowed) { + continue; + } + + if (isset($foundMatchingEntry)) { + throw new ParseException( + "Not a valid $this->dbgEnumDesc value - it matches more than one entry as a prefix." + . " Raw option value: `$rawValue'" + ); + } + $foundMatchingEntry = $enumEntryNameValuePair[1]; + } + } + + if (!isset($foundMatchingEntry)) { + throw new ParseException("Not a valid $this->dbgEnumDesc value. Raw option value: `$rawValue'"); + } + + return $foundMatchingEntry; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/EnvVarsRawSnapshotSource.php b/tests/ElasticOTelTests/Util/Config/EnvVarsRawSnapshotSource.php new file mode 100644 index 0000000..1f1c7cf --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/EnvVarsRawSnapshotSource.php @@ -0,0 +1,65 @@ +envVarNamesPrefix = $envVarNamesPrefix; + } + + public static function optionNameToEnvVarName(string $envVarNamesPrefix, string $optionName): string + { + return $envVarNamesPrefix . strtoupper($optionName); + } + + public function currentSnapshot(array $optionNameToMeta): RawSnapshotInterface + { + /** @var array */ + $optionNameToEnvVarValue = []; + + foreach ($optionNameToMeta as $optionName => $optionMeta) { + $envVarValue = getenv(self::optionNameToEnvVarName($this->envVarNamesPrefix, $optionName)); + if ($envVarValue !== false) { + $optionNameToEnvVarValue[$optionName] = $envVarValue; + } + } + + return new RawSnapshotFromArray($optionNameToEnvVarValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/FloatOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/FloatOptionMetadata.php new file mode 100644 index 0000000..46f1633 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/FloatOptionMetadata.php @@ -0,0 +1,39 @@ + + */ +final class FloatOptionMetadata extends OptionWithDefaultValueMetadata +{ + public function __construct(?float $minValidValue, ?float $maxValidValue, float $defaultValue) + { + parent::__construct(new FloatOptionParser($minValidValue, $maxValidValue), $defaultValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/FloatOptionParser.php b/tests/ElasticOTelTests/Util/Config/FloatOptionParser.php new file mode 100644 index 0000000..29a3995 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/FloatOptionParser.php @@ -0,0 +1,52 @@ + + */ +final class FloatOptionParser extends NumericOptionParser +{ + /** @inheritDoc */ + protected function dbgValueTypeDesc(): string + { + return 'float'; + } + + /** @inheritDoc */ + public static function isValidFormat(string $rawValue): bool + { + return filter_var($rawValue, FILTER_VALIDATE_FLOAT) !== false; + } + + /** @inheritDoc */ + protected function stringToNumber(string $rawValue) + { + return floatval($rawValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/IniRawSnapshotSource.php b/tests/ElasticOTelTests/Util/Config/IniRawSnapshotSource.php new file mode 100644 index 0000000..56970a2 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/IniRawSnapshotSource.php @@ -0,0 +1,84 @@ +iniNamesPrefix = $iniNamesPrefix; + } + + public static function optionNameToIniName(string $iniNamesPrefix, string $optionName): string + { + return $iniNamesPrefix . $optionName; + } + + public function currentSnapshot(array $optionNameToMeta): RawSnapshotInterface + { + /** @var array */ + $optionNameToValue = []; + + /** @var array */ + $allOpts = ini_get_all(/* extension: */ null, /* details */ false); + + foreach ($optionNameToMeta as $optionName => $optionMeta) { + $iniName = self::optionNameToIniName($this->iniNamesPrefix, $optionName); + if (($iniValue = ArrayUtil::getValueIfKeyExistsElse($iniName, $allOpts, null)) !== null) { + $optionNameToValue[$optionName] = self::iniValueToString($iniValue); + } + } + + return new RawSnapshotFromArray($optionNameToValue); + } + + /** + * @param mixed $iniValue + * + * @return string + */ + private static function iniValueToString($iniValue): string + { + if (is_bool($iniValue)) { + return $iniValue ? 'true' : 'false'; + } + + return strval($iniValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/IntOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/IntOptionMetadata.php new file mode 100644 index 0000000..08c439f --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/IntOptionMetadata.php @@ -0,0 +1,39 @@ + + */ +final class IntOptionMetadata extends OptionWithDefaultValueMetadata +{ + public function __construct(?int $minValidValue, ?int $maxValidValue, int $defaultValue) + { + parent::__construct(new IntOptionParser($minValidValue, $maxValidValue), $defaultValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/IntOptionParser.php b/tests/ElasticOTelTests/Util/Config/IntOptionParser.php new file mode 100644 index 0000000..3ebba76 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/IntOptionParser.php @@ -0,0 +1,52 @@ + + */ +final class IntOptionParser extends NumericOptionParser +{ + /** @inheritDoc */ + protected function dbgValueTypeDesc(): string + { + return 'int'; + } + + /** @inheritDoc */ + public static function isValidFormat(string $rawValue): bool + { + return filter_var($rawValue, FILTER_VALIDATE_INT) !== false; + } + + /** @inheritDoc */ + protected function stringToNumber(string $rawValue) + { + return intval($rawValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/KeyValuePairsOptionParser.php b/tests/ElasticOTelTests/Util/Config/KeyValuePairsOptionParser.php new file mode 100644 index 0000000..c4a3f39 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/KeyValuePairsOptionParser.php @@ -0,0 +1,72 @@ +> + */ +final class KeyValuePairsOptionParser extends OptionParser +{ + /** + * @param string $rawValue + * + * @return array + */ + public function parse(string $rawValue): array + { + // Value format: + // key=value[,key=value[,...]] + + // Treat empty string as zero key-value pairs + if (TextUtil::isEmptyString($rawValue)) { + return []; + } + + $pairs = explode(',', $rawValue); + $result = []; + foreach ($pairs as $keyValuePair) { + $keyValueSeparatorPos = strpos($keyValuePair, '='); + if ($keyValueSeparatorPos === false) { + throw new ParseException('One of key-value pairs is missing key-value separator' . ' ;' . LoggableToString::convert(['keyValuePair' => $keyValuePair, 'rawValue' => $rawValue])); + } + $key = trim(substr($keyValuePair, /* offset */ 0, /* length */ $keyValueSeparatorPos)); + $value = ($keyValueSeparatorPos === (strlen($keyValuePair) - 1)) ? '' : trim(substr($keyValuePair, /* offset */ $keyValueSeparatorPos + 1)); + if (array_key_exists($key, $result)) { + throw new ParseException( + 'Key is present more than once' + . ' ;' . LoggableToString::convert(['key' => $key, '1st value' => $result[$key], '2nd value' => $value, '2nd keyValuePair' => $keyValuePair, 'rawValue' => $rawValue]) + ); + } + $result[$key] = $value; + } + return $result; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/LabelsOptionParser.php b/tests/ElasticOTelTests/Util/Config/LabelsOptionParser.php new file mode 100644 index 0000000..6f4a772 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/LabelsOptionParser.php @@ -0,0 +1,82 @@ +> + */ +final class LabelsOptionParser extends OptionParser +{ + /** + * @param string $valueAsString + * + * @return string|bool|int|float|null + */ + private static function parseValue(string $valueAsString) + { + if ($valueAsString === 'true') { + return true; + } + if ($valueAsString === 'false') { + return false; + } + + if (filter_var($valueAsString, FILTER_VALIDATE_INT) !== false) { + return intval($valueAsString); + } + + if (filter_var($valueAsString, FILTER_VALIDATE_FLOAT) !== false) { + return floatval($valueAsString); + } + + if ($valueAsString === 'null') { + return null; + } + + return Tracer::limitKeywordString($valueAsString); + } + + /** + * @param string $rawValue + * + * @return array + */ + public function parse(string $rawValue): array + { + // Value format: + // key=value[,key=value[,...]] + + $result = []; + foreach ((new KeyValuePairsOptionParser())->parse($rawValue) as $key => $valueAsString) { + $result[is_string($key) ? Tracer::limitKeywordString($key) : $key] = self::parseValue($valueAsString); + } + return $result; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/LogLevelOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/LogLevelOptionMetadata.php new file mode 100644 index 0000000..3f8e600 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/LogLevelOptionMetadata.php @@ -0,0 +1,39 @@ + + */ +final class LogLevelOptionMetadata extends OptionWithDefaultValueMetadata +{ + public function __construct(int $defaultValue) + { + parent::__construct(new LogLevelOptionParser(), $defaultValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/LogLevelOptionParser.php b/tests/ElasticOTelTests/Util/Config/LogLevelOptionParser.php new file mode 100644 index 0000000..df65b85 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/LogLevelOptionParser.php @@ -0,0 +1,41 @@ + + */ +final class LogLevelOptionParser extends EnumOptionParser +{ + public function __construct() + { + parent::__construct(/* dbgEnumDesc */ 'log level', Level::nameIntPairs(), /* isCaseSensitive */ false, /* isUnambiguousPrefixAllowed */ true); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/NullableBoolOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/NullableBoolOptionMetadata.php new file mode 100644 index 0000000..91ba9ba --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/NullableBoolOptionMetadata.php @@ -0,0 +1,39 @@ + + */ +final class NullableBoolOptionMetadata extends NullableOptionMetadata +{ + public function __construct() + { + parent::__construct(new BoolOptionParser()); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/NullableIntOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/NullableIntOptionMetadata.php new file mode 100644 index 0000000..00a1135 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/NullableIntOptionMetadata.php @@ -0,0 +1,39 @@ + + */ +final class NullableIntOptionMetadata extends NullableOptionMetadata +{ + public function __construct(?int $minValidValue, ?int $maxValidValue) + { + parent::__construct(new IntOptionParser($minValidValue, $maxValidValue)); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/NullableLabelsOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/NullableLabelsOptionMetadata.php new file mode 100644 index 0000000..93d435f --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/NullableLabelsOptionMetadata.php @@ -0,0 +1,41 @@ +> + * + * @noinspection PhpUnused + */ +final class NullableLabelsOptionMetadata extends NullableOptionMetadata +{ + public function __construct() + { + parent::__construct(new LabelsOptionParser()); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/NullableLogLevelOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/NullableLogLevelOptionMetadata.php new file mode 100644 index 0000000..1fac978 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/NullableLogLevelOptionMetadata.php @@ -0,0 +1,39 @@ + + */ +final class NullableLogLevelOptionMetadata extends NullableOptionMetadata +{ + public function __construct() + { + parent::__construct(new LogLevelOptionParser()); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/NullableOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/NullableOptionMetadata.php new file mode 100644 index 0000000..2433090 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/NullableOptionMetadata.php @@ -0,0 +1,63 @@ + + */ +abstract class NullableOptionMetadata extends OptionMetadata +{ + /** @var OptionParser */ + private $parser; + + /** + * @param OptionParser $parser + */ + public function __construct(OptionParser $parser) + { + $this->parser = $parser; + } + + /** @inheritDoc */ + public function parser(): OptionParser + { + return $this->parser; + } + + /** + * @inheritDoc + * + * @return null + */ + public function defaultValue() + { + return null; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/NullableStringOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/NullableStringOptionMetadata.php new file mode 100644 index 0000000..d101c3c --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/NullableStringOptionMetadata.php @@ -0,0 +1,39 @@ + + */ +final class NullableStringOptionMetadata extends NullableOptionMetadata +{ + public function __construct() + { + parent::__construct(new StringOptionParser()); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/NullableWildcardListOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/NullableWildcardListOptionMetadata.php new file mode 100644 index 0000000..724c340 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/NullableWildcardListOptionMetadata.php @@ -0,0 +1,41 @@ + + */ +final class NullableWildcardListOptionMetadata extends NullableOptionMetadata +{ + public function __construct() + { + parent::__construct(new WildcardListOptionParser()); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/NumericOptionParser.php b/tests/ElasticOTelTests/Util/Config/NumericOptionParser.php new file mode 100644 index 0000000..35eaaf4 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/NumericOptionParser.php @@ -0,0 +1,121 @@ + + */ +abstract class NumericOptionParser extends OptionParser +{ + /** @var ?TParsedValue */ + private $minValidValue; + + /** + * @var ?TParsedValue */ + private $maxValidValue; + + /** + * NumericOptionMetadata constructor. + * + * @param ?TParsedValue $minValidValue + * @param ?TParsedValue $maxValidValue + */ + public function __construct($minValidValue, $maxValidValue) + { + $this->minValidValue = $minValidValue; + $this->maxValidValue = $maxValidValue; + } + + /** + * @return string + */ + abstract protected function dbgValueTypeDesc(): string; + + /** + * @param string $rawValue + * + * @return bool + */ + abstract public static function isValidFormat(string $rawValue): bool; + + /** + * @param string $rawValue + * + * @return TParsedValue + */ + abstract protected function stringToNumber(string $rawValue); + + /** + * @param string $rawValue + * + * @return TParsedValue + */ + public function parse(string $rawValue) + { + if (!static::isValidFormat($rawValue)) { + throw new ParseException( + 'Not a valid ' . $this->dbgValueTypeDesc() . " value. Raw option value: `''$rawValue'" + ); + } + + $parsedValue = $this->stringToNumber($rawValue); + + if ( + (($this->minValidValue !== null) && ($parsedValue < $this->minValidValue)) + || (($this->maxValidValue !== null) && ($parsedValue > $this->maxValidValue)) + ) { + throw new ParseException( + 'Value is not in range between the valid minimum and maximum values.' + . ' Raw option value: `' . $rawValue . "'." + . ' Parsed option value: ' . $parsedValue . '.' + . ' The valid minimum value: ' . $this->minValidValue . '.' + . ' The valid maximum value: ' . $this->maxValidValue . '.' + ); + } + + return $parsedValue; + } + + /** + * @return ?TParsedValue + */ + public function minValidValue() + { + return $this->minValidValue; + } + + /** + * @return ?TParsedValue + */ + public function maxValidValue() + { + return $this->maxValidValue; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/OptionDefaultValues.php b/tests/ElasticOTelTests/Util/Config/OptionDefaultValues.php new file mode 100644 index 0000000..b45bb2a --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/OptionDefaultValues.php @@ -0,0 +1,40 @@ + + */ + abstract public function parser(): OptionParser; + + /** + * @return TParsedValue|null + */ + abstract public function defaultValue(); +} diff --git a/tests/ElasticOTelTests/Util/Config/OptionNames.php b/tests/ElasticOTelTests/Util/Config/OptionNames.php new file mode 100644 index 0000000..7c9ca7b --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/OptionNames.php @@ -0,0 +1,77 @@ + + */ +abstract class OptionWithDefaultValueMetadata extends OptionMetadata +{ + /** + * @var OptionParser + */ + private $parser; + + /** + * @var TParsedValue + */ + private $defaultValue; + + /** + * @param OptionParser $parser + * @param TParsedValue $defaultValue + */ + public function __construct(OptionParser $parser, $defaultValue) + { + $this->parser = $parser; + $this->defaultValue = $defaultValue; + } + + /** + * @inheritDoc + * + * @return OptionParser + */ + public function parser(): OptionParser + { + return $this->parser; + } + + /** + * @inheritDoc + * + * @return TParsedValue + */ + public function defaultValue() + { + return $this->defaultValue; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/ParseException.php b/tests/ElasticOTelTests/Util/Config/ParseException.php new file mode 100644 index 0000000..1c567c0 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/ParseException.php @@ -0,0 +1,39 @@ +logger = $loggerFactory->loggerForClass(LogCategory::CONFIGURATION, __NAMESPACE__, __CLASS__, __FILE__); + } + + /** + * @param string $rawValue + * @param OptionParser $optionParser + * + * @return mixed + * + * @template T + * @phpstan-param OptionParser $optionParser + * @phpstan-return T + */ + public static function parseOptionRawValue(string $rawValue, OptionParser $optionParser) + { + return $optionParser->parse(trim($rawValue)); + } + + /** + * @param array> $optNameToMeta + * @param RawSnapshotInterface $rawSnapshot + * + * @return array Option name to parsed value + */ + public function parse(array $optNameToMeta, RawSnapshotInterface $rawSnapshot): array + { + $optNameToParsedValue = []; + foreach ($optNameToMeta as $optName => $optMeta) { + $rawValue = $rawSnapshot->valueFor($optName); + if ($rawValue === null) { + $parsedValue = $optMeta->defaultValue(); + + ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log( + "Input raw config snapshot doesn't have a value for the option - using default value", + ['Option name' => $optName, 'Option default value' => $optMeta->defaultValue()] + ); + } else { + try { + $parsedValue = self::parseOptionRawValue($rawValue, $optMeta->parser()); + + ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log( + 'Input raw config snapshot has a value - using parsed value', + ['Option name' => $optName, 'Raw value' => $rawValue, 'Parsed value' => $parsedValue] + ); + } catch (ParseException $ex) { + $parsedValue = $optMeta->defaultValue(); + + ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log( + "Input raw config snapshot has a value but it's invalid - using default value", + [ + 'Option name' => $optName, + 'Option default value' => $optMeta->defaultValue(), + 'Exception' => $ex, + ] + ); + } + } + $optNameToParsedValue[$optName] = $parsedValue; + } + + return $optNameToParsedValue; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/RawSnapshotFromArray.php b/tests/ElasticOTelTests/Util/Config/RawSnapshotFromArray.php new file mode 100644 index 0000000..9844f12 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/RawSnapshotFromArray.php @@ -0,0 +1,50 @@ + */ + private $optNameToRawValue; + + /** + * @param array $optNameToRawValue + */ + public function __construct(array $optNameToRawValue) + { + $this->optNameToRawValue = $optNameToRawValue; + } + + public function valueFor(string $optionName): ?string + { + return ArrayUtil::getValueIfKeyExistsElse($optionName, $this->optNameToRawValue, null); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/RawSnapshotInterface.php b/tests/ElasticOTelTests/Util/Config/RawSnapshotInterface.php new file mode 100644 index 0000000..3b2aa94 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/RawSnapshotInterface.php @@ -0,0 +1,34 @@ +> $optionNameToMeta + * + * @return RawSnapshotInterface + */ + public function currentSnapshot(array $optionNameToMeta): RawSnapshotInterface; +} diff --git a/tests/ElasticOTelTests/Util/Config/Snapshot.php b/tests/ElasticOTelTests/Util/Config/Snapshot.php new file mode 100644 index 0000000..a6aab76 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/Snapshot.php @@ -0,0 +1,417 @@ +/agent/native/ext/ConfigManager.h to add the new option for C part of the agent. + // NOTE: Build C part of the agent after making the changes above and before proceeding to the steps below. + // + // + // 2) Add + // + // public const MY_NEW_OPTION = 'my_new_option'; + // + // to class \Elastic\Apm\Impl\Config\OptionNames + // + // + // 3) Add + // + // OptionNames::MY_NEW_OPTION => new OptionMetadata(), + // + // to class \Elastic\Apm\Impl\Config\AllOptionsMetadata + // + // + // 4) Add + // + // /** @var */ + // private $myNewOption; + // + // to class \Elastic\Apm\Impl\Config\Snapshot + // + // + // 5) Add + // + // public function myNewOption(): + // { + // return $this->myNewOption; + // } + // + // to class \Elastic\Apm\Impl\Config\Snapshot + // + // + // 6) Add + // + // OptionNames::MY_NEW_OPTION => RawToParsedValues, + // + // to return value of buildOptionNameToRawToValue() + // in class \ElasticApmTests\ComponentTests\ConfigSettingTest + // + // + // 7) Optionally add option specific test such as \ElasticApmTests\ComponentTests\ApiKeyTest + // + // + use SnapshotTrait; + use LoggableTrait; + + public const LOG_LEVEL_STDERR_DEFAULT = LogLevel::CRITICAL; + public const LOG_LEVEL_SYSLOG_DEFAULT = LogLevel::INFO; + + /** @var array */ + private $optNameToParsedValue; + + /** @var string */ + private $apiKey; + + /** @var bool */ + private $astProcessEnabled; + + /** @var bool */ + private $astProcessDebugDumpConvertedBackToSource; + + /** @var string */ + private $astProcessDebugDumpForPathPrefix; + + /** @var string */ + private $astProcessDebugDumpOutDir; + + /** @var bool */ + private $asyncBackendComm; + + /** @var bool */ + private $breakdownMetrics; + + /** @var bool */ + private $captureErrors; + + /** @var ?WildcardListMatcher */ + private $devInternal; + + /** @var SnapshotDevInternal */ + private $devInternalParsed; + + /** @var ?WildcardListMatcher */ + private $disableInstrumentations; + + /** @var bool */ + private $disableSend; + + /** @var bool */ + private $enabled; + + /** @var ?string */ + private $environment; + + /** @var ?array */ + private $globalLabels; + + /** @var ?string */ + private $hostname; + + /** @var ?int */ + private $logLevel; + + /** @var ?int */ + private $logLevelStderr; + + /** @var ?int */ + private $logLevelSyslog; + + /** @var int */ + private $nonKeywordStringMaxLength; + + /** @var bool */ + private $profilingInferredSpansEnabled; + + /** @var float */ + private $profilingInferredSpansMinDuration; + + /** @var float */ + private $profilingInferredSpansSamplingInterval; + + /** @var WildcardListMatcher */ + private $sanitizeFieldNames; + + /** @var string */ + private $secretToken; + + /** @var float - In milliseconds */ + private $serverTimeout; + + /** @var ?string */ + private $serviceName; + + /** @var ?string */ + private $serviceNodeName; + + /** @var ?string */ + private $serviceVersion; + + /** @var bool */ + private $spanCompressionEnabled; + + /** @var float */ + private $spanCompressionExactMatchMaxDuration; + + /** @var float */ + private $spanCompressionSameKindMaxDuration; + + /** @var float */ + private $spanStackTraceMinDuration; + + /** @var int */ + private $stackTraceLimit; + + /** @var ?WildcardListMatcher */ + private $transactionIgnoreUrls; + + /** @var int */ + private $transactionMaxSpans; + + /** @var float */ + private $transactionSampleRate; + + /** @var ?WildcardListMatcher */ + private $urlGroups = null; + + /** @var bool */ + private $verifyServerCert; + + /** @var int */ + private $effectiveLogLevel; + + /** + * Snapshot constructor. + * + * @param array $optNameToParsedValue + */ + public function __construct(array $optNameToParsedValue, LoggerFactory $loggerFactory) + { + $this->setPropertiesToValuesFrom($optNameToParsedValue); + + $this->setEffectiveLogLevel(); + $this->adaptTransactionSampleRate(); + + $this->devInternalParsed = new SnapshotDevInternal($this->devInternal, $loggerFactory); + } + + private function setEffectiveLogLevel(): void + { + $this->effectiveLogLevel = max( + ($this->logLevelStderr ?? $this->logLevel) ?? self::LOG_LEVEL_STDERR_DEFAULT, + ($this->logLevelSyslog ?? $this->logLevel) ?? self::LOG_LEVEL_SYSLOG_DEFAULT, + $this->logLevel ?? LogLevel::OFF + ); + } + + private function adaptTransactionSampleRate(): void + { + if ($this->transactionSampleRate === 0.0) { + return; + } + + $minNonZeroValue = 0.0001; + if ($this->transactionSampleRate < $minNonZeroValue) { + $this->transactionSampleRate = $minNonZeroValue; + return; + } + + $this->transactionSampleRate = round( + $this->transactionSampleRate, + 4 /* <- precision - number of decimal digits */ + ); + } + + /** + * @param string $optName + * + * @return mixed + */ + public function parsedValueFor(string $optName) + { + return $this->optNameToParsedValue[$optName]; + } + + public function astProcessEnabled(): bool + { + return $this->astProcessEnabled; + } + + public function breakdownMetrics(): bool + { + return $this->breakdownMetrics; + } + + public function captureErrors(): bool + { + return $this->captureErrors; + } + + public function devInternal(): SnapshotDevInternal + { + return $this->devInternalParsed; + } + + public function disableInstrumentations(): ?WildcardListMatcher + { + return $this->disableInstrumentations; + } + + public function disableSend(): bool + { + return $this->disableSend; + } + + public function effectiveLogLevel(): int + { + return $this->effectiveLogLevel; + } + + public function enabled(): bool + { + return $this->enabled; + } + + public function environment(): ?string + { + return $this->environment; + } + + /** + * @return ?array + */ + public function globalLabels(): ?array + { + return $this->globalLabels; + } + + public function hostname(): ?string + { + return $this->hostname; + } + + public function nonKeywordStringMaxLength(): int + { + return $this->nonKeywordStringMaxLength; + } + + public function profilingInferredSpansEnabled(): bool + { + return $this->profilingInferredSpansEnabled; + } + + public function profilingInferredSpansMinDurationInMilliseconds(): float + { + return $this->profilingInferredSpansMinDuration; + } + + public function profilingInferredSpansSamplingInterval(): float + { + return $this->profilingInferredSpansSamplingInterval; + } + + public function sanitizeFieldNames(): WildcardListMatcher + { + return $this->sanitizeFieldNames; + } + + public function serverTimeout(): float + { + return $this->serverTimeout; + } + + public function serviceName(): ?string + { + return $this->serviceName; + } + + public function serviceNodeName(): ?string + { + return $this->serviceNodeName; + } + + public function serviceVersion(): ?string + { + return $this->serviceVersion; + } + + public function spanCompressionEnabled(): bool + { + return $this->spanCompressionEnabled; + } + + public function spanCompressionExactMatchMaxDuration(): float + { + return $this->spanCompressionExactMatchMaxDuration; + } + + public function spanCompressionSameKindMaxDuration(): float + { + return $this->spanCompressionSameKindMaxDuration; + } + + public function spanStackTraceMinDuration(): float + { + return $this->spanStackTraceMinDuration; + } + + public function stackTraceLimit(): int + { + return $this->stackTraceLimit; + } + + public function transactionIgnoreUrls(): ?WildcardListMatcher + { + return $this->transactionIgnoreUrls; + } + + public function transactionMaxSpans(): int + { + return $this->transactionMaxSpans; + } + + public function transactionSampleRate(): float + { + return $this->transactionSampleRate; + } + + public function urlGroups(): ?WildcardListMatcher + { + return $this->urlGroups; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/SnapshotDevInternal.php b/tests/ElasticOTelTests/Util/Config/SnapshotDevInternal.php new file mode 100644 index 0000000..df4e8d0 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/SnapshotDevInternal.php @@ -0,0 +1,94 @@ +loggerForClass(LogCategory::CONFIGURATION, __NAMESPACE__, __CLASS__, __FILE__); + + foreach (get_object_vars($this) as $propName => $propValue) { + $subOptName = TextUtil::camelToSnakeCase($propName); + $matchedExpr = WildcardListMatcher::matchNullable($devInternal, $subOptName); + if ($matchedExpr === null) { + continue; + } + + ($loggerProxy = $logger->ifInfoLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log( + OptionNames::DEV_INTERNAL . ' sub-option ' . $subOptName . ' is set', + [OptionNames::DEV_INTERNAL . ' configuration' => $devInternal] + ); + + $this->$propName = true; + } + } + + public function dropEventAfterEnd(): bool + { + return $this->dropEventAfterEnd; + } + + public function dropEventsBeforeSendCCode(): bool + { + return $this->dropEventsBeforeSendCCode; + } + + public function gcCollectCyclesAfterEveryTransaction(): bool + { + return $this->gcCollectCyclesAfterEveryTransaction; + } + + public function gcMemCachesAfterEveryTransaction(): bool + { + return $this->gcMemCachesAfterEveryTransaction; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/SnapshotTrait.php b/tests/ElasticOTelTests/Util/Config/SnapshotTrait.php new file mode 100644 index 0000000..7f7640b --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/SnapshotTrait.php @@ -0,0 +1,76 @@ + */ + private $optNameToParsedValue; + + /** + * @param array $optNameToParsedValue + */ + protected function setPropertiesToValuesFrom(array $optNameToParsedValue): void + { + $this->optNameToParsedValue = $optNameToParsedValue; + + foreach ($optNameToParsedValue as $optName => $parsedValue) { + $propertyName = TextUtil::snakeToCamelCase($optName); + $actualClass = get_called_class(); + if (!property_exists($actualClass, $propertyName)) { + throw new RuntimeException("Property `$propertyName' doesn't exist in class " . $actualClass); + } + $this->$propertyName = $parsedValue; + } + + $this->optNameToParsedValue = $optNameToParsedValue; + } + + /** + * @param string $optName + * + * @return mixed + */ + public function getOptionValueByName(string $optName) + { + return ArrayUtil::getValueIfKeyExistsElse($optName, $this->optNameToParsedValue, null); + } + + /** + * @return array + */ + public function getOptionNameToParsedValueMap(): array + { + return $this->optNameToParsedValue; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/StringOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/StringOptionMetadata.php new file mode 100644 index 0000000..c4fc121 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/StringOptionMetadata.php @@ -0,0 +1,41 @@ + + * + * @noinspection PhpUnused + */ +final class StringOptionMetadata extends OptionWithDefaultValueMetadata +{ + public function __construct(string $defaultValue) + { + parent::__construct(new StringOptionParser(), $defaultValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/StringOptionParser.php b/tests/ElasticOTelTests/Util/Config/StringOptionParser.php new file mode 100644 index 0000000..225b773 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/StringOptionParser.php @@ -0,0 +1,39 @@ + + */ +final class StringOptionParser extends OptionParser +{ + public function parse(string $rawValue): string + { + return $rawValue; + } +} diff --git a/tests/ElasticOTelTests/Util/Config/WildcardListOptionMetadata.php b/tests/ElasticOTelTests/Util/Config/WildcardListOptionMetadata.php new file mode 100644 index 0000000..90164e8 --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/WildcardListOptionMetadata.php @@ -0,0 +1,41 @@ + + */ +final class WildcardListOptionMetadata extends OptionWithDefaultValueMetadata +{ + public function __construct(WildcardListMatcher $defaultValue) + { + parent::__construct(new WildcardListOptionParser(), $defaultValue); + } +} diff --git a/tests/ElasticOTelTests/Util/Config/WildcardListOptionParser.php b/tests/ElasticOTelTests/Util/Config/WildcardListOptionParser.php new file mode 100644 index 0000000..448b0ca --- /dev/null +++ b/tests/ElasticOTelTests/Util/Config/WildcardListOptionParser.php @@ -0,0 +1,56 @@ + + */ +final class WildcardListOptionParser extends OptionParser +{ + /** @inheritDoc */ + public function parse(string $rawValue): WildcardListMatcher + { + return self::staticParse($rawValue); + } + + public static function staticParse(string $rawValue): WildcardListMatcher + { + /** + * @return iterable + */ + $splitWildcardExpr = function () use ($rawValue): iterable { + foreach (explode(',', $rawValue) as $listElementRaw) { + yield trim($listElementRaw); + } + }; + + return new WildcardListMatcher($splitWildcardExpr()); + } +} From ad681298bd820aee57d4bc887a451a16559a7aa7 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Mon, 25 Nov 2024 21:18:27 +0200 Subject: [PATCH 02/29] Added static check and GitHub workflow for it --- .github/workflows/test-php-static-check.yml | 45 +++++++++++++++++++ composer.json | 33 +++++--------- phpstan.dist.neon | 11 ++--- prod/php/ElasticOTel/BootstrapStageLogger.php | 40 ++++++++++------- .../HttpTransport/ElasticHttpTransport.php | 32 +++++++++---- .../ElasticHttpTransportFactory.php | 5 +-- .../php/ElasticOTel/InstrumentationBridge.php | 3 +- prod/php/ElasticOTel/Log/ElasticLogWriter.php | 27 ++++++++--- prod/php/ElasticOTel/Log/Level.php | 45 ++++++++++--------- prod/php/ElasticOTel/PhpPartFacade.php | 44 +++++++++++++----- prod/php/ElasticOTel/ProdPhpDir.php | 2 +- .../Util/HiddenConstructorTrait.php | 2 +- .../Util/SingletonInstanceTrait.php | 2 +- .../php/ElasticOTel/Util/StaticClassTrait.php | 2 +- tools/build/configure_php_templates.sh | 10 +++++ tools/build/test_php_static_check.sh | 30 +++++++++++++ 16 files changed, 229 insertions(+), 104 deletions(-) create mode 100644 .github/workflows/test-php-static-check.yml create mode 100644 tools/build/test_php_static_check.sh diff --git a/.github/workflows/test-php-static-check.yml b/.github/workflows/test-php-static-check.yml new file mode 100644 index 0000000..f625b81 --- /dev/null +++ b/.github/workflows/test-php-static-check.yml @@ -0,0 +1,45 @@ +--- +name: test-php-static-check + +on: + pull_request: + paths-ignore: + - "**/*.asciidoc" + - "**/*.md" + - "**/*.png" + push: + branches: + - main + paths-ignore: + - "**/*.asciidoc" + - "**/*.md" + - "**/*.png" + +permissions: + contents: read + +## Concurrency only allowed in the main branch. +## So old builds running for old commits within the same Pull Request are cancelled +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + static-check: + name: static-check + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + php-version: + - "8.1" + - "8.2" + - "8.3" + - "8.4" + env: + PHP_VERSION: ${{ matrix.php-version }} + steps: + - uses: actions/checkout@v4 + - name: Static check + run: ./tools/build/test_php_static_check.sh "${PHP_VERSION}" diff --git a/composer.json b/composer.json index 0032c8e..862f74e 100644 --- a/composer.json +++ b/composer.json @@ -25,12 +25,12 @@ }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.11.4", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpunit/phpunit": "^9.6||^10.5", - "slevomat/coding-standard": "8.14.1", - "squizlabs/php_codesniffer": "3.8.1" + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/phpstan": "2.0.2", + "phpstan/phpstan-phpunit": "^2.0.1", + "phpunit/phpunit": "^10.5", + "slevomat/coding-standard": "8.15.0", + "squizlabs/php_codesniffer": "3.11.1" }, "autoload-dev": { "psr-4": { @@ -61,32 +61,21 @@ "phpcbf ./prod/php/", "phpcbf ./tests/" ], + "fix_code_format_for": [ + "phpcbf" + ], "phpstan-junit-report-for-ci": [ "phpstan analyse --error-format=junit ./prod/php/ --level max --memory-limit=1G | tee build/elastic-otel-phpstan-junit.xml", "phpstan analyse --error-format=junit ./tests/ --level max --memory-limit=1G --error-format=junit | tee build/tests-phpstan-junit.xml" ], "phpstan": [ - "phpstan analyse ./prod/php/ --level max --memory-limit=1G", - "phpstan analyse ./tests/ --level max --memory-limit=1G" + "phpstan analyse ./prod/php/", + "phpstan analyse ./tests/" ], "static_check": [ "composer run-script -- parallel-lint", "composer run-script -- php_codesniffer_check", "composer run-script -- phpstan" - ], - "run_unit_tests": [ - "phpunit" - ], - "run_component_tests": [ - "phpunit" - ], - "run_tests": [ - "composer run-script -- run_unit_tests", - "composer run-script -- run_component_tests" - ], - "static_check_and_run_tests": [ - "composer run-script -- static_check", - "composer run-script -- run_tests" ] } } diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 7a6ed84..9db50a2 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -3,13 +3,8 @@ includes: parameters: bootstrapFiles: - - tests/bootstrap.php + - ./tests/bootstrap.php - reportUnmatchedIgnoredErrors: false - - ignoreErrors: - # - # elastic_otel_* functions are provided by the extension - # - - '#^Function elastic_otel_[a-z_]* not found\.$#' + level: max + reportUnmatchedIgnoredErrors: false diff --git a/prod/php/ElasticOTel/BootstrapStageLogger.php b/prod/php/ElasticOTel/BootstrapStageLogger.php index 7a6ac30..bd09109 100644 --- a/prod/php/ElasticOTel/BootstrapStageLogger.php +++ b/prod/php/ElasticOTel/BootstrapStageLogger.php @@ -25,8 +25,6 @@ use Throwable; -require __DIR__ . DIRECTORY_SEPARATOR . 'Log' . DIRECTORY_SEPARATOR . 'LogFeature.php'; - /** * Code in this file is part of implementation internals, and thus it is not covered by the backward compatibility. * @@ -94,15 +92,18 @@ public static function writeLineToStdErr(string $text): void } } - public static function nullableToLog(mixed $str): mixed + public static function nullableToLog(null|int|string $str): string { - return $str === null ? 'null' : $str; + return $str === null ? 'null' : strval($str); } public static function configure(int $maxEnabledLevel, string $phpSrcCodeRootDir, string $rootNamespace): void { + /** @noinspection PhpIncludeInspection */ + require __DIR__ . DIRECTORY_SEPARATOR . 'Log' . DIRECTORY_SEPARATOR . 'LogFeature.php'; + self::$maxEnabledLevel = $maxEnabledLevel; - if(is_int($pid = getmypid())) { + if (is_int($pid = getmypid())) { self::$pid = $pid; } @@ -116,7 +117,10 @@ public static function configure(int $maxEnabledLevel, string $phpSrcCodeRootDir . '; classNamePrefixToRemove: ' . self::$classNamePrefixToRemove . '; maxEnabledLevel: ' . self::levelToString($maxEnabledLevel) . '; pid: ' . self::nullableToLog(self::$pid), - __FILE__, __LINE__, __CLASS__, __FUNCTION__ + __FILE__, + __LINE__, + __CLASS__, + __FUNCTION__ ); } @@ -179,7 +183,10 @@ public static function logCriticalThrowable(Throwable $throwable, string $messag $message . '.' . ' ' . get_class($throwable) . ': ' . $throwable->getMessage() . PHP_EOL . 'Stack trace:' . PHP_EOL . $throwable->getTraceAsString(), - $file, $line, $class, $func + $file, + $line, + $class, + $func ); } @@ -191,12 +198,12 @@ private static function isPrefixOf(string $prefix, string $text, bool $isCaseSen } return substr_compare( - $text /* <- haystack */, - $prefix /* <- needle */, - 0 /* <- offset */, - $prefixLen /* <- length */, - !$isCaseSensitive /* <- case_insensitivity */ - ) === 0; + $text /* <- haystack */, + $prefix /* <- needle */, + 0 /* <- offset */, + $prefixLen /* <- length */, + !$isCaseSensitive /* <- case_insensitivity */ + ) === 0; } private static function processSourceCodeFilePathForLog(string $file): string @@ -232,13 +239,12 @@ private static function logWithLevel(int $statementLevel, string $message, strin /** * elastic_otel_* functions are provided by the extension * - * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection - * @phpstan-ignore-next-line + * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedClassInspection, PhpUndefinedFunctionInspection */ - \elastic_otel_log_feature( + \elastic_otel_log_feature( // @phpstan-ignore function.notFound 0 /* $isForced */, $statementLevel, - Log\LogFeature::BOOTSTRAP, + Log\LogFeature::BOOTSTRAP, // @phpstan-ignore class.notFound self::LOG_CATEGORY, self::processSourceCodeFilePathForLog($file), $line, diff --git a/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransport.php b/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransport.php index 99f2eb5..d06b42d 100644 --- a/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransport.php +++ b/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransport.php @@ -29,23 +29,26 @@ use OpenTelemetry\SDK\Common\Future\FutureInterface; /** - * @psalm-template CONTENT_TYPE of string - * @template-implements TransportInterface + * @template-implements TransportInterface */ final class ElasticHttpTransport implements TransportInterface { - private string $endpoint; private string $contentType; /** - * @psalm-param CONTENT_TYPE $contentType + * @param array $headers + * + * @noinspection PhpUnusedParameterInspection + * + * Parameters $compression, $cacert, $cert and $key are unused so constructor.unusedParameter is mentioned 4 times below + * @phpstan-ignore constructor.unusedParameter, constructor.unusedParameter, constructor.unusedParameter, constructor.unusedParameter */ public function __construct( string $endpoint, string $contentType, array $headers = [], - $compression = null, + mixed $compression = null, float $timeout = 10., int $retryDelay = 100, int $maxRetries = 3, @@ -56,7 +59,12 @@ public function __construct( $this->endpoint = $endpoint; $this->contentType = $contentType; - initialize($endpoint, $contentType, $headers, $timeout, $retryDelay, $maxRetries); + /** + * \Elastic\OTel\HttpTransport\* functions are provided by the extension + * + * @noinspection PhpUnnecessaryFullyQualifiedNameInspection, PhpUndefinedFunctionInspection + */ + \Elastic\OTel\HttpTransport\initialize($endpoint, $contentType, $headers, $timeout, $retryDelay, $maxRetries); // @phpstan-ignore function.notFound } public function contentType(): string @@ -64,11 +72,19 @@ public function contentType(): string return $this->contentType; } + /** + * @return FutureInterface + */ public function send(string $payload, ?CancellationInterface $cancellation = null): FutureInterface { - enqueue($this->endpoint, $payload); + /** + * \Elastic\OTel\HttpTransport\* functions are provided by the extension + * + * @noinspection PhpUnnecessaryFullyQualifiedNameInspection, PhpUndefinedFunctionInspection + */ + \Elastic\OTel\HttpTransport\enqueue($this->endpoint, $payload); // @phpstan-ignore function.notFound - return new CompletedFuture(null); + return new CompletedFuture($payload); } public function shutdown(?CancellationInterface $cancellation = null): bool diff --git a/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransportFactory.php b/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransportFactory.php index bbe6015..977f1ae 100644 --- a/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransportFactory.php +++ b/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransportFactory.php @@ -23,18 +23,15 @@ namespace Elastic\OTel\HttpTransport; -use Elastic\OTel\HttpTransport\ElasticHttpTransport; use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface; class ElasticHttpTransportFactory implements TransportFactoryInterface { - private const DEFAULT_COMPRESSION = 'none'; - public function create( string $endpoint, string $contentType, array $headers = [], - $compression = null, + mixed $compression = null, float $timeout = 10., int $retryDelay = 100, int $maxRetries = 3, diff --git a/prod/php/ElasticOTel/InstrumentationBridge.php b/prod/php/ElasticOTel/InstrumentationBridge.php index 5ecb7d6..5440a3f 100644 --- a/prod/php/ElasticOTel/InstrumentationBridge.php +++ b/prod/php/ElasticOTel/InstrumentationBridge.php @@ -83,9 +83,8 @@ private static function elasticOTelHook(?string $class, string $function, ?Closu * \elastic_otel_* functions are provided by the extension * * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection - * @phpstan-ignore-next-line */ - $retVal = \elastic_otel_hook($class, $function, $pre, $post); + $retVal = \elastic_otel_hook($class, $function, $pre, $post); // @phpstan-ignore function.notFound if ($retVal) { BootstrapStageLogger::logTrace('Successfully hooked. class: ' . $dbgClassAsString . ', function: ' . $function, __FILE__, __LINE__, __CLASS__, __FUNCTION__); return; diff --git a/prod/php/ElasticOTel/Log/ElasticLogWriter.php b/prod/php/ElasticOTel/Log/ElasticLogWriter.php index 2959764..d2b5a57 100644 --- a/prod/php/ElasticOTel/Log/ElasticLogWriter.php +++ b/prod/php/ElasticOTel/Log/ElasticLogWriter.php @@ -26,15 +26,32 @@ use OpenTelemetry\API\Behavior\Internal\LogWriter\LogWriterInterface; use OpenTelemetry\API\Behavior\Internal\Logging; -class ElasticLogWriter implements LogWriterInterface +class ElasticLogWriter implements LogWriterInterface { - public function write($level, string $message, array $context): void + /** + * @param array $context + */ + public function write(mixed $level, string $message, array $context): void { - \elastic_otel_log_feature(0, Level::getFromPsrLevel($level), LogFeature::OTEL, '', '', 0, $context['source'] ?? '', $message . ' context: ' . var_export($context, true)); + /** + * elastic_otel_* functions are provided by the extension + * + * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedClassInspection, PhpUndefinedFunctionInspection + */ + \elastic_otel_log_feature( // @phpstan-ignore function.notFound + 0 /* isForced */, + Level::getFromPsrLevel(strval($level)) /* level */, // @phpstan-ignore argument.type + LogFeature::OTEL /* feature */, // @phpstan-ignore class.notFound + '' /* category */, + '' /* file */, + 0 /* line */, + $context['source'] ?? '' /* func */, + $message . ' context: ' . var_export($context, true) /* message */ + ); } - public static function enableLogWriter() + public static function enableLogWriter(): void { - Logging::setLogWriter(new ElasticLogWriter()); + Logging::setLogWriter(new self()); } } diff --git a/prod/php/ElasticOTel/Log/Level.php b/prod/php/ElasticOTel/Log/Level.php index de436ab..c435473 100644 --- a/prod/php/ElasticOTel/Log/Level.php +++ b/prod/php/ElasticOTel/Log/Level.php @@ -43,7 +43,7 @@ final class Level public const TRACE = 6; /** @var array */ - private static $nameIntPairs + private static array $nameIntPairs = [ ['OFF', Level::OFF], ['CRITICAL', Level::CRITICAL], @@ -55,16 +55,21 @@ final class Level ]; /** @var ?array */ - private static $intToName = null; + private static ?array $intToName = null; /** * @return array + * + * @noinspection PhpUnused */ public static function nameIntPairs(): array { return self::$nameIntPairs; } + /** + * @noinspection PhpUnused + */ public static function intToName(int $intValueToMap): string { if (self::$intToName === null) { @@ -76,30 +81,26 @@ public static function intToName(int $intValueToMap): string return self::$intToName[$intValueToMap]; } + /** + * @noinspection PhpUnused + */ public static function getHighest(): int { return self::TRACE; } - public static function getFromPsrLevel($level): int + /** + * @noinspection PhpFullyQualifiedNameUsageInspection + */ + public static function getFromPsrLevel(string $level): int { - switch ($level) { - case \Psr\Log\LogLevel::EMERGENCY: - case \Psr\Log\LogLevel::ALERT: - case \Psr\Log\LogLevel::CRITICAL: - return Level::CRITICAL; - case \Psr\Log\LogLevel::ERROR: - return Level::ERROR; - case \Psr\Log\LogLevel::WARNING: - return Level::WARNING; - case \Psr\Log\LogLevel::NOTICE: - case \Psr\Log\LogLevel::INFO: - return Level::INFO; - case \Psr\Log\LogLevel::DEBUG: - return Level::DEBUG; - default: - return Level::OFF; - } + return match ($level) { + \Psr\Log\LogLevel::EMERGENCY, \Psr\Log\LogLevel::ALERT, \Psr\Log\LogLevel::CRITICAL => Level::CRITICAL, + \Psr\Log\LogLevel::ERROR => Level::ERROR, + \Psr\Log\LogLevel::WARNING => Level::WARNING, + \Psr\Log\LogLevel::NOTICE, \Psr\Log\LogLevel::INFO => Level::INFO, + \Psr\Log\LogLevel::DEBUG => Level::DEBUG, + default => Level::OFF, + }; } - -} \ No newline at end of file +} diff --git a/prod/php/ElasticOTel/PhpPartFacade.php b/prod/php/ElasticOTel/PhpPartFacade.php index b28ff21..c3f7153 100644 --- a/prod/php/ElasticOTel/PhpPartFacade.php +++ b/prod/php/ElasticOTel/PhpPartFacade.php @@ -66,16 +66,21 @@ public static function bootstrap(string $elasticOTelNativePartVersion, int $maxE BootstrapStageLogger::logDebug( 'Starting bootstrap sequence...' . "; elasticOTelNativePartVersion: $elasticOTelNativePartVersion" . "; maxEnabledLogLevel: $maxEnabledLogLevel" . "; requestInitStartTime: $requestInitStartTime", - __FILE__, __LINE__, __CLASS__, __FUNCTION__ + __FILE__, + __LINE__, + __CLASS__, + __FUNCTION__ ); self::setElasticOTelVersion($elasticOTelNativePartVersion); 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__ + 'bootstrap() is called even though singleton instance is already created (probably bootstrap() is called more than once)', + __FILE__, + __LINE__, + __CLASS__, + __FUNCTION__ ); return false; } @@ -106,21 +111,23 @@ private static function setElasticOTelVersion(string $nativePartVersion): void require __DIR__ . DIRECTORY_SEPARATOR . 'PhpPartVersion.php'; /** + * @var string $phpPartVersion + * * Constant Elastic\OTel\ELASTIC_OTEL_PHP_VERSION is defined in the generated file prod/php/ElasticOTel/PhpPartVersion.php * * @noinspection PhpUnnecessaryFullyQualifiedNameInspection, PhpUndefinedConstantInspection - * @phpstan-ignore-next-line - * - * @var string $phpPartVersion */ - $phpPartVersion = \Elastic\OTel\ELASTIC_OTEL_PHP_VERSION; + $phpPartVersion = \Elastic\OTel\ELASTIC_OTEL_PHP_VERSION; // @phpstan-ignore constant.notFound if ($nativePartVersion === $phpPartVersion) { self::$elasticOTelVersion = $nativePartVersion; } else { BootstrapStageLogger::logWarning( 'Native part and PHP part versions do not match' . "; nativePartVersion: $nativePartVersion" . "; phpPartVersion: $phpPartVersion", - __FILE__, __LINE__, __CLASS__, __FUNCTION__ + __FILE__, + __LINE__, + __CLASS__, + __FUNCTION__ ); self::$elasticOTelVersion = "$nativePartVersion/$phpPartVersion"; } @@ -130,6 +137,10 @@ private static function isInDevMode(): bool { $modeIsDevEnvVarVal = getenv('ELASTIC_OTEL_PHP_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) { @@ -179,15 +190,21 @@ private static function registerAutoloader(): void private static function registerAsyncTransportFactory(): void { - if (elastic_otel_get_config_option_by_name('async_transport') === false) { + /** + * elastic_otel_* functions are provided by the extension + * + * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection + */ + if (\elastic_otel_get_config_option_by_name('async_transport') === false) { // @phpstan-ignore function.notFound BootstrapStageLogger::logDebug('ELASTIC_OTEL_ASYNC_TRANSPORT set to false', __FILE__, __LINE__, __CLASS__, __FUNCTION__); return; } + /** @noinspection PhpFullyQualifiedNameUsageInspection */ \OpenTelemetry\SDK\Registry::registerTransportFactory('http', ElasticHttpTransportFactory::class, true); } - private static function registerOtelLogWriter() + private static function registerOtelLogWriter(): void { ElasticLogWriter::enableLogWriter(); } @@ -201,7 +218,10 @@ public static function handleError(int $type, string $errorFilename, int $errorL { BootstrapStageLogger::logDebug( "Called with arguments: type: $type, errorFilename: $errorFilename, errorLineno: $errorLineno, message: $message", - __FILE__, __LINE__, __CLASS__, __FUNCTION__ + __FILE__, + __LINE__, + __CLASS__, + __FUNCTION__ ); } diff --git a/prod/php/ElasticOTel/ProdPhpDir.php b/prod/php/ElasticOTel/ProdPhpDir.php index 130b35a..03400d0 100644 --- a/prod/php/ElasticOTel/ProdPhpDir.php +++ b/prod/php/ElasticOTel/ProdPhpDir.php @@ -27,4 +27,4 @@ final class ProdPhpDir { /** @var string */ public static $fullPath; -} \ No newline at end of file +} diff --git a/prod/php/ElasticOTel/Util/HiddenConstructorTrait.php b/prod/php/ElasticOTel/Util/HiddenConstructorTrait.php index 7d42403..c2aa7e2 100644 --- a/prod/php/ElasticOTel/Util/HiddenConstructorTrait.php +++ b/prod/php/ElasticOTel/Util/HiddenConstructorTrait.php @@ -37,4 +37,4 @@ trait HiddenConstructorTrait private function __construct() { } -} \ No newline at end of file +} diff --git a/prod/php/ElasticOTel/Util/SingletonInstanceTrait.php b/prod/php/ElasticOTel/Util/SingletonInstanceTrait.php index 4eadc94..9f2eecc 100644 --- a/prod/php/ElasticOTel/Util/SingletonInstanceTrait.php +++ b/prod/php/ElasticOTel/Util/SingletonInstanceTrait.php @@ -44,4 +44,4 @@ public static function singletonInstance(): self } return self::$singletonInstance; } -} \ No newline at end of file +} diff --git a/prod/php/ElasticOTel/Util/StaticClassTrait.php b/prod/php/ElasticOTel/Util/StaticClassTrait.php index efb9c66..69713d8 100644 --- a/prod/php/ElasticOTel/Util/StaticClassTrait.php +++ b/prod/php/ElasticOTel/Util/StaticClassTrait.php @@ -34,4 +34,4 @@ trait StaticClassTrait * Constructor is hidden because it's a "static" class */ use HiddenConstructorTrait; -} \ No newline at end of file +} diff --git a/tools/build/configure_php_templates.sh b/tools/build/configure_php_templates.sh index 313703c..9511dbe 100755 --- a/tools/build/configure_php_templates.sh +++ b/tools/build/configure_php_templates.sh @@ -99,6 +99,16 @@ function configure_from_template() { echo "Configuring file ${_OUTPUT_FILE} from ${_ARG_INPUT_FILE}" configure_file "${_ARG_INPUT_FILE}" >"${_OUTPUT_FILE}" + + # phpcbf exit code is 1 even when all formatting issues were fixed + set +e + composer run-script -- fix_code_format_for "${_OUTPUT_FILE}" + local exit_code=$? + set -e + if [[ ${exit_code} -ne 0 ]] && [[ ${exit_code} -ne 1 ]]; then + echo "Attempt to fix code format for ${_OUTPUT_FILE} exited with code ${exit_code}" >&2 + exit 1 + fi } # Transform values read from properties before templating diff --git a/tools/build/test_php_static_check.sh b/tools/build/test_php_static_check.sh new file mode 100644 index 0000000..348dc3f --- /dev/null +++ b/tools/build/test_php_static_check.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -xe -o pipefail + +show_help() { + echo "Usage: $0 " + echo + echo "Arguments:" + echo " Required. PHP version in the format ., for example 8.4" + echo + echo "Example:" + echo " $0 8.4" +} + +main() { + local PHP_VERSION="$1" + + if [[ -z "${PHP_VERSION}" ]]; then + echo "Error: Missing required argument " + show_help + exit 1 + fi + + docker run --rm -v "${PWD}:/app" -w /app "php:${PHP_VERSION}-cli" sh -c "\ + apt-get update && apt-get install -y unzip \ + && curl -sS https://getcomposer.org/installer | php -- --filename=composer --install-dir=/usr/local/bin \ + && composer --ignore-platform-req=ext-opentelemetry --ignore-platform-req=php install \ + && composer run-script -- static_check" +} + +main "$@" From 154f62b04481ca683cfec72f83ff4636de064f21 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Mon, 25 Nov 2024 21:23:42 +0200 Subject: [PATCH 03/29] Marked test_php_static_check.sh as executable --- tools/build/test_php_static_check.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tools/build/test_php_static_check.sh diff --git a/tools/build/test_php_static_check.sh b/tools/build/test_php_static_check.sh old mode 100644 new mode 100755 From 0919701e8c4bcc75380ca4e1363fd1e89041db60 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Mon, 25 Nov 2024 21:31:35 +0200 Subject: [PATCH 04/29] Give human readable names to stages in test-php-static-check.yml --- .github/workflows/test-php-static-check.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-php-static-check.yml b/.github/workflows/test-php-static-check.yml index f625b81..585a7d0 100644 --- a/.github/workflows/test-php-static-check.yml +++ b/.github/workflows/test-php-static-check.yml @@ -1,5 +1,5 @@ --- -name: test-php-static-check +name: PHP code static check on: pull_request: @@ -26,7 +26,7 @@ concurrency: jobs: static-check: - name: static-check + name: Check code format and static analysis runs-on: ubuntu-latest timeout-minutes: 30 strategy: @@ -41,5 +41,5 @@ jobs: PHP_VERSION: ${{ matrix.php-version }} steps: - uses: actions/checkout@v4 - - name: Static check + - name: Check code format and static analysis run: ./tools/build/test_php_static_check.sh "${PHP_VERSION}" From 3168b14866743cb99ec08eb7c7a7bcb0935688ad Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Tue, 26 Nov 2024 10:45:03 +0200 Subject: [PATCH 05/29] Revert back to `return new CompletedFuture(null)` in `ElasticHttpTransport::send` --- prod/php/ElasticOTel/HttpTransport/ElasticHttpTransport.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransport.php b/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransport.php index d06b42d..e28cb43 100644 --- a/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransport.php +++ b/prod/php/ElasticOTel/HttpTransport/ElasticHttpTransport.php @@ -73,7 +73,7 @@ public function contentType(): string } /** - * @return FutureInterface + * @return FutureInterface */ public function send(string $payload, ?CancellationInterface $cancellation = null): FutureInterface { @@ -84,7 +84,7 @@ public function send(string $payload, ?CancellationInterface $cancellation = nul */ \Elastic\OTel\HttpTransport\enqueue($this->endpoint, $payload); // @phpstan-ignore function.notFound - return new CompletedFuture($payload); + return new CompletedFuture(null); } public function shutdown(?CancellationInterface $cancellation = null): bool From 84ba309dcd68b001c76feba7f6cc84bdbd784cfa Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Tue, 26 Nov 2024 15:05:42 +0200 Subject: [PATCH 06/29] Fixed issues found by static analysis in the changes from main --- .gitignore | 2 +- composer.json | 3 +- phpcs.xml.dist | 3 + .../ElasticOTel/ElasticOTelPhpPartVersion.php | 49 +++++++++++++++ .../ElasticHttpTransportFactory.php | 3 +- prod/php/ElasticOTel/PhpPartFacade.php | 57 +++++++---------- .../ElasticOTel/Traces/ElasticRootSpan.php | 61 ++++++++++++++----- prod/php/ElasticOTel/Util/ArrayUtil.php | 44 +++++++++++++ prod/php/ElasticOTel/Util/TextUtil.php | 3 + tools/build/build_php_deps.sh | 4 +- 10 files changed, 173 insertions(+), 56 deletions(-) create mode 100644 prod/php/ElasticOTel/ElasticOTelPhpPartVersion.php create mode 100644 prod/php/ElasticOTel/Util/ArrayUtil.php diff --git a/.gitignore b/.gitignore index d086643..0dcf066 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -#generated version file +# Generated files prod/php/ElasticOTel/PhpPartVersion.php prod/php/ElasticOTel/Log/LogFeature.php diff --git a/composer.json b/composer.json index 62b1ef2..b82bb55 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "php-http/discovery": true + "php-http/discovery": true, + "tbachert/spi": true } }, "scripts": { diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 457bc91..1e7b114 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -25,4 +25,7 @@ + + */prod/php/ElasticOTel/Log/LogFeature.php + */prod/php/ElasticOTel/PhpPartVersion.php diff --git a/prod/php/ElasticOTel/ElasticOTelPhpPartVersion.php b/prod/php/ElasticOTel/ElasticOTelPhpPartVersion.php new file mode 100644 index 0000000..641d82f --- /dev/null +++ b/prod/php/ElasticOTel/ElasticOTelPhpPartVersion.php @@ -0,0 +1,49 @@ +bootstrap(); - self::prepareEnvForOTelSdk(); + self::prepareEnvForOTelSdk($elasticOTelNativePartVersion); self::registerAutoloader(); self::registerAsyncTransportFactory(); self::registerOtelLogWriter(); + /** @noinspection PhpInternalEntityUsedInspection */ if (SdkAutoloader::isExcludedUrl()) { BootstrapStageLogger::logDebug('Url is excluded', __FILE__, __LINE__, __CLASS__, __FUNCTION__); return false; @@ -114,32 +110,20 @@ public static function bootstrap(string $elasticOTelNativePartVersion, int $maxE return true; } - private static function setElasticOTelVersion(string $nativePartVersion): void + private static function buildElasticOTelVersion(string $nativePartVersion): string { - /** @noinspection PhpIncludeInspection */ - require __DIR__ . DIRECTORY_SEPARATOR . 'PhpPartVersion.php'; - - /** - * @var string $phpPartVersion - * - * Constant Elastic\OTel\ELASTIC_OTEL_PHP_VERSION is defined in the generated file prod/php/ElasticOTel/PhpPartVersion.php - * - * @noinspection PhpUnnecessaryFullyQualifiedNameInspection, PhpUndefinedConstantInspection - */ - $phpPartVersion = \Elastic\OTel\ELASTIC_OTEL_PHP_VERSION; // @phpstan-ignore constant.notFound - - if ($nativePartVersion === $phpPartVersion) { - self::$elasticOTelVersion = $nativePartVersion; - } else { - BootstrapStageLogger::logWarning( - 'Native part and PHP part versions do not match' . "; nativePartVersion: $nativePartVersion" . "; phpPartVersion: $phpPartVersion", - __FILE__, - __LINE__, - __CLASS__, - __FUNCTION__ - ); - self::$elasticOTelVersion = "$nativePartVersion/$phpPartVersion"; + if ($nativePartVersion === ($phpPartVersion = ElasticOTelPhpPartVersion::get())) { + return $nativePartVersion; } + + BootstrapStageLogger::logWarning( + 'Native part and PHP part versions do not match' . "; nativePartVersion: $nativePartVersion" . "; phpPartVersion: $phpPartVersion", + __FILE__, + __LINE__, + __CLASS__, + __FUNCTION__ + ); + return "$nativePartVersion/$phpPartVersion"; } private static function isInDevMode(): bool @@ -160,14 +144,15 @@ private static function isInDevMode(): bool return false; } - private static function prepareEnvForOTelAttributes(): void + private static function prepareEnvForOTelAttributes(string $elasticOTelNativePartVersion): void { - // https://opentelemetry.io/docs/specs/semconv/resource/#telemetry-distribution-experimental - $envVarName = 'OTEL_RESOURCE_ATTRIBUTES'; $envVarValueOnEntry = getenv($envVarName); $envVarValue = (is_string($envVarValueOnEntry) && strlen($envVarValueOnEntry) !== 0) ? ($envVarValueOnEntry . ',') : ''; - $envVarValue .= 'telemetry.distro.name=elastic,telemetry.distro.version=' . self::$elasticOTelVersion; + + // https://opentelemetry.io/docs/specs/semconv/resource/#telemetry-distribution-experimental + $envVarValue .= 'telemetry.distro.name=elastic,telemetry.distro.version=' . self::buildElasticOTelVersion($elasticOTelNativePartVersion); + self::setEnvVar($envVarName, $envVarValue); } @@ -178,10 +163,10 @@ private static function setEnvVar(string $envVarName, string $envVarValue): void } } - private static function prepareEnvForOTelSdk(): void + private static function prepareEnvForOTelSdk(string $elasticOTelNativePartVersion): void { self::setEnvVar('OTEL_PHP_AUTOLOAD_ENABLED', 'true'); - self::prepareEnvForOTelAttributes(); + self::prepareEnvForOTelAttributes($elasticOTelNativePartVersion); } private static function registerAutoloader(): void diff --git a/prod/php/ElasticOTel/Traces/ElasticRootSpan.php b/prod/php/ElasticOTel/Traces/ElasticRootSpan.php index 60348a6..56b3a2f 100644 --- a/prod/php/ElasticOTel/Traces/ElasticRootSpan.php +++ b/prod/php/ElasticOTel/Traces/ElasticRootSpan.php @@ -23,6 +23,8 @@ namespace Elastic\OTel\Traces; +use Elastic\OTel\Util\ArrayUtil; +use Elastic\OTel\Util\TextUtil; use Http\Discovery\Exception\NotFoundException; use Http\Discovery\Psr17FactoryDiscovery; use Nyholm\Psr7Server\ServerRequestCreator; @@ -42,14 +44,16 @@ class ElasticRootSpan { use LogsMessagesTrait; - public static function startRootSpan() + private const DEFAULT_SPAN_NAME_FOR_SCRIPT = '