Repository: elastic/apm-agent-php (php)

Issue:
Configuration option: SERVICE_NODE_NAME
Add support for `ELASTIC_APM_SERVICE_NODE_NAME` configuration option and `service.node.configured_name` intake API field.

Derived from https://github.com/elastic/apm/issues/141

Pull request description:
Implemented configuration option SERVICE_NODE_NAME
Closes #143

Patch:
diff --git a/src/ElasticApm/Impl/Config/AllOptionsMetadata.php b/src/ElasticApm/Impl/Config/AllOptionsMetadata.php
index e847a9cf..d6f862f6 100644
--- a/src/ElasticApm/Impl/Config/AllOptionsMetadata.php
+++ b/src/ElasticApm/Impl/Config/AllOptionsMetadata.php
@@ -66,6 +66,7 @@ final class AllOptionsMetadata
                     30 * 1000.0 /* <- defaultValueInMilliseconds - 30s */
                 ),
                 OptionNames::SERVICE_NAME            => new NullableStringOptionMetadata(),
+                OptionNames::SERVICE_NODE_NAME       => new NullableStringOptionMetadata(),
                 OptionNames::SERVICE_VERSION         => new NullableStringOptionMetadata(),
                 OptionNames::TRANSACTION_MAX_SPANS   => new IntOptionMetadata(
                     0 /* <- minValidValue */,
diff --git a/src/ElasticApm/Impl/Config/OptionNames.php b/src/ElasticApm/Impl/Config/OptionNames.php
index 84011309..2c7f5eab 100644
--- a/src/ElasticApm/Impl/Config/OptionNames.php
+++ b/src/ElasticApm/Impl/Config/OptionNames.php
@@ -46,6 +46,7 @@ final class OptionNames
     public const SERVER_TIMEOUT = 'server_timeout';
     public const SERVER_URL = 'server_url';
     public const SERVICE_NAME = 'service_name';
+    public const SERVICE_NODE_NAME = 'service_node_name';
     public const SERVICE_VERSION = 'service_version';
     public const TRANSACTION_MAX_SPANS = 'transaction_max_spans';
     public const TRANSACTION_SAMPLE_RATE = 'transaction_sample_rate';
diff --git a/src/ElasticApm/Impl/Config/Snapshot.php b/src/ElasticApm/Impl/Config/Snapshot.php
index acf0de72..a8eb96a2 100644
--- a/src/ElasticApm/Impl/Config/Snapshot.php
+++ b/src/ElasticApm/Impl/Config/Snapshot.php
@@ -122,6 +122,9 @@ final class Snapshot implements LoggableInterface
     /** @var ?string */
     private $serviceName;
 
+    /** @var ?string */
+    private $serviceNodeName;
+
     /** @var ?string */
     private $serviceVersion;
 
@@ -192,6 +195,11 @@ final class Snapshot implements LoggableInterface
         return $this->serviceName;
     }
 
+    public function serviceNodeName(): ?string
+    {
+        return $this->serviceNodeName;
+    }
+
     public function serviceVersion(): ?string
     {
         return $this->serviceVersion;
diff --git a/src/ElasticApm/Impl/MetadataDiscoverer.php b/src/ElasticApm/Impl/MetadataDiscoverer.php
index 26d30cff..4b65cd9b 100644
--- a/src/ElasticApm/Impl/MetadataDiscoverer.php
+++ b/src/ElasticApm/Impl/MetadataDiscoverer.php
@@ -80,21 +80,25 @@ final class MetadataDiscoverer
             : Tracer::limitKeywordString($charsAdaptedName);
     }
 
+    private static function setKeywordStringIfNotNull(?string $srcCfgVal, ?string &$dstProp): void
+    {
+        if ($srcCfgVal !== null) {
+            $dstProp = Tracer::limitKeywordString($srcCfgVal);
+        }
+    }
+
     public function discoverServiceData(ConfigSnapshot $config): ServiceData
     {
         $result = new ServiceData();
 
-        if ($config->environment() !== null) {
-            $result->environment = Tracer::limitKeywordString($config->environment());
-        }
+        self::setKeywordStringIfNotNull($config->environment(), /* ref */ $result->environment);
 
-        $result->name = is_null($config->serviceName())
+        $result->name = $config->serviceName() === null
             ? MetadataDiscoverer::DEFAULT_SERVICE_NAME
             : MetadataDiscoverer::adaptServiceName($config->serviceName());
 
-        if (!is_null($config->serviceVersion())) {
-            $result->version = Tracer::limitKeywordString($config->serviceVersion());
-        }
+        self::setKeywordStringIfNotNull($config->serviceNodeName(), /* ref */ $result->nodeConfiguredName);
+        self::setKeywordStringIfNotNull($config->serviceVersion(), /* ref */ $result->version);
 
         $result->agent = new ServiceAgentData();
         $result->agent->name = self::AGENT_NAME;
diff --git a/src/ElasticApm/Impl/ServiceData.php b/src/ElasticApm/Impl/ServiceData.php
index 92de8f4c..1292a952 100644
--- a/src/ElasticApm/Impl/ServiceData.php
+++ b/src/ElasticApm/Impl/ServiceData.php
@@ -37,27 +37,16 @@ class ServiceData implements SerializableDataInterface, LoggableInterface
     use LoggableTrait;
 
     /**
-     * @var string|null
-     *
-     * Immutable name of the service emitting this event.
-     * Valid characters are: 'a'-'z', 'A'-'Z', '0'-'9', '_' and '-'.
-     *
-     * The length of this string is limited to 1024.
-     *
-     * @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/service.json#L50
-     */
-    public $name = null;
-
-    /**
-     * @var string|null
-     *
-     * Version of the service emitting this event.
+     * @var ServiceAgentData|null
      *
-     * The length of this string is limited to 1024.
+     * Name and version of the Elastic APM agent.
+     * Name of the Elastic APM agent, e.g. "php".
+     * Version of the Elastic APM agent, e.g."1.0.0".
      *
-     * @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/service.json#L75
+     * @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/service.json#L10
+     * @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/service.json#L15
      */
-    public $version = null;
+    public $agent = null;
 
     /**
      * @var string|null
@@ -70,18 +59,6 @@ class ServiceData implements SerializableDataInterface, LoggableInterface
      */
     public $environment = null;
 
-    /**
-     * @var ServiceAgentData|null
-     *
-     * Name and version of the Elastic APM agent.
-     * Name of the Elastic APM agent, e.g. "php".
-     * Version of the Elastic APM agent, e.g."1.0.0".
-     *
-     * @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/service.json#L10
-     * @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/service.json#L15
-     */
-    public $agent = null;
-
     /**
      * @var NameVersionData|null
      *
@@ -102,6 +79,29 @@ class ServiceData implements SerializableDataInterface, LoggableInterface
      */
     public $language = null;
 
+    /**
+     * @var string|null
+     *
+     * Immutable name of the service emitting this event.
+     * Valid characters are: 'a'-'z', 'A'-'Z', '0'-'9', '_' and '-'.
+     *
+     * The length of this string is limited to 1024.
+     *
+     * @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/service.json#L50
+     */
+    public $name = null;
+
+    /**
+     * @var string|null
+     *
+     * Unique meaningful name of the service node.
+     *
+     * The length of this string is limited to 1024.
+     *
+     * @link https://github.com/elastic/apm-server/blob/v7.5.0/docs/spec/service.json#L85
+     */
+    public $nodeConfiguredName = null;
+
     /**
      * @var NameVersionData|null
      *
@@ -112,18 +112,35 @@ class ServiceData implements SerializableDataInterface, LoggableInterface
      */
     public $runtime = null;
 
+    /**
+     * @var string|null
+     *
+     * Version of the service emitting this event.
+     *
+     * The length of this string is limited to 1024.
+     *
+     * @link https://github.com/elastic/apm-server/blob/7.0/docs/spec/service.json#L75
+     */
+    public $version = null;
+
     /** @inheritDoc */
     public function jsonSerialize()
     {
         $result = [];
 
-        SerializationUtil::addNameValueIfNotNull('name', $this->name, /* ref */ $result);
-        SerializationUtil::addNameValueIfNotNull('version', $this->version, /* ref */ $result);
-        SerializationUtil::addNameValueIfNotNull('environment', $this->environment, /* ref */ $result);
         SerializationUtil::addNameValueIfNotNull('agent', $this->agent, /* ref */ $result);
+        SerializationUtil::addNameValueIfNotNull('environment', $this->environment, /* ref */ $result);
         SerializationUtil::addNameValueIfNotNull('framework', $this->framework, /* ref */ $result);
         SerializationUtil::addNameValueIfNotNull('language', $this->language, /* ref */ $result);
+        SerializationUtil::addNameValueIfNotNull('name', $this->name, /* ref */ $result);
+
+        if ($this->nodeConfiguredName !== null) {
+            $nodeSubObject = ['configured_name' => $this->nodeConfiguredName];
+            SerializationUtil::addNameValue('node', $nodeSubObject, /* ref */ $result);
+        }
+
         SerializationUtil::addNameValueIfNotNull('runtime', $this->runtime, /* ref */ $result);
+        SerializationUtil::addNameValueIfNotNull('version', $this->version, /* ref */ $result);
 
         return SerializationUtil::postProcessResult($result);
     }
diff --git a/src/ext/ConfigManager.c b/src/ext/ConfigManager.c
index bec609a1..487204f6 100644
--- a/src/ext/ConfigManager.c
+++ b/src/ext/ConfigManager.c
@@ -583,6 +583,7 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, secretToken )
 ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serverTimeout )
 ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serverUrl )
 ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serviceName )
+ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serviceNodeName )
 ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serviceVersion )
 ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, transactionMaxSpans )
 ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, transactionSampleRate )
@@ -782,7 +783,13 @@ static void initOptionsMetadata( OptionMetadata* optsMeta )
             buildStringOptionMetadata,
             serviceName,
             ELASTIC_APM_CFG_OPT_NAME_SERVICE_NAME,
-            /* defaultValue: */ "Unknown PHP service" );
+            /* defaultValue: */ NULL );
+
+    ELASTIC_APM_INIT_METADATA(
+            buildStringOptionMetadata,
+            serviceNodeName,
+            ELASTIC_APM_CFG_OPT_NAME_SERVICE_NODE_NAME,
+            /* defaultValue: */ NULL );
 
     ELASTIC_APM_INIT_METADATA(
             buildStringOptionMetadata,
diff --git a/src/ext/ConfigManager.h b/src/ext/ConfigManager.h
index e29f1d2f..6213cf06 100644
--- a/src/ext/ConfigManager.h
+++ b/src/ext/ConfigManager.h
@@ -87,6 +87,7 @@ enum OptionId
     optionId_serverTimeout,
     optionId_serverUrl,
     optionId_serviceName,
+    optionId_serviceNodeName,
     optionId_serviceVersion,
     optionId_transactionMaxSpans,
     optionId_transactionSampleRate,
@@ -131,6 +132,7 @@ struct ConfigSnapshot
     String serverUrl;
     String serverTimeout;
     String serviceName;
+    String serviceNodeName;
     String serviceVersion;
     String transactionMaxSpans;
     String transactionSampleRate;
@@ -249,6 +251,7 @@ const ConfigSnapshot* getGlobalCurrentConfigSnapshot();
 #define ELASTIC_APM_CFG_OPT_NAME_SERVER_TIMEOUT "server_timeout"
 #define ELASTIC_APM_CFG_OPT_NAME_SERVER_URL "server_url"
 #define ELASTIC_APM_CFG_OPT_NAME_SERVICE_NAME "service_name"
+#define ELASTIC_APM_CFG_OPT_NAME_SERVICE_NODE_NAME "service_node_name"
 #define ELASTIC_APM_CFG_OPT_NAME_SERVICE_VERSION "service_version"
 #define ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_MAX_SPANS "transaction_max_spans"
 #define ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_SAMPLE_RATE "transaction_sample_rate"
diff --git a/src/ext/elastic_apm.c b/src/ext/elastic_apm.c
index e81a076e..1326bbb9 100644
--- a/src/ext/elastic_apm.c
+++ b/src/ext/elastic_apm.c
@@ -125,6 +125,7 @@ PHP_INI_BEGIN()
     ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SERVER_TIMEOUT )
     ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SERVER_URL )
     ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SERVICE_NAME )
+    ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SERVICE_NODE_NAME )
     ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SERVICE_VERSION )
     ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_MAX_SPANS )
     ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_SAMPLE_RATE )


Test patch:
diff --git a/src/ext/tests/config_defaults.phpt b/src/ext/tests/config_defaults.phpt
index 623f1531..4d8865fe 100644
--- a/src/ext/tests/config_defaults.phpt
+++ b/src/ext/tests/config_defaults.phpt
@@ -103,7 +103,7 @@ elasticApmAssertSame("getenv('ELASTIC_APM_SERVICE_NAME')", getenv('ELASTIC_APM_S
 
 elasticApmAssertEqual("ini_get('elastic_apm.service_name')", ini_get('elastic_apm.service_name'), false);
 
-elasticApmAssertSame("elastic_apm_get_config_option_by_name('service_name')", elastic_apm_get_config_option_by_name('service_name'), 'Unknown PHP service');
+elasticApmAssertSame("elastic_apm_get_config_option_by_name('service_name')", elastic_apm_get_config_option_by_name('service_name'), null);
 
 echo 'Test completed'
 ?>
diff --git a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php
index 6933ac7c..4b3e1848 100644
--- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php
+++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php
@@ -104,6 +104,7 @@ final class ConfigSettingTest extends ComponentTestCaseBase
             OptionNames::SECRET_TOKEN            => $stringRawToParsedValues(['my_secret_token']),
             OptionNames::SERVER_TIMEOUT          => $durationRawToParsedValues,
             OptionNames::SERVICE_NAME            => $stringRawToParsedValues(['my_service_name']),
+            OptionNames::SERVICE_NODE_NAME       => $stringRawToParsedValues(['my_service_node_name']),
             OptionNames::SERVICE_VERSION         => $stringRawToParsedValues(['my_service_version']),
             OptionNames::TRANSACTION_MAX_SPANS   => $intRawToParsedValues,
             OptionNames::TRANSACTION_SAMPLE_RATE => $doubleRawToParsedValues,
diff --git a/tests/ElasticApmTests/ComponentTests/MetadataTest.php b/tests/ElasticApmTests/ComponentTests/MetadataTest.php
index ca8b59a2..0f01be08 100644
--- a/tests/ElasticApmTests/ComponentTests/MetadataTest.php
+++ b/tests/ElasticApmTests/ComponentTests/MetadataTest.php
@@ -215,4 +215,42 @@ final class MetadataTest extends ComponentTestCaseBase
     {
         $this->hostnameConfigTestImpl($this->randomConfigSetter(), $configured);
     }
+
+    private function serviceNodeNameConfigTestImpl(
+        ?AgentConfigSetter $configSetter,
+        ?string $configured,
+        ?string $expected
+    ): void {
+        $this->configTestImpl(
+            $configSetter,
+            $configured,
+            function (AgentConfigSetter $configSetter, string $configured): void {
+                $configSetter->set(OptionNames::SERVICE_NODE_NAME, $configured);
+            },
+            function (DataFromAgent $dataFromAgent) use ($expected): void {
+                TestEnvBase::verifyServiceNodeName($expected, $dataFromAgent);
+            }
+        );
+    }
+
+    public function testDefaultServiceNodeName(): void
+    {
+        $this->serviceNodeNameConfigTestImpl(/* configSetter: */ null, /* configured: */ null, /* expected: */ null);
+    }
+
+    public function testCustomServiceNodeName(): void
+    {
+        $configured = 'alpha-centauri node@CI#.!?.';
+        $this->serviceNodeNameConfigTestImpl($this->randomConfigSetter(), $configured, /* expected: */ $configured);
+    }
+
+    public function testInvalidServiceNodeNameTooLong(): void
+    {
+        $expected = self::generateDummyMaxKeywordString();
+        $this->serviceNodeNameConfigTestImpl(
+            $this->randomConfigSetter(),
+            /* configured: */ $expected . '_tail',
+            $expected
+        );
+    }
 }
diff --git a/tests/ElasticApmTests/ComponentTests/Util/FlakyAssertions.php b/tests/ElasticApmTests/ComponentTests/Util/FlakyAssertions.php
index a5506e3b..4dda8349 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/FlakyAssertions.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/FlakyAssertions.php
@@ -27,6 +27,7 @@ use Closure;
 use Elastic\Apm\Impl\Config\BoolOptionParser;
 use Elastic\Apm\Impl\Log\LoggableToEncodedJson;
 use Elastic\Apm\Impl\Util\StaticClassTrait;
+use ElasticApmTests\Util\TestCaseBase;
 use PHPUnit\Framework\ExpectationFailedException;
 
 final class FlakyAssertions
@@ -68,13 +69,13 @@ final class FlakyAssertions
                 throw $ex;
             }
 
-            fwrite(
-                STDERR,
-                PHP_EOL . __METHOD__ . ': ' . 'Flaky assertions are disabled but one has just failed' . PHP_EOL
+            TestCaseBase::printMessage(
+                __METHOD__,
+                'Flaky assertions are disabled but one has just failed' . PHP_EOL
                 . '+-> Exception:' . PHP_EOL
                 . LoggableToEncodedJson::convert($ex) . PHP_EOL
                 . '+-> Stack trace:' . PHP_EOL
-                . $ex->getTraceAsString() . PHP_EOL
+                . $ex->getTraceAsString()
             );
         }
     }
diff --git a/tests/ElasticApmTests/ComponentTests/Util/PhpUnitExtension.php b/tests/ElasticApmTests/ComponentTests/Util/PhpUnitExtension.php
index 99daa15c..f6a04279 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/PhpUnitExtension.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/PhpUnitExtension.php
@@ -85,7 +85,7 @@ final class PhpUnitExtension implements
             ]
         );
 
-        TestCaseBase::printMessage('Test starting... (' . $test . ')');
+        TestCaseBase::printMessage(__METHOD__, 'Test starting... (' . $test . ')');
 
         TestConfigUtil::assertAgentDisabled();
     }
@@ -105,7 +105,10 @@ final class PhpUnitExtension implements
             ['test' => $test, 'time' => $time, 'testEnvId' => self::$testEnvId]
         );
 
-        TestCaseBase::printMessage('Test finished successfully (' . $test . '). Time: ' . self::formatTime($time));
+        TestCaseBase::printMessage(
+            __METHOD__,
+            'Test finished successfully (' . $test . '). Time: ' . self::formatTime($time)
+        );
     }
 
     private function testFinishedUnsuccessfully(string $issue, string $test, string $message, float $time): void
@@ -117,6 +120,7 @@ final class PhpUnitExtension implements
         );
 
         TestCaseBase::printMessage(
+            __METHOD__,
             'Test finished ' . $issue . '(' . $test . ')'
             . '. Message: ' . $message . '. Time: ' . self::formatTime($time)
         );
diff --git a/tests/ElasticApmTests/ComponentTests/Util/TestEnvBase.php b/tests/ElasticApmTests/ComponentTests/Util/TestEnvBase.php
index 7dd991fd..8f3d74ca 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/TestEnvBase.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/TestEnvBase.php
@@ -264,6 +264,11 @@ abstract class TestEnvBase implements LoggableInterface
             ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
             && $loggerProxy->log('Starting HTTP server...');
 
+            TestCaseBase::printMessage(
+                __METHOD__,
+                "Starting HTTP server. cmdLine: `${cmdLine}', currentTryPort: ${currentTryPort} ..."
+            );
+
             $sharedDataPerProcessOptName = AllComponentTestsOptionsMetadata::SHARED_DATA_PER_PROCESS_OPTION_NAME;
             $sharedDataPerProcess = $this->buildSharedDataPerProcess($currentTryServerId, $currentTryPort);
             TestProcessUtil::startBackgroundProcess(
@@ -622,6 +627,11 @@ abstract class TestEnvBase implements LoggableInterface
             : MetadataDiscoverer::adaptServiceName($configuredServiceName);
         self::verifyServiceName($expectedServiceName, $this->dataFromAgent);
 
+        $expectedServiceNodeName = Tracer::limitNullableKeywordString(
+            $testProperties->getConfiguredAgentOption(OptionNames::SERVICE_NODE_NAME)
+        );
+        self::verifyServiceNodeName($expectedServiceNodeName, $this->dataFromAgent);
+
         $expectedServiceVersion = Tracer::limitNullableKeywordString(
             $testProperties->getConfiguredAgentOption(OptionNames::SERVICE_VERSION)
         );
@@ -679,6 +689,13 @@ abstract class TestEnvBase implements LoggableInterface
         }
     }
 
+    public static function verifyServiceNodeName(?string $expected, DataFromAgent $dataFromAgent): void
+    {
+        foreach ($dataFromAgent->metadata() as $metadata) {
+            TestCase::assertSame($expected, $metadata->service->nodeConfiguredName);
+        }
+    }
+
     public static function verifyServiceVersion(?string $expected, DataFromAgent $dataFromAgent): void
     {
         foreach ($dataFromAgent->metadata() as $metadata) {
diff --git a/tests/ElasticApmTests/ComponentTests/Util/TestHttpClientUtil.php b/tests/ElasticApmTests/ComponentTests/Util/TestHttpClientUtil.php
index b647e2c7..2f5942ef 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/TestHttpClientUtil.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/TestHttpClientUtil.php
@@ -29,6 +29,7 @@ use Elastic\Apm\Impl\BackendComm\SerializationUtil;
 use Elastic\Apm\Impl\Util\StaticClassTrait;
 use Elastic\Apm\Impl\Util\UrlParts;
 use Elastic\Apm\Impl\Util\UrlUtil;
+use ElasticApmTests\Util\TestCaseBase;
 use GuzzleHttp\Client;
 use GuzzleHttp\Exception\GuzzleException;
 use GuzzleHttp\RequestOptions;
@@ -54,10 +55,15 @@ final class TestHttpClientUtil
         SharedDataPerRequest $sharedDataPerRequest,
         array $headers = []
     ): ResponseInterface {
-        $client = new Client(['base_uri' => UrlUtil::buildRequestBaseUrl($urlParts)]);
+        $baseUrl = UrlUtil::buildRequestBaseUrl($urlParts);
+        $urlRelPart = UrlUtil::buildRequestMethodArg($urlParts);
+
+        TestCaseBase::printMessage(__METHOD__, "Sending HTTP request to `${baseUrl}${urlRelPart}'...");
+
+        $client = new Client(['base_uri' => $baseUrl]);
         return $client->request(
             $httpMethod,
-            UrlUtil::buildRequestMethodArg($urlParts),
+            $urlRelPart,
             [
                 RequestOptions::HEADERS     =>
                     $headers
diff --git a/tests/ElasticApmTests/ComponentTests/Util/TestProcessUtil.php b/tests/ElasticApmTests/ComponentTests/Util/TestProcessUtil.php
index 37a06d08..46912788 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/TestProcessUtil.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/TestProcessUtil.php
@@ -26,6 +26,7 @@ namespace ElasticApmTests\ComponentTests\Util;
 use Elastic\Apm\Impl\Util\ExceptionUtil;
 use Elastic\Apm\Impl\Util\StaticClassTrait;
 use ElasticApmTests\Util\LogCategoryForTests;
+use ElasticApmTests\Util\TestCaseBase;
 use RuntimeException;
 
 final class TestProcessUtil
@@ -84,7 +85,9 @@ final class TestProcessUtil
         )->addAllContext(['adaptedCmd' => $adaptedCmd, 'envVars' => $envVars]);
 
         ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
-        && $loggerProxy->log('Starting an external process...');
+        && $loggerProxy->log('Starting process...');
+
+        TestCaseBase::printMessage(__METHOD__, "Starting process: `$adaptedCmd'...");
 
         $pipes = [];
         $openedProc = proc_open(
@@ -95,6 +98,7 @@ final class TestProcessUtil
             $envVars
         );
         if ($openedProc === false) {
+            TestCaseBase::printMessage(__METHOD__, "Failed to start process. adaptedCmd: `$adaptedCmd'");
             throw new RuntimeException("Failed to start process. adaptedCmd: `$adaptedCmd'");
         }
 
@@ -102,6 +106,7 @@ final class TestProcessUtil
 
         $exitCode = proc_close($openedProc);
         if ($exitCode === SpawnedProcessBase::FAILURE_PROCESS_EXIT_CODE) {
+            TestCaseBase::printMessage(__METHOD__, "Process exited with the failure exit code: $exitCode");
             throw new RuntimeException(
                 ExceptionUtil::buildMessage(
                     'Process exited with the failure exit code',
@@ -110,7 +115,9 @@ final class TestProcessUtil
             );
         }
 
+        TestCaseBase::printMessage(__METHOD__, "Process started");
+
         ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
-        && $loggerProxy->log('External process started', ['newProcessInfo' => $newProcessInfo]);
+        && $loggerProxy->log('Process started', ['newProcessInfo' => $newProcessInfo]);
     }
 }
diff --git a/tests/ElasticApmTests/TestsSharedCode/TransactionMaxSpansTest/SharedCode.php b/tests/ElasticApmTests/TestsSharedCode/TransactionMaxSpansTest/SharedCode.php
index 3dbf1450..362ad502 100644
--- a/tests/ElasticApmTests/TestsSharedCode/TransactionMaxSpansTest/SharedCode.php
+++ b/tests/ElasticApmTests/TestsSharedCode/TransactionMaxSpansTest/SharedCode.php
@@ -26,7 +26,6 @@ namespace ElasticApmTests\TestsSharedCode\TransactionMaxSpansTest;
 use Ds\Set;
 use Elastic\Apm\Impl\Config\OptionDefaultValues;
 use Elastic\Apm\Impl\Log\LoggableToString;
-use Elastic\Apm\Impl\SpanData;
 use Elastic\Apm\Impl\Util\StaticClassTrait;
 use Elastic\Apm\TransactionInterface;
 use ElasticApmTests\TestsSharedCode\EventsFromAgent;
@@ -153,8 +152,8 @@ final class SharedCode
      */
     public static function testArgsVariants(bool $isFullTestingMode): iterable
     {
-        /** @var ?int */
-        $limitVariousCombinationsToVariantIndex = null;
+        // /** @var ?int */
+        // $limitVariousCombinationsToVariantIndex = null;
 
         $variantIndex = 1;
         foreach ([true, false] as $isSampled) {
@@ -190,7 +189,7 @@ final class SharedCode
                 $msg = 'LIMITED to variant #'
                        . self::LIMIT_TO_VARIANT_INDEX . ' out of '
                        . $testArgsVariantsCount;
-                fwrite(STDERR, PHP_EOL . __METHOD__ . ': ' . $msg . PHP_EOL);
+                TestCaseBase::printMessage(__METHOD__, $msg);
             }
         }
 
@@ -206,7 +205,7 @@ final class SharedCode
         if ($shouldPrintProgress) {
             $msg = 'variant #' . $testArgs->variantIndex . ' out of ' . $testArgsVariantsCount
                    . ': ' . LoggableToString::convert($testArgs);
-            fwrite(STDERR, PHP_EOL . __METHOD__ . ': ' . $msg . PHP_EOL);
+            TestCaseBase::printMessage(__METHOD__, $msg);
         }
 
         return $isThisVariantEnabled;
@@ -241,10 +240,6 @@ final class SharedCode
         TestCase::assertSame($expectedDroppedSpansCount, $tx->droppedSpansCount, $msg);
         TestCase::assertCount($expectedStartedSpansCount, $eventsFromAgent->idToSpan, $msg);
 
-        /** @var ?SpanData $spanMissingChildren */
-        $spanMissingChildren = null;
-
-        /** @var SpanData $span */
         foreach ($eventsFromAgent->idToSpan as $span) {
             $msg2 = $msg . ' spanId: ' . $span->id . '.';
             TestCaseBase::assertHasLabel($span, AppCode::NUMBER_OF_CHILD_SPANS_LABEL_KEY, $msg2);
diff --git a/tests/ElasticApmTests/Util/Deserialization/ServiceDataDeserializer.php b/tests/ElasticApmTests/Util/Deserialization/ServiceDataDeserializer.php
index 11b90b75..e05275da 100644
--- a/tests/ElasticApmTests/Util/Deserialization/ServiceDataDeserializer.php
+++ b/tests/ElasticApmTests/Util/Deserialization/ServiceDataDeserializer.php
@@ -64,22 +64,14 @@ final class ServiceDataDeserializer extends DataDeserializer
     protected function deserializeKeyValue(string $key, $value): bool
     {
         switch ($key) {
-            case 'name':
-                $this->result->name = ValidationUtil::assertValidServiceName($value);
-                return true;
-
-            case 'version':
-                $this->result->version = ValidationUtil::assertValidKeywordString($value);
+            case 'agent':
+                $this->result->agent = ServiceAgentDataDeserializer::deserialize($value);
                 return true;
 
             case 'environment':
                 $this->result->environment = ValidationUtil::assertValidKeywordString($value);
                 return true;
 
-            case 'agent':
-                $this->result->agent = ServiceAgentDataDeserializer::deserialize($value);
-                return true;
-
             case 'framework':
                 $this->result->framework = NameVersionDataDeserializer::deserialize($value);
                 return true;
@@ -88,12 +80,41 @@ final class ServiceDataDeserializer extends DataDeserializer
                 $this->result->language = NameVersionDataDeserializer::deserialize($value);
                 return true;
 
+            case 'name':
+                $this->result->name = ValidationUtil::assertValidServiceName($value);
+                return true;
+
+            case 'node':
+                $this->deserializeNodeSubObject($value);
+                return true;
+
             case 'runtime':
                 $this->result->runtime = NameVersionDataDeserializer::deserialize($value);
                 return true;
 
+            case 'version':
+                $this->result->version = ValidationUtil::assertValidKeywordString($value);
+                return true;
+
             default:
                 return false;
         }
     }
+
+    /**
+     * @param array<string, mixed> $deserializedRawData
+     */
+    private function deserializeNodeSubObject(array $deserializedRawData): void
+    {
+        foreach ($deserializedRawData as $key => $value) {
+            switch ($key) {
+                case 'configured_name':
+                    $this->result->nodeConfiguredName = ValidationUtil::assertValidKeywordString($value);
+                    break;
+
+                default:
+                    throw DataDeserializer::buildException("Unknown key: node->`$key'");
+            }
+        }
+    }
 }
diff --git a/tests/ElasticApmTests/Util/Deserialization/TransactionDataDeserializer.php b/tests/ElasticApmTests/Util/Deserialization/TransactionDataDeserializer.php
index adc465c1..b2a6fd5d 100644
--- a/tests/ElasticApmTests/Util/Deserialization/TransactionDataDeserializer.php
+++ b/tests/ElasticApmTests/Util/Deserialization/TransactionDataDeserializer.php
@@ -81,7 +81,7 @@ final class TransactionDataDeserializer extends ExecutionSegmentDataDeserializer
                 return true;
 
             case 'span_count':
-                $this->deserializeSpanCountObject($value);
+                $this->deserializeSpanCountSubObject($value);
                 return true;
 
             case 'sampled':
@@ -96,7 +96,7 @@ final class TransactionDataDeserializer extends ExecutionSegmentDataDeserializer
     /**
      * @param array<string, mixed> $deserializedRawData
      */
-    private function deserializeSpanCountObject(array $deserializedRawData): void
+    private function deserializeSpanCountSubObject(array $deserializedRawData): void
     {
         foreach ($deserializedRawData as $key => $value) {
             switch ($key) {
diff --git a/tests/ElasticApmTests/Util/TestCaseBase.php b/tests/ElasticApmTests/Util/TestCaseBase.php
index a77b52b2..361670fc 100644
--- a/tests/ElasticApmTests/Util/TestCaseBase.php
+++ b/tests/ElasticApmTests/Util/TestCaseBase.php
@@ -43,7 +43,6 @@ use Elastic\Apm\Impl\Util\ArrayUtil;
 use Elastic\Apm\Impl\Util\DbgUtil;
 use Elastic\Apm\Impl\Util\TimeUtil;
 use ElasticApmTests\ComponentTests\Util\FlakyAssertions;
-use ElasticApmTests\UnitTests\Util\EmptyConfigRawSnapshotSource;
 use PHPUnit\Framework\Constraint\Exception as ConstraintException;
 use PHPUnit\Framework\Constraint\IsEqual;
 use PHPUnit\Framework\Constraint\LessThan;
@@ -583,8 +582,13 @@ class TestCaseBase extends TestCase
         yield [false];
     }
 
-    public static function printMessage(string $msg): void
+    public static function printMessage(string $srcMethod, string $msg): void
     {
-        fwrite(STDERR, PHP_EOL . $msg . PHP_EOL);
+        if (!defined('STDERR')) {
+            define('STDERR', fopen('php://stderr', 'w'));
+        }
+        if (defined('STDERR')) {
+            fwrite(STDERR, PHP_EOL . $srcMethod . ': ' . $msg . PHP_EOL);
+        }
     }
 }
diff --git a/tests/ElasticApmTests/Util/ValidationUtil.php b/tests/ElasticApmTests/Util/ValidationUtil.php
index ff8a54bb..ddaaea9c 100644
--- a/tests/ElasticApmTests/Util/ValidationUtil.php
+++ b/tests/ElasticApmTests/Util/ValidationUtil.php
@@ -620,6 +620,7 @@ final class ValidationUtil
     public static function assertValidServiceData(ServiceData $serviceData): void
     {
         self::assertValidServiceName($serviceData->name);
+        self::assertValidNullableKeywordString($serviceData->nodeConfiguredName);
         self::assertValidNullableKeywordString($serviceData->version);
         self::assertValidNullableKeywordString($serviceData->environment);