Skip to content

Commit

Permalink
Add transaction.result to public API and automatically fill it for HTTP
Browse files Browse the repository at this point in the history
  • Loading branch information
SergeyKleyman committed Oct 2, 2020
1 parent eed5c90 commit d5eace5
Show file tree
Hide file tree
Showing 23 changed files with 327 additions and 50 deletions.
25 changes: 25 additions & 0 deletions docs/public-api.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,31 @@ Example:
$parentId = $transaction->getParentId();
----

[float]
[[api-transaction-interface-set-result]]
==== `TransactionInterface::setResult`
Sets the result of the transaction.

Transaction result is optional and can be set to `null`.
For HTTP-related transactions, the result is HTTP status code formatted like `HTTP 2xx`.

Example:
[source,php]
----
$transaction->setResult('my custom transaction result');
----

[float]
[[api-transaction-interface-get-result]]
==== `TransactionInterface::getResult`
Gets the result of the transaction.

Example:
[source,php]
----
$transactionResult = $transaction->getResult();
----

[float]
[[api-transaction-interface-end]]
==== `TransactionInterface::end`
Expand Down
2 changes: 1 addition & 1 deletion src/ElasticApm/ElasticApm.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class ElasticApm
use StaticClassTrait;

/** @var string */
public const VERSION = '0.1-preview';
public const VERSION = '0.2';

/**
* Begins a new transaction and sets it as the current transaction.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ public function onShutdown(): void
return;
}

if (is_null($this->transactionForRequest->getResult()) && !self::isCliScript()) {
$discoveredResult = $this->discoverHttpResult();
if (!is_null($discoveredResult)) {
$this->transactionForRequest->setResult($discoveredResult);
}
}

$this->transactionForRequest->end();
}

Expand Down Expand Up @@ -128,4 +135,27 @@ private function discoverTimestamp(float $requestInitStartTime): float
);
return $requestInitStartTime;
}

private function discoverHttpResult(): ?string
{
$statusCode = http_response_code();
if (!is_int($statusCode)) {
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'http_response_code() returned a value that is not an int',
['statusCode' => $statusCode]
);
return null;
}

$statusCode100s = intdiv($statusCode, 100);

($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Discovered result for HTTP transaction',
['statusCode' => $statusCode, '$statusCode100s' => $statusCode100s]
);

return 'HTTP ' . $statusCode100s . 'xx';
}
}
9 changes: 9 additions & 0 deletions src/ElasticApm/Impl/NoopTransaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ public function getCurrentSpan(): SpanInterface
return NoopSpan::singletonInstance();
}

public function getResult(): ?string
{
return null;
}

public function setResult(?string $result): void
{
}

public function __toString(): string
{
return 'NO-OP Transaction';
Expand Down
9 changes: 9 additions & 0 deletions src/ElasticApm/Impl/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ public function popCurrentSpan(): void
}
}

public function setResult(?string $result): void
{
if ($this->checkIfAlreadyEnded(__FUNCTION__)) {
return;
}

$this->result = $this->tracer->limitNullableKeywordString($result);
}

public function end(?float $duration = null): void
{
if (!$this->endExecutionSegment($duration)) {
Expand Down
8 changes: 8 additions & 0 deletions src/ElasticApm/Impl/TransactionData.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class TransactionData extends ExecutionSegmentData implements TransactionDataInt
/** @var int */
protected $startedSpansCount = 0;

/** @var string|null */
protected $result = null;

public function getDroppedSpansCount(): int
{
return $this->droppedSpansCount;
Expand All @@ -39,6 +42,11 @@ public function getStartedSpansCount(): int
return $this->startedSpansCount;
}

public function getResult(): ?string
{
return $this->result;
}

/**
* @param array<string, mixed> $result
*/
Expand Down
8 changes: 8 additions & 0 deletions src/ElasticApm/TransactionDataInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ public function getStartedSpansCount(): int;
* @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/transactions/transaction.json#L32
*/
public function getDroppedSpansCount(): int;

/**
* The result of the transaction.
* For HTTP-related transactions, this should be the status code formatted like 'HTTP 2xx'.
*
* @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/transactions/transaction.json#L52
*/
public function getResult(): ?string;
}
7 changes: 7 additions & 0 deletions src/ElasticApm/TransactionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,12 @@ public function captureCurrentSpan(
*/
public function getCurrentSpan(): SpanInterface;

/**
* @param string|null $result
*
* @see TransactionDataInterface::getResult() For the description
*/
public function setResult(?string $result): void;

public function __toString(): string;
}
70 changes: 60 additions & 10 deletions tests/ElasticApmTests/ComponentTests/TransactionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
namespace Elastic\Apm\Tests\ComponentTests;

use Elastic\Apm\ElasticApm;
use Elastic\Apm\Impl\Util\ArrayUtil;
use Elastic\Apm\Tests\ComponentTests\Util\ComponentTestCaseBase;
use Elastic\Apm\Tests\ComponentTests\Util\DataFromAgent;
use Elastic\Apm\Tests\ComponentTests\Util\HttpConsts;
use Elastic\Apm\Tests\ComponentTests\Util\TestProperties;
use Elastic\Apm\TransactionDataInterface;

Expand Down Expand Up @@ -40,6 +42,22 @@ private function verifyTransactionWithoutSpans(DataFromAgent $dataFromAgent): Tr
return $tx;
}

public static function appCodeForTransactionWithoutSpansCustomProperties(): void
{
ElasticApm::getCurrentTransaction()->setName('custom TX name');
ElasticApm::getCurrentTransaction()->setType('custom TX type');
ElasticApm::getCurrentTransaction()->setLabel('string_label_key', 'string_label_value');
ElasticApm::getCurrentTransaction()->setLabel('bool_label_key', true);
ElasticApm::getCurrentTransaction()->setLabel('int_label_key', -987654321);
ElasticApm::getCurrentTransaction()->setLabel('float_label_key', 1234.56789);
ElasticApm::getCurrentTransaction()->setLabel('null_label_key', null);

usleep(/* microseconds - 200 milliseconds */ 200 * 1000);

ElasticApm::getCurrentTransaction()->setResult('custom TX result');
ElasticApm::getCurrentTransaction()->end(/* milliseconds */ 100);
}

public function testTransactionWithoutSpansCustomProperties(): void
{
$this->sendRequestToInstrumentedAppAndVerifyDataFromAgentEx(
Expand All @@ -53,21 +71,53 @@ function (DataFromAgent $dataFromAgent): void {
$this->assertSame(-987654321, $tx->getLabels()['int_label_key']);
$this->assertSame(1234.56789, $tx->getLabels()['float_label_key']);
$this->assertNull($tx->getLabels()['null_label_key']);
$this->assertSame('custom TX result', $tx->getResult());
$this->assertEquals(100, $tx->getDuration());
}
);
}

public static function appCodeForTransactionWithoutSpansCustomProperties(): void
/**
* @param array<string, mixed> $args
*/
public static function appCodeForTransactionWithCustomHttpStatus(array $args): void
{
ElasticApm::getCurrentTransaction()->setName('custom TX name');
ElasticApm::getCurrentTransaction()->setType('custom TX type');
ElasticApm::getCurrentTransaction()->setLabel('string_label_key', 'string_label_value');
ElasticApm::getCurrentTransaction()->setLabel('bool_label_key', true);
ElasticApm::getCurrentTransaction()->setLabel('int_label_key', -987654321);
ElasticApm::getCurrentTransaction()->setLabel('float_label_key', 1234.56789);
ElasticApm::getCurrentTransaction()->setLabel('null_label_key', null);
usleep(/* microseconds - 200 milliseconds */ 200 * 1000);
ElasticApm::getCurrentTransaction()->end(/* milliseconds */ 100);
$customHttpStatus = ArrayUtil::getValueIfKeyExistsElse('customHttpStatus', $args, null);
if (!is_null($customHttpStatus)) {
http_response_code($customHttpStatus);
}
}

/**
* @return array<array<null|int|string>>
*/
public function transactionWithCustomHttpStatusDataProvider(): array
{
return [
[null, 'HTTP 2xx'],
[200, 'HTTP 2xx'],
[404, 'HTTP 4xx'],
[599, 'HTTP 5xx'],
];
}

/**
* @dataProvider transactionWithCustomHttpStatusDataProvider
*
* @param int|null $customHttpStatus
* @param string $expectedTxResult
*/
public function testTransactionWithCustomHttpStatus(?int $customHttpStatus, string $expectedTxResult): void
{
$this->sendRequestToInstrumentedAppAndVerifyDataFromAgentEx(
(new TestProperties(
[__CLASS__, 'appCodeForTransactionWithCustomHttpStatus'],
['customHttpStatus' => $customHttpStatus]
))->withExpectedStatusCode($customHttpStatus ?? HttpConsts::STATUS_OK),
function (DataFromAgent $dataFromAgent) use ($expectedTxResult): void {
$tx = $this->verifyTransactionWithoutSpans($dataFromAgent);
self::assertSame($this->testEnv->isHttp() ? $expectedTxResult : null, $tx->getResult());
}
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ final class AllComponentTestsOptionsMetadata
{
use StaticClassTrait;

public const APP_CODE_ARGUMENTS_OPTION_NAME = 'app_code_arguments';
public const APP_CODE_CLASS_OPTION_NAME = 'app_code_class';
public const APP_CODE_METHOD_OPTION_NAME = 'app_code_method';
public const RESOURCES_CLEANER_PORT_OPTION_NAME = 'resources_cleaner_port';
Expand All @@ -34,6 +35,7 @@ final class AllComponentTestsOptionsMetadata
public static function build(): array
{
return [
self::APP_CODE_ARGUMENTS_OPTION_NAME => new NullableStringOptionMetadata(),
self::APP_CODE_CLASS_OPTION_NAME => new NullableStringOptionMetadata(),
AppCodeHostKindOptionMetadata::NAME => new AppCodeHostKindOptionMetadata(),
self::APP_CODE_METHOD_OPTION_NAME => new NullableStringOptionMetadata(),
Expand Down
31 changes: 25 additions & 6 deletions tests/ElasticApmTests/ComponentTests/Util/AppCodeHostBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@
use Elastic\Apm\ElasticApm;
use Elastic\Apm\Impl\Log\Logger;
use Elastic\Apm\Impl\Util\ObjectToStringBuilder;
use Elastic\Apm\Tests\Util\Deserialization\SerializationTestUtil;
use Elastic\Apm\Tests\Util\TestLogCategory;
use RuntimeException;
use Throwable;

abstract class AppCodeHostBase extends CliProcessBase
{
/** @var Logger */
private $logger;

/** @var array<string, mixed>|null */
public $appCodeArgs;
/** @var string */
protected $appCodeClass;

/** @var string */
protected $appCodeMethod;
/** @var Logger */
private $logger;

public function __construct(string $runScriptFile)
{
Expand Down Expand Up @@ -55,14 +56,31 @@ protected function registerWithResourcesCleaner(): void
}
}

/**
* @param string|null $appCodeArgsAsString
*
* @return array<string, mixed>|null
*/
protected static function deserializeAppCodeArguments(?string $appCodeArgsAsString): ?array
{
return is_null($appCodeArgsAsString)
? null
: SerializationTestUtil::deserializeJson($appCodeArgsAsString, /* asAssocArray */ true);
}

protected function callAppCode(): void
{
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Calling application code...');

try {
/** @phpstan-ignore-next-line */
call_user_func([$this->appCodeClass, $this->appCodeMethod]);
if (is_null($this->appCodeArgs)) {
/** @phpstan-ignore-next-line */
call_user_func([$this->appCodeClass, $this->appCodeMethod]);
} else {
/** @phpstan-ignore-next-line */
call_user_func([$this->appCodeClass, $this->appCodeMethod], $this->appCodeArgs);
}
} catch (Throwable $throwable) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Call to application code exited by exception', ['throwable' => $throwable]);
Expand All @@ -78,5 +96,6 @@ protected function toStringAddProperties(ObjectToStringBuilder $builder): void
parent::toStringAddProperties($builder);
$builder->add('appCodeClass', $this->appCodeClass);
$builder->add('appCodeMethod', $this->appCodeMethod);
$builder->add('appCodeArgs', $this->appCodeArgs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class BuiltinHttpServerAppCodeHost extends AppCodeHostBase
{
use HttpServerProcessTrait;

public const ARGUMENTS_HEADER_NAME = TestEnvBase::HEADER_NAME_PREFIX . 'APP_CODE_ARGUMENTS';
public const CLASS_HEADER_NAME = TestEnvBase::HEADER_NAME_PREFIX . 'APP_CODE_CLASS';
public const METHOD_HEADER_NAME = TestEnvBase::HEADER_NAME_PREFIX . 'APP_CODE_METHOD';

Expand Down Expand Up @@ -59,14 +60,15 @@ protected function processConfig(): void
parent::processConfig();

if (!self::isStatusCheck()) {
$this->appCodeClass = self::getRequestHeader(self::CLASS_HEADER_NAME);
$this->appCodeMethod = self::getRequestHeader(self::METHOD_HEADER_NAME);
$this->appCodeClass = self::getRequiredRequestHeader(self::CLASS_HEADER_NAME);
$this->appCodeMethod = self::getRequiredRequestHeader(self::METHOD_HEADER_NAME);
$this->appCodeArgs = self::deserializeAppCodeArguments(self::getRequestHeader(self::ARGUMENTS_HEADER_NAME));
}
}

protected function runImpl(): void
{
$response = self::verifyServerIdEx([__CLASS__, 'getRequestHeader']);
$response = self::verifyServerIdEx([__CLASS__, 'getRequiredRequestHeader']);
if ($response->getStatusCode() !== HttpConsts::STATUS_OK) {
self::sendResponse($response);
return;
Expand All @@ -88,13 +90,23 @@ protected function cliHelpOptions(): string
throw new RuntimeException('This method should not be called: ' . __METHOD__);
}

protected static function getRequestHeader(string $headerName): string
protected static function getRequestHeader(string $headerName): ?string
{
$headerKey = 'HTTP_' . $headerName;
if (!array_key_exists($headerKey, $_SERVER)) {
throw new RuntimeException('Required HTTP request header `' . $headerName . '\' is missing');
return null;
}

return $_SERVER[$headerKey];
}

protected static function getRequiredRequestHeader(string $headerName): string
{
$headerValue = self::getRequestHeader($headerName);
if (is_null($headerValue)) {
throw new RuntimeException('Required HTTP request header `' . $headerName . '\' is missing');
}

return $headerValue;
}
}
Loading

0 comments on commit d5eace5

Please sign in to comment.