Plack-Middleware-OpenTelemetry

 view release on metacpan or  search on metacpan

t/otel_integration.t  view on Meta::CPAN

use strict;
use warnings;
use Test::More;
use Plack::Builder;
use Plack::Test;
use HTTP::Request::Common;
use Capture::Tiny qw(capture_stderr);
use JSON::PP;

BEGIN {
    $ENV{OTEL_TRACES_EXPORTER} = 'console';
}

use OpenTelemetry::SDK;
use OpenTelemetry::Constants qw( SPAN_KIND_SERVER SPAN_STATUS_ERROR SPAN_STATUS_OK );

sub build_app {
    my %config = @_;
    builder {
        enable "Plack::Middleware::OpenTelemetry", %config;
        sub {
            my $env = shift;
            my $path = $env->{PATH_INFO} || '/';
            
            if ($path eq '/success') {
                return [200, ['Content-Type' => 'text/plain'], ['OK']];
            } elsif ($path eq '/not_found') {
                return [404, ['Content-Type' => 'text/plain'], ['Not Found']];
            } elsif ($path eq '/server_error') {
                return [500, ['Content-Type' => 'text/plain'], ['Server Error']];
            } elsif ($path eq '/exception') {
                die "Test exception message";
            } elsif ($path eq '/streaming') {
                return sub {
                    my $respond = shift;
                    my $writer = $respond->([200, ['Content-Type' => 'text/plain']]);
                    $writer->write("chunk1");
                    $writer->write("chunk2");
                    $writer->close;
                };
            } else {
                return [200, ['Content-Type' => 'text/plain'], ['Default']];
            }
        };
    };
}

sub parse_console_span {
    my $output = shift;
    # The console exporter outputs Perl data structure format
    return unless $output;
    
    # Find the first complete span structure (handle multiple spans)
    if ($output =~ /(\{'attributes'.*?'trace_state' => '[^']*'\})/s) {
        my $span_data = $1;
        # Use eval to parse the Perl data structure
        my $span = eval $span_data;
        if ($@) {
            # Parsing failed, just return undef - tests will handle gracefully
            return;
        }
        return $span;
    }
    return;
}

# Test basic span creation and attributes
subtest 'Basic Span Creation' => sub {
    my $app = build_app();
    
    my $span_output = capture_stderr {
        test_psgi $app, sub {
            my $cb = shift;
            my $res = $cb->(GET 'http://example.com/success');
            is $res->code, 200;
        };
    };
    
    my $span = parse_console_span($span_output);
    ok $span, 'Span captured from console output';
    
    # Check span name
    is $span->{name}, 'GET request', 'Correct span name';
    
    # Check span kind (2 = SPAN_KIND_SERVER)
    is $span->{kind}, SPAN_KIND_SERVER, 'Server span kind';
    
    # Check HTTP semantic attributes
    my $attrs = $span->{attributes};
    is $attrs->{'http.request.method'}, 'GET', 'HTTP method attribute';
    is $attrs->{'url.full'}, 'http://example.com/success', 'Full URL attribute';
    is $attrs->{'url.scheme'}, 'http', 'URL scheme attribute';
    is $attrs->{'url.path'}, '/success', 'URL path attribute';

t/otel_integration.t  view on Meta::CPAN

            is $res->code, 404;
        };
    };
    
    my $span = parse_console_span($span_output);
    # Status code 0 means unset (not error), which is what we want for 404 by default
    isnt $span->{status}{code}, SPAN_STATUS_ERROR, '404 not marked as error by default';
    is $span->{attributes}->{'http.response.status_code'}, 404, '404 status code recorded';
    
    # Test 5xx (should be error)
    $span_output = capture_stderr {
        test_psgi $app, sub {
            my $cb = shift;
            my $res = $cb->(GET 'http://example.com/server_error');
            is $res->code, 500;
        };
    };
    
    $span = parse_console_span($span_output);
    is $span->{status}{code}, SPAN_STATUS_ERROR, '500 marked as error';
    is $span->{attributes}->{'http.response.status_code'}, 500, '500 status code recorded';
};

# Test include_client_errors configuration
subtest 'Include Client Errors' => sub {
    my $app = build_app(include_client_errors => 1);
    
    my $span_output = capture_stderr {
        test_psgi $app, sub {
            my $cb = shift;
            my $res = $cb->(GET 'http://example.com/not_found');
            is $res->code, 404;
        };
    };
    
    my $span = parse_console_span($span_output);
    is $span->{status}{code}, SPAN_STATUS_ERROR, '404 marked as error with include_client_errors';
};

# Test exception handling
subtest 'Exception Handling' => sub {
    my $app = build_app();
    
    my $span_output = capture_stderr {
        test_psgi $app, sub {
            my $cb = shift;
            my $res = $cb->(GET 'http://example.com/exception');
            # Plack::Test catches exceptions and converts them to 500 responses
            is $res->code, 500, 'Exception converted to 500 response';
        };
    };
    
    my $span = parse_console_span($span_output);
    is $span->{status}{code}, SPAN_STATUS_ERROR, 'Exception marked span as error';
    is $span->{attributes}->{'http.response.status_code'}, 500, 'Exception sets 500 status';
    
    # Check if exception was recorded
    ok $span->{events} && @{$span->{events}}, 'Exception events recorded';
};

# Test streaming responses
subtest 'Streaming Response' => sub {
    my $app = build_app();
    
    my $span_output = capture_stderr {
        test_psgi $app, sub {
            my $cb = shift;
            my $res = $cb->(GET 'http://example.com/streaming');
            is $res->code, 200;
            is $res->content, 'chunk1chunk2', 'Streaming content received';
        };
    };
    
    # For streaming responses, multiple spans might be output or format might differ
    # Just verify we captured some span output
    ok $span_output, 'Span output captured for streaming response';
    like $span_output, qr/streaming|GET request/, 'Span output contains expected content';
};

# Test resource attributes
subtest 'Resource Attributes' => sub {
    my $app = build_app(
        resource_attributes => {
            'service.name' => 'test-service',
            'service.version' => '1.0.0'
        }
    );
    
    my $span_output = capture_stderr {
        test_psgi $app, sub {
            my $cb = shift;
            my $res = $cb->(GET 'http://example.com/success');
            is $res->code, 200;
        };
    };
    
    my $span = parse_console_span($span_output);
    ok $span->{resource}, 'Span has resource';
    # Resource attributes should be present, exact format may vary
    ok ref($span->{resource}) eq 'HASH', 'Resource is a hash structure';
};

done_testing();



( run in 1.622 second using v1.01-cache-2.11-cpan-df04353d9ac )