opentelemetry-php/tests/Unit/Config/SDK/Configuration/ConfigurationFactoryTest.php

263 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace OpenTelemetry\Test\Unit\Config\SDK\Configuration;
use BadMethodCallException;
use ExampleSDK\ComponentProvider;
use OpenTelemetry\Config\SDK\Configuration\ComponentPlugin;
use OpenTelemetry\Config\SDK\Configuration\ComponentProviderRegistry;
use OpenTelemetry\Config\SDK\Configuration\ConfigurationFactory;
use OpenTelemetry\Config\SDK\Configuration\Context;
use OpenTelemetry\Config\SDK\Configuration\Environment\ArrayEnvSource;
use OpenTelemetry\Config\SDK\Configuration\Environment\EnvSourceReader;
use OpenTelemetry\Config\SDK\Configuration\Environment\PhpIniEnvSource;
use OpenTelemetry\Config\SDK\Configuration\Environment\ServerEnvSource;
use OpenTelemetry\Config\SDK\Configuration\Internal;
use PHPUnit\Framework\Attributes\BackupGlobals;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Yaml\Yaml;
#[CoversClass(ConfigurationFactory::class)]
final class ConfigurationFactoryTest extends TestCase
{
public string $cacheDir;
public $properties;
public function setUp(): void
{
$this->cacheDir = __DIR__ . '/configurations';
}
public function tearDown(): void
{
array_map('unlink', array_filter((array) glob($this->cacheDir . '/*cache*')));
}
/**
* @psalm-suppress MissingTemplateParam
*/
public function test_env_substitution_spec_examples(): void
{
// see example https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/file-configuration.md#environment-variable-substitution
$factory = new ConfigurationFactory(
[],
new class() implements \OpenTelemetry\Config\SDK\Configuration\ComponentProvider {
public function createPlugin(array $properties, Context $context): mixed
{
throw new BadMethodCallException();
}
/**
* @psalm-suppress UndefinedInterfaceMethod,PossiblyNullReference
*/
public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition
{
$node = new ArrayNodeDefinition('env_substitution');
/** @phpstan-ignore-next-line */
$node
->children()
->scalarNode('string_key')->end()
->scalarNode('other_string_key')->end()
->scalarNode('another_string_key')->end()
->scalarNode('string_key_with_quoted_hex_value')->end()
->scalarNode('yet_another_string_key')->end()
->booleanNode('bool_key')->end()
->integerNode('int_key')->end()
->integerNode('int_key_with_unquoted_hex_value')->end()
->floatNode('float_key')->end()
->scalarNode('combo_string_key')->end()
->scalarNode('string_key_with_default')->end()
->variableNode('undefined_key')->end()
->variableNode('${STRING_VALUE}')->end()
->end()
;
return $node;
}
},
new EnvSourceReader([
new ArrayEnvSource([
'STRING_VALUE' => 'value',
'BOOl_VALUE' => 'true',
'INT_VALUE' => '1',
'FLOAT_VALUE' => '1.1',
'HEX_VALUE' => '0xdeadbeef',
'INVALID_MAP_VALUE' => "value\nkey:value",
]),
]),
);
/** @todo int_key_with_unquoted_hex_value is being interpreted as string */
$parsed = $factory->process([
Yaml::parse(<<<'YAML'
string_key: ${STRING_VALUE} # Valid reference to STRING_VALUE
other_string_key: "${STRING_VALUE}" # Valid reference to STRING_VALUE inside double quotes
another_string_key: "${BOOl_VALUE}" # Valid reference to BOOl_VALUE inside double quotes
string_key_with_quoted_hex_value: "${HEX_VALUE}" # Valid reference to HEX_VALUE inside double quotes
yet_another_string_key: ${INVALID_MAP_VALUE} # Valid reference to INVALID_MAP_VALUE, but YAML structure from INVALID_MAP_VALUE MUST NOT be injected
bool_key: ${BOOl_VALUE} # Valid reference to BOOl_VALUE
int_key: ${INT_VALUE} # Valid reference to INT_VALUE
#int_key_with_unquoted_hex_value: ${HEX_VALUE} # Valid reference to HEX_VALUE without quotes
float_key: ${FLOAT_VALUE} # Valid reference to FLOAT_VALUE
combo_string_key: foo ${STRING_VALUE} ${FLOAT_VALUE} # Valid reference to STRING_VALUE and FLOAT_VALUE
string_key_with_default: ${UNDEFINED_KEY:-fallback} # UNDEFINED_KEY is not defined but a default value is included
undefined_key: ${UNDEFINED_KEY} # Invalid reference, UNDEFINED_KEY is not defined and is replaced with ""
${STRING_VALUE}: value # Invalid reference, substitution is not valid in mapping keys and reference is ignored
YAML),
]);
$this->assertSame(
Yaml::parse(<<<'YAML'
string_key: value # Interpreted as type string, tag URI tag:yaml.org,2002:str
other_string_key: "value" # Interpreted as type string, tag URI tag:yaml.org,2002:str
another_string_key: "true" # Interpreted as type string, tag URI tag:yaml.org,2002:str
string_key_with_quoted_hex_value: "0xdeadbeef" # Interpreted as type string, tag URI tag:yaml.org,2002:str
yet_another_string_key: "value\nkey:value" # Interpreted as type string, tag URI tag:yaml.org,2002:str
bool_key: true # Interpreted as type bool, tag URI tag:yaml.org,2002:bool
int_key: 1 # Interpreted as type int, tag URI tag:yaml.org,2002:int
#int_key_with_unquoted_hex_value: 3735928559 # Interpreted as type int, tag URI tag:yaml.org,2002:int
float_key: 1.1 # Interpreted as type float, tag URI tag:yaml.org,2002:float
combo_string_key: foo value 1.1 # Interpreted as type string, tag URI tag:yaml.org,2002:str
string_key_with_default: fallback # Interpreted as type string, tag URI tag:yaml.org,2002:str
# undefined_key removed as null is treated as unset
# undefined_key: # Interpreted as type null, tag URI tag:yaml.org,2002:null
${STRING_VALUE}: value # Interpreted as type string, tag URI tag:yaml.org,2002:str
YAML),
self::getPropertiesFromPlugin($parsed),
);
}
#[BackupGlobals(true)]
#[CoversNothing]
public function test_env_substitution_string(): void
{
$_SERVER['OTEL_SERVICE_NAME'] = 'example-service';
$parsed = self::factory()->process([[
'file_format' => '0.1',
'resource' => [
'attributes' => [
'service.name' => '${OTEL_SERVICE_NAME}',
],
],
]]);
$this->assertInstanceOf(ComponentPlugin::class, $parsed);
$this->assertSame('example-service', self::getPropertiesFromPlugin($parsed)['resource']['attributes']['service.name']);
}
#[BackupGlobals(true)]
public function test_env_substitution_non_string(): void
{
$_SERVER['OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT'] = '2048';
$parsed = self::factory()->process([[
'file_format' => '0.1',
'attribute_limits' => [
'attribute_value_length_limit' => '${OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT}',
],
]]);
$this->assertInstanceOf(ComponentPlugin::class, $parsed);
$this->assertSame(2048, self::getPropertiesFromPlugin($parsed)['attribute_limits']['attribute_value_length_limit']);
}
public function test_treat_null_as_unset(): void
{
$parsed = self::factory()->process([[
'file_format' => '0.1',
'attribute_limits' => [
'attribute_count_limit' => null,
],
]]);
$this->assertInstanceOf(ComponentPlugin::class, $parsed);
$this->assertSame(128, self::getPropertiesFromPlugin($parsed)['attribute_limits']['attribute_count_limit']);
}
/**
* @psalm-suppress UndefinedThisPropertyFetch,PossiblyNullFunctionCall
*/
private function getPropertiesFromPlugin(ComponentPlugin $plugin): array
{
assert($plugin instanceof Internal\ComponentPlugin);
return (fn () => $this->properties)->bindTo($plugin, Internal\ComponentPlugin::class)();
}
public static function openTelemetryConfigurationDataProvider(): iterable
{
yield 'kitchen-sink' => [__DIR__ . '/configurations/kitchen-sink.yaml'];
yield 'anchors' => [__DIR__ . '/configurations/anchors.yaml'];
}
#[DataProvider('openTelemetryConfigurationDataProvider')]
public function test_open_telemetry_configuration(string $file): void
{
$this->expectNotToPerformAssertions();
self::factory()->parseFile($file);
}
private function factory(): ConfigurationFactory
{
return new ConfigurationFactory(
[
new ComponentProvider\Propagator\TextMapPropagatorB3(),
new ComponentProvider\Propagator\TextMapPropagatorB3Multi(),
new ComponentProvider\Propagator\TextMapPropagatorBaggage(),
new ComponentProvider\Propagator\TextMapPropagatorComposite(),
new ComponentProvider\Propagator\TextMapPropagatorJaeger(),
new ComponentProvider\Propagator\TextMapPropagatorOTTrace(),
new ComponentProvider\Propagator\TextMapPropagatorTraceContext(),
new ComponentProvider\Propagator\TextMapPropagatorXRay(),
new ComponentProvider\Trace\SamplerAlwaysOff(),
new ComponentProvider\Trace\SamplerAlwaysOn(),
new ComponentProvider\Trace\SamplerParentBased(),
new ComponentProvider\Trace\SamplerTraceIdRatioBased(),
new ComponentProvider\Trace\SpanExporterConsole(),
new ComponentProvider\Trace\SpanExporterOtlp(),
new ComponentProvider\Trace\SpanExporterZipkin(),
new ComponentProvider\Trace\SpanProcessorBatch(),
new ComponentProvider\Trace\SpanProcessorSimple(),
new ComponentProvider\Metrics\AggregationResolverDefault(),
new ComponentProvider\Metrics\AggregationResolverDrop(),
new ComponentProvider\Metrics\AggregationResolverExplicitBucketHistogram(),
new ComponentProvider\Metrics\AggregationResolverLastValue(),
new ComponentProvider\Metrics\AggregationResolverSum(),
new ComponentProvider\Metrics\MetricExporterConsole(),
new ComponentProvider\Metrics\MetricExporterOtlp(),
new ComponentProvider\Metrics\MetricExporterPrometheus(),
new ComponentProvider\Metrics\MetricReaderPeriodic(),
new ComponentProvider\Metrics\MetricReaderPull(),
new ComponentProvider\Logs\LogRecordExporterConsole(),
new ComponentProvider\Logs\LogRecordExporterOtlp(),
new ComponentProvider\Logs\LogRecordProcessorBatch(),
new ComponentProvider\Logs\LogRecordProcessorSimple(),
],
new ComponentProvider\OpenTelemetryConfiguration(),
new EnvSourceReader([
new ServerEnvSource(),
new PhpIniEnvSource(),
]),
);
}
public function test_cache_configuration(): void
{
$file = $this->cacheDir . '/kitchen-sink.yaml';
$cacheFile = $this->cacheDir . '/kitchen-sink.cache';
$this->assertFalse(file_exists($cacheFile), 'cache does not initially exist');
$plugin = self::factory()->parseFile($file, $cacheFile);
$this->assertTrue(file_exists($cacheFile));
$fromCache = self::factory()->parseFile($file, $cacheFile);
$this->assertEquals($fromCache, $plugin);
}
}