agent/php/ElasticApm/Impl/MetadataDiscoverer.php (186 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;
use Elastic\Apm\ElasticApm;
use Elastic\Apm\Impl\Config\Snapshot as ConfigSnapshot;
use Elastic\Apm\Impl\Log\LogCategory;
use Elastic\Apm\Impl\Log\Logger;
use Elastic\Apm\Impl\Log\LoggerFactory;
use Elastic\Apm\Impl\Util\TextUtil;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*/
final class MetadataDiscoverer
{
public const AGENT_NAME = 'php';
public const LANGUAGE_NAME = 'PHP';
// https://github.com/elastic/apm/blob/main/specs/agents/configuration.md#zero-configuration-support
// ... the default value: unknown-${service.agent.name}-service ...
public const DEFAULT_SERVICE_NAME = 'unknown-php-service';
/** @var ConfigSnapshot */
private $config;
/** @var Logger */
private $logger;
/** @var array<string, callable(): ?NameVersionData> */
private $serviceFrameworkDiscoverers = [];
/** @var ?string */
private $agentEphemeralId = null;
public function __construct(ConfigSnapshot $config, LoggerFactory $loggerFactory)
{
$this->config = $config;
$this->logger = $loggerFactory->loggerForClass(LogCategory::DISCOVERY, __NAMESPACE__, __CLASS__, __FILE__);
}
/**
* @param string $dbgDiscovererName
* @param callable(): ?NameVersionData $dbgDiscoverCall
*
* @return void
*/
public function addServiceFrameworkDiscoverer(string $dbgDiscovererName, callable $dbgDiscoverCall): void
{
$this->serviceFrameworkDiscoverers[$dbgDiscovererName] = $dbgDiscoverCall;
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Added', ['dbgDiscovererName' => $dbgDiscovererName]);
}
public function setAgentEphemeralId(?string $agentEphemeralId): void
{
$this->agentEphemeralId = $agentEphemeralId;
}
public function discover(): Metadata
{
$result = new Metadata();
$result->labels = $this->config->globalLabels();
$result->process = $this->discoverProcessData();
$result->service = $this->discoverServiceData();
$result->system = $this->discoverSystemData();
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Exiting ...', ['result' => $result]);
return $result;
}
public static function adaptServiceName(string $configuredName): string
{
if (TextUtil::isEmptyString($configuredName)) {
return self::DEFAULT_SERVICE_NAME;
}
$charsAdaptedName = preg_replace('/[^a-zA-Z0-9 _\-]/', '_', $configuredName);
return $charsAdaptedName === null ? MetadataDiscoverer::DEFAULT_SERVICE_NAME : Tracer::limitKeywordString($charsAdaptedName);
}
private static function setKeywordStringIfNotNull(?string $srcCfgVal, ?string &$dstProp): void
{
if ($srcCfgVal !== null) {
$dstProp = Tracer::limitKeywordString($srcCfgVal);
}
}
private function discoverServiceData(): ServiceData
{
$result = new ServiceData();
self::setKeywordStringIfNotNull($this->config->environment(), /* ref */ $result->environment);
$result->name = $this->config->serviceName() === null ? MetadataDiscoverer::DEFAULT_SERVICE_NAME : MetadataDiscoverer::adaptServiceName($this->config->serviceName());
self::setKeywordStringIfNotNull($this->config->serviceNodeName(), /* ref */ $result->nodeConfiguredName);
self::setKeywordStringIfNotNull($this->config->serviceVersion(), /* ref */ $result->version);
$result->agent = new ServiceAgentData();
$result->agent->name = self::AGENT_NAME;
$result->agent->version = ElasticApm::VERSION;
$result->agent->ephemeralId = Tracer::limitNullableKeywordString($this->agentEphemeralId);
$result->language = new NameVersionData(MetadataDiscoverer::LANGUAGE_NAME, PHP_VERSION);
$result->runtime = $result->language;
$result->framework = $this->discoverServiceFramework();
return $result;
}
private function discoverServiceFramework(): ?NameVersionData
{
$loggerProxyDebug = $this->logger->ifDebugLevelEnabledNoLine(__FUNCTION__);
/** @var ?NameVersionData $result */
$result = null;
/** @var ?string $resultFrom */
$resultFrom = null;
foreach ($this->serviceFrameworkDiscoverers as $currentDbgDiscovererName => $currentDiscoverCall) {
if (($currentDiscovererResult = $currentDiscoverCall()) === null) {
$loggerProxyDebug && $loggerProxyDebug->log(__LINE__, $currentDbgDiscovererName . ' did not discover service framework');
continue;
}
$loggerProxyDebug && $loggerProxyDebug->log(__LINE__, $currentDbgDiscovererName . ' discovered service framework', ['result' => $currentDiscovererResult]);
if ($result !== null) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'More than one discover returned a non-null result',
['1st' => ['result' => $result, 'from' => $resultFrom], '2nd' => ['result' => $currentDiscovererResult, 'from' => $currentDbgDiscovererName]]
);
return null;
}
$result = $currentDiscovererResult;
$resultFrom = $currentDbgDiscovererName;
}
$loggerProxyDebug && $loggerProxyDebug->log(__LINE__, 'Exiting...', ['result' => $result, 'from' => $resultFrom]);
return $result;
}
private function discoverSystemData(): SystemData
{
$result = new SystemData();
$configuredHostname = $this->config->hostname();
if ($configuredHostname !== null) {
$result->configuredHostname = Tracer::limitKeywordString($configuredHostname);
$result->hostname = $result->configuredHostname;
} else {
$detectedHostname = self::discoverHostname();
if ($detectedHostname !== null) {
$result->detectedHostname = $detectedHostname;
$result->hostname = $detectedHostname;
}
}
$result->containerId = $this->discoverContainerId();
$result->architecture = $this->discoverSystemArchitecture();
$result->platform = $this->discoverSystemPlatform();
return $result;
}
public static function discoverHostname(): ?string
{
$detected = gethostname();
if ($detected === false) {
return null;
}
return Tracer::limitKeywordString($detected);
}
private const DETECT_CONTAINER_ID_FILENAME_TI_REGEX = [
'/proc/self/mountinfo' => '/\/var\/lib\/docker\/containers\/([0-9a-f]+)\/hostname/m',
'/proc/self/cgroup' => '/\/docker\/([0-9a-f]+)$/m',
];
/**
* @param callable(string $fileName): ?string $getFileContents
*
* @return ?string
*/
public function discoverContainerIdImpl(callable $getFileContents): ?string
{
$loggerPxyDbg = $this->logger->ifDebugLevelEnabledNoLine(__FUNCTION__);
foreach (self::DETECT_CONTAINER_ID_FILENAME_TI_REGEX as $fileName => $regex) {
if (($fileContents = $getFileContents($fileName)) !== null) {
if (preg_match($regex, $fileContents, $matches)) {
$loggerPxyDbg && $loggerPxyDbg->log(__LINE__, 'Found container ID in ' . $fileName, ['found container ID' => $matches[1], 'fileContents' => $fileContents, 'regex' => $regex]);
return $matches[1];
}
$loggerPxyDbg && $loggerPxyDbg->log(__LINE__, 'Could not find container ID in ' . $fileName, ['fileContents' => $fileContents, 'regex' => $regex]);
}
}
$loggerPxyDbg && $loggerPxyDbg->log(__LINE__, 'Could not find container ID anywhere');
return null;
}
private function getFileContentsForDetectContainerId(string $fileName): ?string
{
if (!file_exists($fileName)) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('File ' . $fileName . ' does not exit');
return null;
}
$contents = file_get_contents($fileName);
if ($contents === false) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Failed to get ' . $fileName . ' contents');
return null;
}
return $contents;
}
private function discoverContainerId(): ?string
{
return self::discoverContainerIdImpl(
function (string $fileName): ?string {
return $this->getFileContentsForDetectContainerId($fileName);
}
);
}
private function discoverProcessData(): ProcessData
{
$result = new ProcessData();
$currentProcessId = getmypid();
if ($currentProcessId === false) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Failed to get current process ID - setting it to 0 instead');
$result->pid = 0;
} else {
$result->pid = $currentProcessId;
}
return $result;
}
private function discoverSystemArchitecture(): string
{
return php_uname('m');
}
private function discoverSystemPlatform(): string
{
return php_uname('s');
}
}