Skip to content

Conversation

@cedricziel
Copy link
Contributor

@cedricziel cedricziel commented Apr 1, 2025

OpenTelemetry Test Utilities

This package provides testing utilities for OpenTelemetry PHP instrumentations. It includes tools to help test and validate trace structures, span relationships, and other OpenTelemetry-specific functionality.

Features

TraceStructureAssertionTrait

The TraceStructureAssertionTrait provides methods to assess if spans match an expected trace structure. It's particularly useful for testing complex trace hierarchies and relationships between spans.

Key features:

  • Support for hierarchical span relationships
  • Verification of span names, kinds, attributes, events, and status
  • Flexible matching with strict and non-strict modes
  • Support for PHPUnit matchers/constraints for more flexible assertions
  • Detailed error messages for failed assertions
  • Two interfaces: array-based and fluent

Requirements

  • PHP 7.4 or higher
  • OpenTelemetry SDK and API (for testing)
  • PHPUnit 9.5 or higher

Usage

TraceStructureAssertionTrait

Add the trait to your test class:

use OpenTelemetry\TestUtils\TraceStructureAssertionTrait;
use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    use TraceStructureAssertionTrait;

    // Your test methods...
}

Array-Based Interface

Use the assertTraceStructure method to verify trace structures using an array-based approach:

public function testTraceStructure(): void
{
    // Create spans using the OpenTelemetry SDK
    // ...

    // Define the expected structure
    $expectedStructure = [
        [
            'name' => 'root-span',
            'kind' => SpanKind::KIND_SERVER,
            'children' => [
                [
                    'name' => 'child-span',
                    'kind' => SpanKind::KIND_INTERNAL,
                    'attributes' => [
                        'attribute.one' => 'value1',
                        'attribute.two' => 42,
                    ],
                    'events' => [
                        [
                            'name' => 'event.processed',
                            'attributes' => [
                                'processed.id' => 'abc123',
                            ],
                        ],
                    ],
                ],
                [
                    'name' => 'another-child-span',
                    'kind' => SpanKind::KIND_CLIENT,
                    'status' => [
                        'code' => StatusCode::STATUS_ERROR,
                        'description' => 'Something went wrong',
                    ],
                ],
            ],
        ],
    ];

    // Assert the trace structure
    $this->assertTraceStructure($spans, $expectedStructure);
}

The assertTraceStructure method takes the following parameters:

  • $spans: An array or ArrayObject of spans (typically from an InMemoryExporter)
  • $expectedStructure: An array defining the expected structure of the trace
  • $strict (optional): Whether to perform strict matching (all attributes must match)

Fluent Interface

Use the assertTrace method to verify trace structures using a fluent, chainable interface:

public function testTraceStructure(): void
{
    // Create spans using the OpenTelemetry SDK
    // ...

    // Assert the trace structure using the fluent interface
    $this->assertTrace($spans)
        ->hasRootSpan('root-span')
            ->withKind(SpanKind::KIND_SERVER)
            ->hasChild('child-span')
                ->withKind(SpanKind::KIND_INTERNAL)
                ->withAttribute('attribute.one', 'value1')
                ->withAttribute('attribute.two', 42)
                ->hasEvent('event.processed')
                    ->withAttribute('processed.id', 'abc123')
                ->end()
            ->end()
            ->hasChild('another-child-span')
                ->withKind(SpanKind::KIND_CLIENT)
                ->withStatus(StatusCode::STATUS_ERROR, 'Something went wrong')
            ->end()
        ->end();
}

The fluent interface provides the following methods:

TraceAssertion:

  • hasRootSpan(string|Constraint $name): Assert that the trace has a root span with the given name
  • hasRootSpans(int $count): Assert that the trace has the expected number of root spans
  • inStrictMode(): Enable strict mode for all assertions

SpanAssertion:

  • withKind(int|Constraint $kind): Assert that the span has the expected kind
  • withAttribute(string $key, mixed|Constraint $value): Assert that the span has an attribute with the expected key and value
  • withAttributes(array $attributes): Assert that the span has the expected attributes
  • withStatus(int|Constraint $code, string|Constraint|null $description = null): Assert that the span has the expected status
  • hasEvent(string|Constraint $name): Assert that the span has an event with the expected name
  • hasChild(string|Constraint $name): Assert that the span has a child span with the expected name
  • hasChildren(int $count): Assert that the span has the expected number of children
  • end(): Return to the parent assertion

SpanEventAssertion:

  • withAttribute(string $key, mixed|Constraint $value): Assert that the event has an attribute with the expected key and value
  • withAttributes(array $attributes): Assert that the event has the expected attributes
  • end(): Return to the parent span assertion

Using PHPUnit Matchers

You can use PHPUnit constraints/matchers for more flexible assertions with both interfaces:

Array-Based Interface with Matchers

use PHPUnit\Framework\Constraint\Callback;
use PHPUnit\Framework\Constraint\IsIdentical;
use PHPUnit\Framework\Constraint\IsType;
use PHPUnit\Framework\Constraint\RegularExpression;
use PHPUnit\Framework\Constraint\StringContains;

// Define the expected structure with matchers
$expectedStructure = [
    [
        'name' => 'root-span',
        'kind' => new IsIdentical(SpanKind::KIND_SERVER),
        'attributes' => [
            'string.attribute' => new StringContains('World'),
            'numeric.attribute' => new Callback(function ($value) {
                return $value > 40 || $value === 42;
            }),
            'boolean.attribute' => new IsType('boolean'),
            'array.attribute' => new Callback(function ($value) {
                return is_array($value) && count($value) === 3 && in_array('b', $value);
            }),
        ],
        'children' => [
            [
                'name' => new RegularExpression('/child-span-\d+/'),
                'kind' => SpanKind::KIND_INTERNAL,
                'attributes' => [
                    'timestamp' => new IsType('integer'),
                ],
                'events' => [
                    [
                        'name' => 'process.start',
                        'attributes' => [
                            'process.id' => new IsType('integer'),
                            'process.name' => new StringContains('process'),
                        ],
                    ],
                ],
            ],
        ],
    ],
];

// Assert the trace structure with matchers
$this->assertTraceStructure($spans, $expectedStructure);

Fluent Interface with Matchers

use PHPUnit\Framework\Constraint\Callback;
use PHPUnit\Framework\Constraint\IsIdentical;
use PHPUnit\Framework\Constraint\IsType;
use PHPUnit\Framework\Constraint\RegularExpression;
use PHPUnit\Framework\Constraint\StringContains;

// Assert the trace structure using the fluent interface with matchers
$this->assertTrace($spans)
    ->hasRootSpan('root-span')
        ->withKind(new IsIdentical(SpanKind::KIND_SERVER))
        ->withAttribute('string.attribute', new StringContains('World'))
        ->withAttribute('numeric.attribute', new Callback(function ($value) {
            return $value > 40 || $value === 42;
        }))
        ->withAttribute('boolean.attribute', new IsType('boolean'))
        ->withAttribute('array.attribute', new Callback(function ($value) {
            return is_array($value) && count($value) === 3 && in_array('b', $value);
        }))
        ->hasChild(new RegularExpression('/child-span-\d+/'))
            ->withKind(SpanKind::KIND_INTERNAL)
            ->withAttribute('timestamp', new IsType('integer'))
            ->hasEvent('process.start')
                ->withAttribute('process.id', new IsType('integer'))
                ->withAttribute('process.name', new StringContains('process'))
            ->end()
        ->end()
    ->end();

Supported PHPUnit matchers include:

  • StringContains for partial string matching
  • RegularExpression for pattern matching
  • IsIdentical for strict equality
  • IsEqual for loose equality
  • IsType for type checking
  • Callback for custom validation logic

@cedricziel cedricziel requested a review from a team as a code owner April 1, 2025 10:45
@codecov
Copy link

codecov bot commented Apr 1, 2025

Codecov Report

Attention: Patch coverage is 70.49020% with 301 lines in your changes missing coverage. Please review.

Project coverage is 79.36%. Comparing base (56e4005) to head (3e1cb7d).
Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
src/Utils/Test/src/Fluent/SpanAssertion.php 70.20% 104 Missing ⚠️
src/Utils/Test/src/Fluent/TraceAssertion.php 49.04% 80 Missing ⚠️
.../Test/src/Fluent/TraceAssertionFailedException.php 61.65% 51 Missing ⚠️
src/Utils/Test/src/Fluent/SpanEventAssertion.php 54.21% 38 Missing ⚠️
...rc/Utils/Test/src/TraceStructureAssertionTrait.php 90.60% 28 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #343      +/-   ##
============================================
- Coverage     80.83%   79.36%   -1.47%     
- Complexity     1502     1737     +235     
============================================
  Files           128      133       +5     
  Lines          6183     7203    +1020     
============================================
+ Hits           4998     5717     +719     
- Misses         1185     1486     +301     
Flag Coverage Δ
Aws 92.24% <ø> (ø)
Context/Swoole 0.00% <ø> (ø)
Instrumentation/CakePHP 20.27% <ø> (ø)
Instrumentation/CodeIgniter 73.77% <ø> (ø)
Instrumentation/Curl 90.42% <ø> (ø)
Instrumentation/ExtAmqp 89.26% <ø> (ø)
Instrumentation/ExtRdKafka 86.23% <ø> (ø)
Instrumentation/Guzzle 69.51% <ø> (ø)
Instrumentation/HttpAsyncClient 78.31% <ø> (ø)
Instrumentation/IO 70.68% <ø> (ø)
Instrumentation/Laravel 62.68% <ø> (ø)
Instrumentation/MongoDB 74.28% <ø> (ø)
Instrumentation/MySqli 95.81% <ø> (ø)
Instrumentation/OpenAIPHP 87.31% <ø> (ø)
Instrumentation/PDO 90.15% <ø> (ø)
Instrumentation/Psr14 77.14% <ø> (ø)
Instrumentation/Psr15 89.41% <ø> (ø)
Instrumentation/Psr16 97.56% <ø> (ø)
Instrumentation/Psr18 77.77% <ø> (ø)
Instrumentation/Psr3 59.49% <ø> (ø)
Instrumentation/Psr6 97.67% <ø> (ø)
Instrumentation/Slim 86.30% <ø> (ø)
Instrumentation/Symfony 84.93% <ø> (ø)
Instrumentation/Yii 77.68% <ø> (ø)
Logs/Monolog 100.00% <ø> (ø)
Propagation/TraceResponse 100.00% <ø> (ø)
ResourceDetectors/Azure 91.66% <ø> (ø)
ResourceDetectors/Container 93.02% <ø> (ø)
Sampler/RuleBased 33.51% <ø> (ø)
Shims/OpenTracing 92.45% <ø> (ø)
Symfony 87.81% <ø> (ø)
Utils/Test 70.49% <70.49%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...rc/Utils/Test/src/TraceStructureAssertionTrait.php 90.60% <90.60%> (ø)
src/Utils/Test/src/Fluent/SpanEventAssertion.php 54.21% <54.21%> (ø)
.../Test/src/Fluent/TraceAssertionFailedException.php 61.65% <61.65%> (ø)
src/Utils/Test/src/Fluent/TraceAssertion.php 49.04% <49.04%> (ø)
src/Utils/Test/src/Fluent/SpanAssertion.php 70.20% <70.20%> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 56e4005...3e1cb7d. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@brettmc
Copy link
Contributor

brettmc commented Apr 1, 2025

LGTM! One thing you'll need to do is add it to the gitsplit file. I'll then create a destination repo in opentelemetry-php we can point packagist to.

@cedricziel
Copy link
Contributor Author

@brettmc added a pointer to opentelemetry-php/contrib-utils-test

@cedricziel cedricziel changed the title chore: init test-utils WIP - chore: init test-utils Apr 1, 2025
@cedricziel cedricziel changed the title WIP - chore: init test-utils chore: init test-utils Apr 1, 2025
@brettmc brettmc merged commit ea137c0 into open-telemetry:main Apr 7, 2025
125 of 131 checks passed
@brettmc
Copy link
Contributor

brettmc commented Apr 7, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants