Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 74 additions & 69 deletions documentation/components/bridges/phpunit-telemetry-bridge.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions documentation/components/libs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,10 @@ The `EnvironmentDetector` reads standard OpenTelemetry environment variables:
- `OTEL_SERVICE_NAME` - Sets the `service.name` attribute
- `OTEL_RESOURCE_ATTRIBUTES` - Sets additional attributes in `key=value,key2=value2` format

Per the OpenTelemetry Resource SDK specification, `,` and `=` inside keys and values MUST be
percent-encoded (other characters MAY be); both keys and values are percent-decoded. For example,
`note=a%2Cb` yields the attribute `note` with value `a,b`.

```bash
export OTEL_SERVICE_NAME=my-service
export OTEL_RESOURCE_ATTRIBUTES=service.version=1.0.0,deployment.environment.name=production
Expand Down
19 changes: 15 additions & 4 deletions documentation/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ to_pgsql_table($client, 'users')->withTypesMap(new EntryTypesMap(
));
```

### 6) `flow-php/symfony-postgresql-messenger` - `messenger_messages` time columns use `timestamp` instead of `timestamptz`
### 6) `flow-php/symfony-postgresql-messenger` - `messenger_messages` time columns use `timestamp` instead of
`timestamptz`

| Column type for `created_at`, `available_at`, `delivered_at` | Before | After |
|--------------------------------------------------------------|---------------|-------------|
Expand All @@ -132,11 +133,21 @@ Existing tables, realign the column type (UTC instants preserved):

```sql
ALTER TABLE messenger_messages
ALTER COLUMN created_at TYPE timestamp USING created_at AT TIME ZONE 'UTC',
ALTER COLUMN available_at TYPE timestamp USING available_at AT TIME ZONE 'UTC',
ALTER COLUMN delivered_at TYPE timestamp USING delivered_at AT TIME ZONE 'UTC';
ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC',
ALTER COLUMN available_at TYPE TIMESTAMP USING available_at AT TIME ZONE 'UTC',
ALTER COLUMN delivered_at TYPE TIMESTAMP USING delivered_at AT TIME ZONE 'UTC';
```

### 7) `flow-php/telemetry` - `OTEL_RESOURCE_ATTRIBUTES` keys and values are percent-decoded, not backslash-escaped

| Escaping a `,` or `=` in `OTEL_RESOURCE_ATTRIBUTES` | Before | After |
|-----------------------------------------------------|---------------------------|-----------------------------|
| literal comma in a value | `key=value\,with\,commas` | `key=value%2Cwith%2Ccommas` |
| literal `=` in a value | not supported | `key=a%3Db` |

Re-encode any `OTEL_RESOURCE_ATTRIBUTES` that relied on backslash escaping; both keys and values are now
percent-decoded.

---

## Upgrading from 0.37.x to 0.38.x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@
'error_handler_severity',
];

/**
* @param array<string, string> $resourceAttributes
*/
public function __construct(
public string $serviceName,
public CurlTransportConfig|GrpcTransportConfig|StreamTransportConfig $transport,
Expand All @@ -138,6 +141,7 @@ public function __construct(
public int $batchSize,
public ErrorLogHandlerConfig|NullErrorHandlerConfig|StreamErrorHandlerConfig|SyslogErrorHandlerConfig|UdpSyslogErrorHandlerConfig $errorHandler,
public bool $memoryRealUsage = false,
public array $resourceAttributes = [],
) {}

public static function fromParameters(ParameterCollection $parameters): self
Expand Down Expand Up @@ -168,6 +172,7 @@ public static function fromParameters(ParameterCollection $parameters): self
batchSize: self::resolveInt($parameters, 'batch_size', self::DEFAULT_BATCH_SIZE),
errorHandler: self::resolveErrorHandler($parameters),
memoryRealUsage: self::resolveBool($parameters, 'memory_real_usage', false),
resourceAttributes: self::parseResourceAttributes(self::resolve($parameters, 'resource_attributes') ?? ''),
);
}

Expand Down Expand Up @@ -280,6 +285,44 @@ private static function parseHeaders(string $raw): array
return $headers;
}

/**
* @return array<string, string>
*/
private static function parseResourceAttributes(string $raw): array
{
if ($raw === '') {
return [];
}

$attributes = [];

foreach (explode(',', $raw) as $entry) {
$entry = trim($entry);

if ($entry === '') {
continue;
}

if (!str_contains($entry, '=')) {
throw new InvalidArgumentException(sprintf(
'Invalid resource attribute entry "%s", expected format "name=value".',
$entry,
));
}

[$name, $value] = explode('=', $entry, 2);
$name = urldecode(trim($name));

if ($name === '') {
throw new InvalidArgumentException('Resource attribute name cannot be empty.');
}

$attributes[$name] = urldecode($value);
}

return $attributes;
}

private static function readEnv(string $fullName): ?string
{
$value = $_ENV[$fullName] ?? $_SERVER[$fullName] ?? getenv($fullName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public static function create(Configuration $config): Telemetry
'service.name' => $config->serviceName,
'telemetry.sdk.name' => 'flow-php-phpunit-telemetry',
'telemetry.sdk.language' => 'php',
]));
]))
->merge(resource($config->resourceAttributes));

$clock = new SystemClock();
$contextStorage = memory_context_storage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,22 @@ public static function withRealMemoryUsage(): Configuration
memoryRealUsage: true,
);
}

/**
* @param array<string, string> $resourceAttributes
*/
public static function withResourceAttributes(array $resourceAttributes): Configuration
{
return new Configuration(
serviceName: 'phpunit',
transport: self::defaultTransport(),
emitTraces: true,
emitMetrics: true,
emitTestSpans: true,
emitTestCaseSpans: true,
batchSize: Configuration::DEFAULT_BATCH_SIZE,
errorHandler: self::defaultErrorHandler(),
resourceAttributes: $resourceAttributes,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ public function test_default_configuration_uses_curl_transport_with_localhost_en
static::assertTrue($config->emitTestSpans);
static::assertTrue($config->emitTestCaseSpans);
static::assertFalse($config->memoryRealUsage);
static::assertSame([], $config->resourceAttributes);

static::assertInstanceOf(CurlTransportConfig::class, $config->transport);
static::assertSame('http://localhost:4318', $config->transport->endpoint);
Expand Down Expand Up @@ -710,6 +711,89 @@ public function test_noop_error_handler_rejects_specific_params(): void
]));
}

public function test_resource_attributes_duplicate_name_last_wins(): void
{
$config = Configuration::fromParameters(ParameterCollection::fromArray([
'resource_attributes' => 'team=first,team=second',
]));

static::assertSame(['team' => 'second'], $config->resourceAttributes);
}

public function test_resource_attributes_empty_name_throws(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Resource attribute name cannot be empty');

Configuration::fromParameters(ParameterCollection::fromArray([
'resource_attributes' => '=value',
]));
}

public function test_resource_attributes_empty_string_is_empty_array(): void
{
$config = Configuration::fromParameters(ParameterCollection::fromArray([
'resource_attributes' => '',
]));

static::assertSame([], $config->resourceAttributes);
}

public function test_resource_attributes_env_var_wins_over_parameter(): void
{
putenv('FLOW_PHPUNIT_OTEL_RESOURCE_ATTRIBUTES=service.version=2.0.0');

try {
$config = Configuration::fromParameters(ParameterCollection::fromArray([
'resource_attributes' => 'service.version=1.0.0',
]));

static::assertSame(['service.version' => '2.0.0'], $config->resourceAttributes);
} finally {
putenv('FLOW_PHPUNIT_OTEL_RESOURCE_ATTRIBUTES');
}
}

public function test_resource_attributes_missing_equals_throws(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid resource attribute entry "team", expected format "name=value"');

Configuration::fromParameters(ParameterCollection::fromArray([
'resource_attributes' => 'team',
]));
}

public function test_resource_attributes_multiple_pairs_parsed(): void
{
$config = Configuration::fromParameters(ParameterCollection::fromArray([
'resource_attributes' => 'service.version=1.0.0,deployment.environment.name=ci',
]));

static::assertSame(
['service.version' => '1.0.0', 'deployment.environment.name' => 'ci'],
$config->resourceAttributes,
);
}

public function test_resource_attributes_single_pair_parsed(): void
{
$config = Configuration::fromParameters(ParameterCollection::fromArray([
'resource_attributes' => 'service.version=1.0.0',
]));

static::assertSame(['service.version' => '1.0.0'], $config->resourceAttributes);
}

public function test_resource_attributes_url_encoded_value_decoded(): void
{
$config = Configuration::fromParameters(ParameterCollection::fromArray([
'resource_attributes' => 'note=a%2Cb%20c',
]));

static::assertSame(['note' => 'a,b c'], $config->resourceAttributes);
}

public function test_server_superglobal_wins_over_getenv(): void
{
$_SERVER['FLOW_PHPUNIT_OTEL_ENDPOINT'] = 'https://from-server:4318';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ public function test_create_with_null_error_handler(): void
$telemetry->shutdown();
}

public function test_create_with_resource_attributes(): void
{
$telemetry = TelemetryFactory::create(ConfigurationMother::withResourceAttributes([
'service.version' => '1.0.0',
'deployment.environment.name' => 'ci',
]));

static::assertInstanceOf(Telemetry::class, $telemetry);
$telemetry->shutdown();
}

public function test_create_with_stream_error_handler(): void
{
$config = $this->configWithErrorHandler(new StreamErrorHandlerConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@
use Flow\Telemetry\Resource\Attribute\ServiceAttribute;
use Flow\Telemetry\Resource\ResourceDetector;

use function count;
use function explode;
use function getenv;
use function stripslashes;
use function strlen;
use function strpos;
use function substr;
use function trim;
use function urldecode;

/**
* Detects resource attributes from environment variables.
Expand All @@ -28,9 +27,11 @@
* OTEL_RESOURCE_ATTRIBUTES=key1=value1,key2=value2
* ```
*
* Special characters in values can be escaped with backslash:
* Per the OpenTelemetry Resource SDK specification, the `,` and `=` characters in keys and
* values MUST be percent-encoded (other characters MAY be percent-encoded); both keys and
* values are percent-decoded:
* ```
* OTEL_RESOURCE_ATTRIBUTES=key=value\,with\,commas
* OTEL_RESOURCE_ATTRIBUTES=key=value%2Cwith%2Ccommas
* ```
*
* OTEL_SERVICE_NAME takes precedence over service.name in OTEL_RESOURCE_ATTRIBUTES.
Expand Down Expand Up @@ -83,9 +84,8 @@ private function parseResourceAttributes(): array
}

$attributes = [];
$pairs = $this->splitByComma($rawAttributes);

foreach ($pairs as $pair) {
foreach (explode(',', $rawAttributes) as $pair) {
$pair = trim($pair);

if ($pair === '') {
Expand All @@ -98,58 +98,15 @@ private function parseResourceAttributes(): array
continue;
}

$key = trim(substr($pair, 0, $equalsPos));
$value = trim(substr($pair, $equalsPos + 1));
$key = urldecode(trim(substr($pair, 0, $equalsPos)));

if ($key === '') {
continue;
}

$value = stripslashes($value);
$attributes[$key] = $value;
$attributes[$key] = urldecode(trim(substr($pair, $equalsPos + 1)));
}

return $attributes;
}

/**
* Split a string by commas, respecting escaped commas.
*
* @return array<string>
*/
private function splitByComma(string $input): array
{
$result = [];
$current = '';
$length = strlen($input);
$i = 0;

while ($i < $length) {
$char = $input[$i];

if ($char === '\\' && ($i + 1) < $length) {
$current .= $char . $input[$i + 1];
$i += 2;

continue;
}

if ($char === ',') {
$result[] = $current;
$current = '';
$i++;

continue;
}

$current .= $char;
$i++;
}

if ($current !== '' || count($result) > 0) {
$result[] = $current;
}

return $result;
}
}
Loading
Loading