agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php (541 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\AutoInstrument;
use Elastic\Apm\Impl\Config\DevInternalSubOptionNames;
use Elastic\Apm\Impl\Config\OptionNames;
use Elastic\Apm\Impl\Config\Snapshot as ConfigSnapshot;
use Elastic\Apm\Impl\Constants;
use Elastic\Apm\Impl\HttpDistributedTracing;
use Elastic\Apm\Impl\InferredSpansManager;
use Elastic\Apm\Impl\Log\LogCategory;
use Elastic\Apm\Impl\Log\Logger;
use Elastic\Apm\Impl\Span;
use Elastic\Apm\Impl\Tracer;
use Elastic\Apm\Impl\Transaction;
use Elastic\Apm\Impl\Util\ArrayUtil;
use Elastic\Apm\Impl\Util\DbgUtil;
use Elastic\Apm\Impl\Util\TextUtil;
use Elastic\Apm\Impl\Util\TimeUtil;
use Elastic\Apm\Impl\Util\UrlParts;
use Elastic\Apm\Impl\Util\UrlUtil;
use Elastic\Apm\Impl\Util\WildcardListMatcher;
use Elastic\Apm\TransactionInterface;
use Throwable;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*/
final class TransactionForExtensionRequest
{
private const DEFAULT_NAME = 'Unnamed transaction';
private const LARAVEL_ARTISAN_COMMAND_SCRIPT = 'artisan';
/** @var Tracer */
private $tracer;
/** @var Logger */
private $logger;
/** @var ?string */
private $httpMethod = null;
/** @var ?string */
private $fullUrl = null;
/** @var ?UrlParts */
private $urlParts = null;
/** @var ?TransactionInterface */
private $transactionForRequest;
/** @var ?Throwable */
private $lastThrown = null;
/** @var ?InferredSpansManager */
private $inferredSpansManager = null;
public function __construct(Tracer $tracer, float $requestInitStartTime)
{
$this->tracer = $tracer;
$this->logger = $tracer->loggerFactory()
->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__)
->addContext('this', $this);
$this->transactionForRequest = $this->beginTransaction($requestInitStartTime);
if ($this->transactionForRequest instanceof Transaction && $this->transactionForRequest->isSampled()) {
$this->inferredSpansManager = new InferredSpansManager($tracer);
}
$this->tracer->onNewCurrentTransactionHasBegun->add(
function (Transaction $transaction): void {
PhpPartFacade::ensureHaveLatestDataDeferredByExtension();
$transaction->onAboutToEnd->add(
function (/** @noinspection PhpUnusedParameterInspection */ Transaction $ignored): void {
PhpPartFacade::ensureHaveLatestDataDeferredByExtension();
}
);
$transaction->onCurrentSpanChanged->add(
function (?Span $span): void {
PhpPartFacade::ensureHaveLatestDataDeferredByExtension();
if ($span !== null) {
$span->onAboutToEnd->add(
function (/** @noinspection PhpUnusedParameterInspection */ Span $ignored): void {
PhpPartFacade::ensureHaveLatestDataDeferredByExtension();
}
);
}
}
);
}
);
}
public function getConfig(): ConfigSnapshot
{
return $this->tracer->getConfig();
}
private function beginTransaction(float $requestInitStartTime): ?TransactionInterface
{
if (!self::isCliScript()) {
if (!$this->discoverHttpRequestData()) {
return null;
}
}
$name = self::isCliScript() ? $this->discoverCliName() : $this->discoverHttpName();
$type = self::isCliScript() ? Constants::TRANSACTION_TYPE_CLI : Constants::TRANSACTION_TYPE_REQUEST;
$timestamp = $this->discoverStartTime($requestInitStartTime);
$distributedTracingHeaders = $this->getDistributedTracingHeaders();
$distributedTracingHeaderExtractor = function (string $headerName) use ($distributedTracingHeaders): ?string {
return ArrayUtil::getValueIfKeyExistsElse($headerName, $distributedTracingHeaders, null);
};
$tx = $this->tracer->newTransaction($name, $type)
->asCurrent()
->timestamp($timestamp)
->distributedTracingHeaderExtractor($distributedTracingHeaderExtractor)
->begin();
if (!self::isCliScript() && !$tx->isNoop()) {
$this->setTxPropsBasedOnHttpRequestData($tx);
}
return $tx;
}
private static function isGlobalServerVarSet(): bool
{
/**
* Sometimes $_SERVER is not set. It seems related to auto_globals_jit
* but it's not easily reproducible even with auto_globals_jit=On
* See also https://bugs.php.net/bug.php?id=69081
*
* Disable PHPStan complaining:
* Variable $_SERVER in isset() always exists and is not nullable.
*
* @noinspection PhpConditionCheckedByNextConditionInspection
*
* @phpstan-ignore-next-line
*/
return isset($_SERVER) && !empty($_SERVER);
}
private function discoverHttpRequestData(): bool
{
/** @phpstan-ignore-next-line */
if (!self::isGlobalServerVarSet()) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('$_SERVER variable is not populated - forcing PHP engine to populate it...');
/** @phpstan-ignore-next-line */
if (!self::isGlobalServerVarSet()) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'$_SERVER variable is not populated even after forcing PHP engine to populate it'
. ' - agent will have to fallback on defaults'
);
return true;
}
}
/** @var ?string $urlPath */
$urlPath = null;
/** @var ?string $urlQuery */
$urlQuery = null;
$pathQuery = $this->getMandatoryServerVarStringElement('REQUEST_URI');
if (is_string($pathQuery)) {
UrlUtil::splitPathQuery($pathQuery, /* ref */ $urlPath, /* ref */ $urlQuery);
if ($urlPath === null) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Failed to extract path part from $_SERVER["REQUEST_URI"]',
['$_SERVER["REQUEST_URI"]' => $pathQuery]
);
} else {
if ($this->shouldHttpTransactionBeIgnored($urlPath)) {
return false;
}
}
}
$this->httpMethod = $this->getMandatoryServerVarStringElement('REQUEST_METHOD');
$this->urlParts = new UrlParts();
$this->urlParts->path = $urlPath;
$this->urlParts->query = $urlQuery;
$serverHttps = self::getOptionalServerVarElement('HTTPS');
$this->urlParts->scheme = !empty($serverHttps) ? 'https' : 'http';
$hostPort = $this->getMandatoryServerVarStringElement('HTTP_HOST');
if ($hostPort !== null) {
UrlUtil::splitHostPort($hostPort, /* ref */ $this->urlParts->host, /* ref */ $this->urlParts->port);
if ($this->urlParts->host === null) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Failed to extract host part from $_SERVER["HTTP_HOST"]',
['$_SERVER["HTTP_HOST"]' => $hostPort]
);
}
}
$queryString = self::getOptionalServerVarElement('QUERY_STRING');
if (is_string($queryString)) {
$this->urlParts->query = $queryString;
}
$this->fullUrl = self::buildFullUrl($this->urlParts->scheme, $hostPort, $pathQuery);
return true;
}
private function shouldHttpTransactionBeIgnored(string $urlPath): bool
{
$ignoreMatcher = $this->tracer->getConfig()->transactionIgnoreUrls();
$matchedIgnoreExpr = WildcardListMatcher::matchNullable($ignoreMatcher, $urlPath);
if ($matchedIgnoreExpr !== null) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Transaction is ignored because its URL path matched ' . OptionNames::TRANSACTION_IGNORE_URLS
. ' configuration',
[
'urlPath' => $urlPath,
'matched ignore expression' => $matchedIgnoreExpr,
OptionNames::TRANSACTION_IGNORE_URLS . ' configuration' => $ignoreMatcher,
]
);
return true;
}
return false;
}
private static function buildFullUrl(?string $scheme, ?string $hostPort, ?string $pathQuery): ?string
{
if ($hostPort === null) {
return null;
}
$fullUrl = '';
if ($scheme !== null) {
$fullUrl .= $scheme . '://';
}
$fullUrl .= $hostPort;
if ($pathQuery !== null) {
$fullUrl .= $pathQuery;
}
return $fullUrl;
}
private function setTxPropsBasedOnHttpRequestData(TransactionInterface $tx): void
{
if ($this->httpMethod !== null) {
$tx->context()->request()->setMethod($this->httpMethod);
}
if ($this->urlParts !== null) {
if ($this->urlParts->host !== null) {
$tx->context()->request()->url()->setDomain($this->urlParts->host);
}
if ($this->urlParts->path !== null) {
$tx->context()->request()->url()->setPath($this->urlParts->path);
}
if ($this->urlParts->port !== null) {
$tx->context()->request()->url()->setPort($this->urlParts->port);
}
if ($this->urlParts->scheme !== null) {
$tx->context()->request()->url()->setProtocol($this->urlParts->scheme);
}
if ($this->urlParts->query !== null) {
$tx->context()->request()->url()->setQuery($this->urlParts->query);
}
}
if ($this->fullUrl !== null) {
$tx->context()->request()->url()->setFull($this->fullUrl);
$tx->context()->request()->url()->setOriginal($this->fullUrl);
}
}
private function beforeHttpEnd(TransactionInterface $tx): void
{
if ($tx->getResult() === null) {
$this->discoverHttpResult($tx);
}
if ($tx->getOutcome() === null) {
$this->discoverHttpOutcome($tx);
}
if ($tx->getOutcome() === Constants::OUTCOME_FAILURE && $this->lastThrown !== null) {
$this->tracer->createErrorFromThrowable($this->lastThrown);
}
}
private function logGcStatus(): void
{
if (!function_exists('gc_status')) {
return;
}
/** @phpstan-ignore-next-line */
$gcStatusRetVal = gc_status();
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Called gc_status()', ['gc_status() return value' => $gcStatusRetVal]);
}
public function onPhpError(PhpErrorData $phpErrorData): void
{
$relatedThrowable = null;
if (
$this->lastThrown !== null
&& $phpErrorData->message !== null
&& TextUtil::isPrefixOf('Uncaught Exception: ', $phpErrorData->message, /* isCaseSensitive: */ false)
) {
$relatedThrowable = $this->lastThrown;
$this->lastThrown = null;
}
$this->tracer->onPhpError($phpErrorData, $relatedThrowable, /* numberOfStackFramesToSkip */ 1);
}
/**
* @param mixed $lastThrown
*
* @return void
*/
public function setLastThrown($lastThrown): void
{
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered', ['lastThrown' => $lastThrown]);
if (!($lastThrown instanceof Throwable)) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'lastThrown is not an instance of Throwable - ignoring it...',
['lastThrown' => $lastThrown]
);
return;
}
$this->lastThrown = $lastThrown;
}
public function onShutdown(): void
{
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered');
PhpPartFacade::ensureHaveLatestDataDeferredByExtension();
if ($this->inferredSpansManager !== null) {
$this->inferredSpansManager->shutdown();
}
$tx = $this->transactionForRequest;
if ($tx === null || $tx->isNoop() || $tx->hasEnded()) {
return;
}
if (!self::isCliScript()) {
$this->beforeHttpEnd($tx);
}
$tx->end();
if ($this->tracer->getConfig()->devInternal()->gcCollectCyclesAfterEveryTransaction()) {
$this->logGcStatus();
$numberOfCollectedCycles = gc_collect_cycles();
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Called gc_collect_cycles() because ' . OptionNames::DEV_INTERNAL
. ' sub-option ' . DevInternalSubOptionNames::GC_COLLECT_CYCLES_AFTER_EVERY_TRANSACTION . ' is set',
['numberOfCollectedCycles' => $numberOfCollectedCycles]
);
$this->logGcStatus();
}
if ($this->tracer->getConfig()->devInternal()->gcMemCachesAfterEveryTransaction()) {
$numberOfBytesFreed = gc_mem_caches();
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Called gc_mem_caches() because ' . OptionNames::DEV_INTERNAL
. ' sub-option ' . DevInternalSubOptionNames::GC_MEM_CACHES_AFTER_EVERY_TRANSACTION . ' is set',
['numberOfBytesFreed' => $numberOfBytesFreed]
);
}
}
private static function isCliScript(): bool
{
return PHP_SAPI === 'cli';
}
private static function sanitizeCliName(string $name): string
{
return preg_replace('/[^a-zA-Z0-9.:_\-]/', '_', $name) ?: ' ';
}
private function discoverCliName(): string
{
global $argc, $argv;
/** @noinspection PhpConditionAlreadyCheckedInspection */
if (
!isset($argc)
|| ($argc <= 0)
|| !isset($argv)
|| (count($argv) == 0)
|| !is_string($argv[0])
|| TextUtil::isEmptyString($argv[0])
) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Could not discover CLI script name - using default transaction name',
['DEFAULT_NAME' => self::DEFAULT_NAME]
);
return self::DEFAULT_NAME;
}
$cliScriptName = self::sanitizeCliName(basename($argv[0]));
if (
($argc < 2)
|| (count($argv) < 2)
|| ($cliScriptName !== self::LARAVEL_ARTISAN_COMMAND_SCRIPT)
) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Using CLI script name as transaction name',
['cliScriptName' => $cliScriptName, 'argc' => $argc, 'argv' => $argv]
);
return $cliScriptName;
}
$txName = $cliScriptName . ' ' . self::sanitizeCliName($argv[1]);
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'CLI script is Laravel ' . self::LARAVEL_ARTISAN_COMMAND_SCRIPT . ' command with arguments'
. ' - including the first argument in transaction name',
['txName' => $txName, 'argc' => $argc, 'argv' => $argv]
);
return $txName;
}
/**
* @param string $key
*
* @return mixed
*/
private function getOptionalServerVarElement(string $key)
{
/** @noinspection PhpIssetCanBeReplacedWithCoalesceInspection */
return isset($_SERVER[$key]) ? $_SERVER[$key] : null;
}
/**
* @param string $key
*
* @return mixed
*/
private function getMandatoryServerVarElement(string $key)
{
$val = $this->getOptionalServerVarElement($key);
if ($val === null) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('$_SERVER does not contain `' . $key . '\' key');
return null;
}
return $_SERVER[$key];
}
private function getMandatoryServerVarStringElement(string $key): ?string
{
$val = $this->getMandatoryServerVarElement($key);
if ($val === null) {
/** @noinspection PhpExpressionAlwaysNullInspection */
return $val;
}
if (!is_string($val)) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'$_SERVER contains `' . $key . '\' key but the value is not a string',
['value type' => DbgUtil::getType($val)]
);
return null;
}
return $val;
}
private function discoverHttpName(): string
{
if ($this->urlParts === null || $this->urlParts->path === null) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Failed to discover path part of URL to derive transaction name - using default transaction name',
['DEFAULT_NAME' => self::DEFAULT_NAME]
);
return self::DEFAULT_NAME;
}
$urlGroupsMatcher = $this->tracer->getConfig()->urlGroups();
$urlPath = $this->urlParts->path;
$urlPathGroup = WildcardListMatcher::matchNullable($urlGroupsMatcher, $urlPath);
if ($urlPathGroup !== null) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'For transaction name URL path is mapped to matched URL group',
[
'urlPath' => $urlPath,
'matched URL group' => $urlPathGroup,
OptionNames::URL_GROUPS . ' configuration' => $urlGroupsMatcher,
]
);
}
if ($urlPathGroup === null) {
$urlPathGroup = $urlPath;
}
$name = ($this->httpMethod === null)
? $urlPathGroup
: ($this->httpMethod . ' ' . $urlPathGroup);
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Successfully discovered HTTP data to derive transaction name', ['name' => $name]);
return $name;
}
private function discoverStartTime(float $requestInitStartTime): float
{
$serverRequestTimeAsString = self::getMandatoryServerVarElement('REQUEST_TIME_FLOAT');
if ($serverRequestTimeAsString === null) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Using requestInitStartTime for transaction start time'
. ' because $_SERVER[\'REQUEST_TIME_FLOAT\'] is not set',
['requestInitStartTime' => $requestInitStartTime]
);
return $requestInitStartTime;
}
/** @phpstan-ignore-next-line */
$serverRequestTimeInSeconds = floatval($serverRequestTimeAsString);
$serverRequestTimeInMicroseconds = $serverRequestTimeInSeconds * TimeUtil::NUMBER_OF_MICROSECONDS_IN_SECOND;
if ($requestInitStartTime < $serverRequestTimeInMicroseconds) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Using requestInitStartTime for transaction start time'
. ' because $_SERVER[\'REQUEST_TIME_FLOAT\'] is later'
. ' (further into the future) than requestInitStartTime',
[
'requestInitStartTime' => $requestInitStartTime,
'$_SERVER[\'REQUEST_TIME_FLOAT\']' => $serverRequestTimeInMicroseconds,
'$_SERVER[\'REQUEST_TIME_FLOAT\'] - requestInitStartTime (seconds)'
=> TimeUtil::microsecondsToSeconds(
$serverRequestTimeInMicroseconds - $requestInitStartTime
),
]
);
return $requestInitStartTime;
}
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Using $_SERVER[\'REQUEST_TIME_FLOAT\'] for transaction start time',
[
'$_SERVER[\'REQUEST_TIME_FLOAT\']' => $serverRequestTimeInMicroseconds,
'requestInitStartTime' => $requestInitStartTime,
'requestInitStartTime - $_SERVER[\'REQUEST_TIME_FLOAT\'] (seconds)'
=> TimeUtil::microsecondsToSeconds(
$serverRequestTimeInMicroseconds - $requestInitStartTime
),
]
);
return $serverRequestTimeInMicroseconds;
}
/**
* @return array<string, string>
*/
private function getDistributedTracingHeaders(): array
{
$result = [];
$traceParentHeaderValue = $this->getHttpHeader(HttpDistributedTracing::TRACE_PARENT_HEADER_NAME);
if ($traceParentHeaderValue === null) {
return [];
}
$result[HttpDistributedTracing::TRACE_PARENT_HEADER_NAME] = $traceParentHeaderValue;
$traceStateHeaderValue = $this->getHttpHeader(HttpDistributedTracing::TRACE_STATE_HEADER_NAME);
if ($traceStateHeaderValue !== null) {
$result[HttpDistributedTracing::TRACE_STATE_HEADER_NAME] = $traceStateHeaderValue;
}
return $result;
}
private function getHttpHeader(string $headerName): ?string
{
$headerKey = 'HTTP_' . strtoupper($headerName);
$traceParentHeaderValue = self::getOptionalServerVarElement($headerKey);
if ($traceParentHeaderValue === null) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Incoming ' . $headerName . ' HTTP request header not found');
return null;
}
if (!is_string($traceParentHeaderValue)) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'$_SERVER contains `' . $headerKey . '\' key but the value is not a string',
['value type' => DbgUtil::getType($traceParentHeaderValue)]
);
return null;
}
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Incoming ' . HttpDistributedTracing::TRACE_PARENT_HEADER_NAME . ' HTTP request header found',
['traceParentHeaderValue' => $traceParentHeaderValue]
);
return $traceParentHeaderValue;
}
private function discoverHttpStatusCode(): ?int
{
$statusCode = http_response_code();
if (!is_int($statusCode)) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'http_response_code() returned a value that is not an int',
['statusCode' => $statusCode]
);
return null;
}
return $statusCode;
}
private function discoverHttpResult(TransactionInterface $tx): void
{
$httpStatusCode = $this->discoverHttpStatusCode();
if ($httpStatusCode === null) {
return;
}
$httpStatusCode100s = intdiv($httpStatusCode, 100);
$result = 'HTTP ' . $httpStatusCode100s . 'xx';
$tx->setResult($result);
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Discovered result for HTTP transaction',
['httpStatusCode' => $httpStatusCode, 'result' => $result, 'httpStatusCode100s' => $httpStatusCode100s]
);
}
private function discoverHttpOutcome(TransactionInterface $tx): void
{
$httpStatusCode = $this->discoverHttpStatusCode();
if ($httpStatusCode === null) {
return;
}
$outcome = (500 <= $httpStatusCode && $httpStatusCode < 600)
? Constants::OUTCOME_FAILURE
: Constants::OUTCOME_SUCCESS;
$tx->setOutcome($outcome);
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Discovered outcome for HTTP transaction',
['httpStatusCode' => $httpStatusCode, 'outcome' => $outcome]
);
}
}