agent/php/ElasticApm/Impl/BackendComm/EventSender.php (102 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\BackendComm;
use Elastic\Apm\ElasticApm;
use Elastic\Apm\Impl\BreakdownMetrics\PerTransaction as BreakdownMetricsPerTransaction;
use Elastic\Apm\Impl\Config\DevInternalSubOptionNames;
use Elastic\Apm\Impl\Config\OptionNames;
use Elastic\Apm\Impl\Config\Snapshot as ConfigSnapshot;
use Elastic\Apm\Impl\EventSinkInterface;
use Elastic\Apm\Impl\Log\LogCategory;
use Elastic\Apm\Impl\Log\Logger;
use Elastic\Apm\Impl\Log\LoggerFactory;
use Elastic\Apm\Impl\Metadata;
use Elastic\Apm\Impl\MetricSet;
use Elastic\Apm\Impl\Transaction;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*/
final class EventSender implements EventSinkInterface
{
public const AGENT_GITHUB_REPO_NAME = 'apm-agent-php';
/** @var Logger */
private $logger;
/** @var ConfigSnapshot */
private $config;
/** @var ?string */
private $userAgentHttpHeader = null;
public function __construct(ConfigSnapshot $config, LoggerFactory $loggerFactory)
{
$this->config = $config;
$this->logger = $loggerFactory->loggerForClass(LogCategory::BACKEND_COMM, __NAMESPACE__, __CLASS__, __FILE__);
$this->logger->addContext('this', $this);
}
public static function buildUserAgentHttpHeader(string $serviceName, ?string $serviceVersion): string
{
// https://github.com/elastic/apm/blob/main/specs/agents/transport.md#user-agent
// Header value should start with agent github repository as prefix and version:
// apm-agent-${language}/${agent.version}.
// If both service.name and service.version are set, append (${service.name} ${service.version})
// If only service.name is set, append (${service.name})
//
// Examples:
// apm-agent-java/v1.25.0
// apm-agent-ruby/4.4.0 (my_service)
// apm-agent-python/6.4.0 (my_service v42.7)
$headerValue = self::AGENT_GITHUB_REPO_NAME . '/' . ElasticApm::VERSION;
$serviceNameVersionSuffix = $serviceName;
// Escape characters not allowed in User-Agent. Taken from
// https://github.com/elastic/apm-agent-nodejs/blob/52c8f27a379b2f1914c41aaef7e54ac2cb4c92f8/lib/config.js#L844
$serviceNameVersionSuffix .= ($serviceVersion === null)
? ''
: (' ' . preg_replace('/[^\t \x21-\x27\x2a-\x5b\x5d-\x7e\x80-\xff]/', '_', $serviceVersion));
$headerValue .= ' (' . $serviceNameVersionSuffix . ')';
return $headerValue;
}
/** @inheritDoc */
public function consume(
Metadata $metadata,
array $spans,
array $errors,
?BreakdownMetricsPerTransaction $breakdownMetricsPerTransaction,
?Transaction $transaction
): void {
if ($this->userAgentHttpHeader === null) {
$this->userAgentHttpHeader = self::buildUserAgentHttpHeader(
$metadata->service->name,
$metadata->service->version
);
}
$serializedMetadata = '{"metadata":';
$serializedMetadata .= SerializationUtil::serializeAsJson($metadata);
$serializedMetadata .= "}";
$serializedEvents = $serializedMetadata;
foreach ($spans as $span) {
$serializedEvents .= "\n";
$serializedEvents .= '{"span":';
$serializedEvents .= SerializationUtil::serializeAsJson($span);
$serializedEvents .= '}';
}
foreach ($errors as $error) {
$serializedEvents .= "\n";
$serializedEvents .= '{"error":';
$serializedEvents .= SerializationUtil::serializeAsJson($error);
$serializedEvents .= '}';
}
if ($breakdownMetricsPerTransaction !== null) {
$breakdownMetricsPerTransaction->forEachMetricSet(
function (MetricSet $metricSet) use (&$serializedEvents) {
$serializedEvents .= "\n";
$serializedEvents .= '{"metricset":';
$serializedEvents .= SerializationUtil::serializeAsJson($metricSet);
$serializedEvents .= '}';
}
);
}
if ($transaction !== null) {
$serializedEvents .= "\n";
$serializedEvents .= '{"transaction":';
$serializedEvents .= SerializationUtil::serializeAsJson($transaction);
$serializedEvents .= "}";
}
if ($this->config->devInternal()->dropEventsBeforeSendCCode()) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Dropping events because '
. OptionNames::DEV_INTERNAL . ' sub-option ' . DevInternalSubOptionNames::DROP_EVENTS_BEFORE_SEND_C_CODE
. ' is set'
);
} else {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Calling elastic_apm_send_to_server...',
[
'userAgentHttpHeader' => $this->userAgentHttpHeader,
'strlen(serializedEvents)' => strlen($serializedEvents),
]
);
/**
* elastic_apm_* functions are provided by the elastic_apm extension
*
* @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection
* @phpstan-ignore-next-line
*/
\elastic_apm_send_to_server($this->userAgentHttpHeader, $serializedEvents);
}
}
}