agent/php/ElasticApm/Impl/Log/LoggableToJsonEncodable.php (242 lines of code) (raw):

<?php /* * 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. */ declare(strict_types=1); namespace Elastic\Apm\Impl\Log; use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\DbgUtil; use Elastic\Apm\Impl\Util\StaticClassTrait; use Elastic\Apm\Impl\Util\TextUtil; use ReflectionClass; use ReflectionException; use Throwable; /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ final class LoggableToJsonEncodable { use StaticClassTrait; public const MAX_DEPTH_IN_PROD_MODE = 10; /** @var int */ public static $maxDepth = self::MAX_DEPTH_IN_PROD_MODE; private const IS_DTO_OBJECT_CACHE_MAX_COUNT_LOW_WATER_MARK = 10000; private const IS_DTO_OBJECT_CACHE_MAX_COUNT_HIGH_WATER_MARK = 2 * self::IS_DTO_OBJECT_CACHE_MAX_COUNT_LOW_WATER_MARK; /** @var array<string, bool> */ private static $isDtoObjectCache = []; /** * @param array<string, mixed> $value * @param int $depth * * @return array<string, mixed> */ public static function convertArrayForMaxDepth(array $value, int $depth): array { return [LogConsts::MAX_DEPTH_REACHED => $depth, LogConsts::TYPE_KEY => DbgUtil::getType($value), LogConsts::ARRAY_COUNT_KEY => count($value)]; } /** * @param object $value * @param int $depth * * @return array<string, mixed> */ public static function convertObjectForMaxDepth(object $value, int $depth): array { return [LogConsts::MAX_DEPTH_REACHED => $depth, LogConsts::TYPE_KEY => DbgUtil::getType($value)]; } /** * @param mixed $value * * @return mixed */ public static function convert($value, int $depth) { if ($value === null) { return null; } // Scalar variables are those containing an int, float, string or bool. // Types array, object and resource are not scalar. if (is_scalar($value)) { return $value; } if (is_array($value)) { if ($depth >= self::$maxDepth) { return self::convertArrayForMaxDepth($value, $depth); } return self::convertArray($value, $depth + 1); } if (is_resource($value)) { return self::convertOpenResource($value); } if (is_object($value)) { if ($depth >= self::$maxDepth) { return self::convertObjectForMaxDepth($value, $depth); } return self::convertObject($value, $depth); } /** @phpstan-ignore-next-line */ return [LogConsts::TYPE_KEY => DbgUtil::getType($value), LogConsts::VALUE_AS_STRING_KEY => strval($value)]; } /** * @param array<mixed> $array * * @return array<mixed> */ private static function convertArray(array $array, int $depth): array { return self::convertArrayImpl($array, ArrayUtil::isList($array), $depth); } /** * @param array<mixed> $array * @param bool $isListArray * @param int $depth * * @return array<mixed> */ private static function convertArrayImpl(array $array, bool $isListArray, int $depth): array { $arrayCount = count($array); $smallArrayMaxCount = $isListArray ? LogConsts::SMALL_LIST_ARRAY_MAX_COUNT : LogConsts::SMALL_MAP_ARRAY_MAX_COUNT; if ($arrayCount <= $smallArrayMaxCount) { return self::convertSmallArray($array, $isListArray, $depth); } $result = [LogConsts::TYPE_KEY => LogConsts::LIST_ARRAY_TYPE_VALUE]; $result[LogConsts::ARRAY_COUNT_KEY] = $arrayCount; $halfOfSmallArrayMaxCount = intdiv($smallArrayMaxCount, 2); $firstElements = array_slice($array, 0, $halfOfSmallArrayMaxCount); $result['0-' . intdiv($smallArrayMaxCount, 2)] = self::convertSmallArray($firstElements, $isListArray, $depth); $result[($arrayCount - $halfOfSmallArrayMaxCount) . '-' . $arrayCount] = self::convertSmallArray(array_slice($array, -$halfOfSmallArrayMaxCount), $isListArray, $depth); return $result; } /** * @param array<mixed> $array * @param bool $isListArray * @param int $depth * * @return array<mixed> */ private static function convertSmallArray(array $array, bool $isListArray, int $depth): array { return $isListArray ? self::convertSmallListArray($array, $depth) : self::convertSmallMapArray($array, $depth); } /** * @param array<mixed> $listArray * * @return array<mixed> */ private static function convertSmallListArray(array $listArray, int $depth): array { $result = []; foreach ($listArray as $value) { $result[] = self::convert($value, $depth); } return $result; } /** * @param array<mixed> $mapArrayValue * * @return array<mixed> */ private static function convertSmallMapArray(array $mapArrayValue, int $depth): array { return self::isStringKeysMapArray($mapArrayValue) ? self::convertSmallStringKeysMapArray($mapArrayValue, $depth) : self::convertSmallMixedKeysMapArray($mapArrayValue, $depth); } /** * @param array<mixed> $mapArrayValue * * @return bool */ private static function isStringKeysMapArray(array $mapArrayValue): bool { foreach ($mapArrayValue as $key => $_) { if (!is_string($key)) { return false; } } return true; } /** * @param array<mixed> $mapArrayValue * * @return array<mixed> */ private static function convertSmallStringKeysMapArray(array $mapArrayValue, int $depth): array { $result = []; foreach ($mapArrayValue as $key => $value) { $result[$key] = self::convert($value, $depth); } return $result; } /** * @param array<mixed> $mapArrayValue * @param int $depth * * @return array<mixed> */ private static function convertSmallMixedKeysMapArray(array $mapArrayValue, int $depth): array { $result = []; foreach ($mapArrayValue as $key => $value) { $result[] = ['key' => self::convert($key, $depth), 'value' => self::convert($value, $depth)]; } return $result; } /** * @param resource $resource * * @return array<string, mixed> */ private static function convertOpenResource($resource): array { return [ LogConsts::TYPE_KEY => LogConsts::RESOURCE_TYPE_VALUE, LogConsts::RESOURCE_TYPE_KEY => get_resource_type($resource), LogConsts::RESOURCE_ID_KEY => intval($resource), ]; } /** * @param object $object * @param int $depth * * @return mixed */ private static function convertObject(object $object, int $depth) { if ($object instanceof LoggableInterface) { return self::convertLoggable($object, $depth); } if ($object instanceof Throwable) { return self::convertThrowable($object, $depth); } $fqClassName = get_class($object); $isFromElasticNamespace = TextUtil::isPrefixOf('Elastic\\Apm\\', $fqClassName) || TextUtil::isPrefixOf('ElasticApmTests\\', $fqClassName); if ($isFromElasticNamespace && self::isDtoObject($object)) { return self::convertDtoObject($object, $depth); } if (method_exists($object, '__debugInfo')) { return [ LogConsts::TYPE_KEY => get_class($object), LogConsts::VALUE_AS_DEBUG_INFO_KEY => self::convert($object->__debugInfo(), $depth), ]; } if (method_exists($object, '__toString')) { return [ LogConsts::TYPE_KEY => get_class($object), LogConsts::VALUE_AS_STRING_KEY => self::convert($object->__toString(), $depth), ]; } return [ LogConsts::TYPE_KEY => get_class($object), LogConsts::OBJECT_ID_KEY => spl_object_id($object), LogConsts::OBJECT_HASH_KEY => spl_object_hash($object), ]; } /** * @param LoggableInterface $loggable * @param int $depth * * @return mixed */ private static function convertLoggable(LoggableInterface $loggable, int $depth) { $logStream = new LogStream(); $loggable->toLog($logStream); return self::convert($logStream->value, $depth); } /** * @param Throwable $throwable * @param int $depth * * @return array<string, mixed> */ private static function convertThrowable(Throwable $throwable, int $depth): array { return [ LogConsts::TYPE_KEY => get_class($throwable), LogConsts::VALUE_AS_STRING_KEY => self::convert($throwable->__toString(), $depth), ]; } /** * @param object $object * @param int $depth * * @return string|array<string, mixed> * @phpstan-return array<string, mixed> */ private static function convertDtoObject(object $object, int $depth) { $class = get_class($object); try { $currentClass = new ReflectionClass($class); /** @phpstan-ignore-next-line */ } catch (ReflectionException $ex) { return LoggingSubsystem::onInternalFailure('Failed to reflect', ['class' => $class], $ex); } $nameToValue = []; while (true) { foreach ($currentClass->getProperties() as $reflectionProperty) { if ($reflectionProperty->isStatic()) { continue; } $propName = $reflectionProperty->name; $propValue = $reflectionProperty->getValue($object); $nameToValue[$propName] = self::convert($propValue, $depth); } $currentClass = $currentClass->getParentClass(); if ($currentClass === false) { break; } } return $nameToValue; } private static function isDtoObject(object $object): bool { $class = get_class($object); $valueInCache = ArrayUtil::getValueIfKeyExistsElse($class, self::$isDtoObjectCache, null); if ($valueInCache !== null) { return $valueInCache; } $value = self::detectIfDtoObject($class); self::addToIsDtoObjectCache($class, $value); return $value; } /** * @template T of object * * @param class-string<T> $className * * @return bool */ private static function detectIfDtoObject(string $className): bool { try { $currentClass = new ReflectionClass($className); /** @phpstan-ignore-next-line */ } catch (ReflectionException $ex) { LoggingSubsystem::onInternalFailure('Failed to reflect', ['className' => $className], $ex); return false; } while (true) { foreach ($currentClass->getProperties() as $reflectionProperty) { if ($reflectionProperty->isStatic()) { continue; } if (!$reflectionProperty->isPublic()) { return false; } } $currentClass = $currentClass->getParentClass(); if ($currentClass === false) { break; } } return true; } private static function addToIsDtoObjectCache(string $class, bool $value): void { $isDtoObjectCacheCount = count(self::$isDtoObjectCache); if ($isDtoObjectCacheCount >= self::IS_DTO_OBJECT_CACHE_MAX_COUNT_HIGH_WATER_MARK) { self::$isDtoObjectCache = array_slice( self::$isDtoObjectCache, $isDtoObjectCacheCount - self::IS_DTO_OBJECT_CACHE_MAX_COUNT_LOW_WATER_MARK ); } self::$isDtoObjectCache[$class] = $value; } }