diff --git a/.github/workflows/nginx-otel-module-check.yml b/.github/workflows/nginx-otel-module-check.yml index bad1a32..f34bac3 100644 --- a/.github/workflows/nginx-otel-module-check.yml +++ b/.github/workflows/nginx-otel-module-check.yml @@ -24,8 +24,8 @@ jobs: - name: Build module working-directory: build run: | - cmake -DNGX_OTEL_NGINX_BUILD_DIR=${PWD}/../nginx/objs \ - -DNGX_OTEL_DEV=ON .. + cmake -DNGX_OTEL_NGINX_BUILD_DIR=${GITHUB_WORKSPACE}/nginx/objs \ + -DNGX_OTEL_DEV=ON ${GITHUB_WORKSPACE} make -j 4 strip ngx_otel_module.so - name: Archive module @@ -33,13 +33,6 @@ jobs: with: name: nginx-otel-module path: build/ngx_otel_module.so - - name: Archive protoc and opentelemetry-proto - uses: actions/upload-artifact@v4 - with: - name: protoc-opentelemetry-proto - path: | - build/_deps/grpc-build/third_party/protobuf/protoc - build/_deps/otelcpp-src/third_party/opentelemetry-proto test-module: needs: build-module runs-on: ubuntu-latest @@ -51,36 +44,27 @@ jobs: with: name: nginx-otel-module path: build - - name: Download protoc and opentelemetry-proto - uses: actions/download-artifact@v4 - with: - name: protoc-opentelemetry-proto - path: build/_deps - - name: List files - run: ls -laR . - - name: Fix protoc file permissions - run: chmod +x build/_deps/grpc-build/third_party/protobuf/protoc - - name: Install perl modules - run: sudo cpan IO::Socket::SSL Crypt::Misc - - name: Download otelcol - run: | - curl -LO https://github.com/\ - open-telemetry/opentelemetry-collector-releases/releases/download/\ - v0.76.1/otelcol_0.76.1_linux_amd64.tar.gz - tar -xzf otelcol_0.76.1_linux_amd64.tar.gz - - name: Checkout nginx and nginx-test - run: | - hg clone http://hg.nginx.org/nginx/ - hg clone http://hg.nginx.org/nginx-tests/ + - name: Checkout nginx + run: hg clone http://hg.nginx.org/nginx/ - name: Build nginx working-directory: nginx run: | auto/configure --with-compat --with-debug --with-http_ssl_module \ --with-http_v2_module --with-http_v3_module make -j 4 + - name: Install test dependecies + working-directory: tests + run: pip install -r requirements.txt + - name: Download otelcol + uses: robinraju/release-downloader@v1.9 + with: + repository: open-telemetry/opentelemetry-collector-releases + tag: v0.96.0 + fileName: otelcol_0.96.0_linux_amd64.tar.gz + extract: true - name: Run tests working-directory: tests - run: | - PERL5LIB=../nginx-tests/lib TEST_NGINX_UNSAFE=1 \ - TEST_NGINX_VERBOSE=1 TEST_NGINX_GLOBALS="load_module \ - ${PWD}/../build/ngx_otel_module.so;" prove -v . + env: + TEST_NGINX_GLOBALS: | + load_module ${{ github.workspace }}/build/ngx_otel_module.so; + run: pytest --log-cli-level=debug diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7778812 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,107 @@ +from jinja2 import Environment +import logging +from OpenSSL import crypto +import os +import pytest +import subprocess +import time + + +pytest_plugins = [ + "otelcol_fixtures", +] + +NGINX_BINARY = os.getenv("TEST_NGINX_BINARY", "../nginx/objs/nginx") +CAPABILITIES = subprocess.check_output( + [NGINX_BINARY, "-V"], stderr=subprocess.STDOUT +).decode("utf-8") + + +def self_signed_cert(test_dir): + name = "localhost" + k = crypto.PKey() + k.generate_key(crypto.TYPE_RSA, 2048) + cert = crypto.X509() + cert.get_subject().CN = name + cert.set_issuer(cert.get_subject()) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(365 * 86400) # 365 days + cert.set_pubkey(k) + cert.sign(k, "sha512") + (test_dir / f"{name}.key").write_text( + crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode("utf-8") + ) + (test_dir / f"{name}.crt").write_text( + crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8") + ) + + +@pytest.fixture(scope="session") +def logger(): + logging.basicConfig(level=logging.INFO) + return logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def testdir(tmp_path_factory): + return tmp_path_factory.mktemp("nginx") + + +@pytest.fixture(scope="module") +def nginx_config(request, testdir, logger): + tmpl = Environment().from_string(request.module.NGINX_CONFIG) + params = request.param + params["test_globals"] = ( + f"pid {testdir}/nginx.pid;\nerror_log {testdir}/error.log debug;\n" + + os.getenv("TEST_NGINX_GLOBALS", "") + ) + params[ + "test_globals_http" + ] = f"root {testdir};\naccess_log {testdir}/access.log;\n" + os.getenv( + "TEST_NGINX_GLOBALS_HTTP", "" + ) + params["test_globals_stream"] = os.getenv("TEST_NGINX_GLOBALS_STREAM", "") + conf = tmpl.render(params) + logger.debug(conf) + return conf + + +@pytest.fixture(scope="module") +def nginx(testdir, nginx_config, certs, logger): + logger.debug(CAPABILITIES) + (testdir / "nginx.conf").write_text(nginx_config) + args = [ + NGINX_BINARY, + "-p", + f"{testdir}", + "-c", + "nginx.conf", + "-e", + "error.log", + ] + logger.info("Starting nginx...") + proc = subprocess.Popen(args) + logger.debug(f"path={NGINX_BINARY}") + logger.debug(f"args={' '.join(proc.args[1:])}") + logger.debug(f"pid={proc.pid}") + while not (testdir / "nginx.pid").exists(): + time.sleep(0.1) + if proc.poll() is not None: + raise subprocess.SubprocessError("Can't start nginx") + yield proc + logger.info("Stopping nginx...") + proc.terminate() + try: + proc.wait(timeout=15) + except subprocess.TimeoutExpired: + proc.kill() + log = (testdir / "error.log").read_text() + assert "[alert]" not in log + if os.getenv("TEST_NGINX_CATLOG", "0") in ["1", "true"]: + logger.debug(log) + + +@pytest.fixture(scope="module") +def certs(request, testdir): + if getattr(request.module, "CERT_GEN", None) is not None: + return eval(request.module.CERT_GEN)(testdir) diff --git a/tests/h2_otel.t b/tests/h2_otel.t deleted file mode 100644 index 24987d7..0000000 --- a/tests/h2_otel.t +++ /dev/null @@ -1,553 +0,0 @@ -#!/usr/bin/perl - -# (C) Nginx, Inc. - -# Tests for OTel exporter in case HTTP/2. - -############################################################################### - -use warnings; -use strict; - -use Test::More; - -BEGIN { use FindBin; chdir($FindBin::Bin); } - -use Test::Nginx; -use Test::Nginx::HTTP2; -use MIME::Base64; - -############################################################################### - -select STDERR; $| = 1; -select STDOUT; $| = 1; - -my $t = Test::Nginx->new() - ->has(qw/http_v2 http_ssl rewrite mirror grpc socket_ssl_alpn/) - ->has_daemon(qw/openssl base64/) - ->write_file_expand('nginx.conf', <<'EOF'); - -%%TEST_GLOBALS%% - -daemon off; - -events { -} - -http { - %%TEST_GLOBALS_HTTP%% - - ssl_certificate_key localhost.key; - ssl_certificate localhost.crt; - - otel_exporter { - endpoint 127.0.0.1:8083; - interval 1s; - batch_size 10; - batch_count 1; - } - - otel_service_name test_server; - otel_trace on; - - server { - listen 127.0.0.1:8080 http2; - listen 127.0.0.1:8081; - listen 127.0.0.1:8082 http2 ssl; - server_name localhost; - - location /trace-on { - otel_trace_context extract; - otel_span_name default_location; - otel_span_attr http.request.header.completion - $request_completion; - otel_span_attr http.response.header.content.type - $sent_http_content_type; - otel_span_attr http.request $request; - add_header "X-Otel-Trace-Id" $otel_trace_id; - add_header "X-Otel-Span-Id" $otel_span_id; - add_header "X-Otel-Parent-Id" $otel_parent_id; - add_header "X-Otel-Parent-Sampled" $otel_parent_sampled; - return 200 "TRACE-ON"; - } - - location /context-ignore { - otel_trace_context ignore; - otel_span_name context_ignore; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://127.0.0.1:8081/trace-off; - } - - location /context-extract { - otel_trace_context extract; - otel_span_name context_extract; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://127.0.0.1:8081/trace-off; - } - - location /context-inject { - otel_trace_context inject; - otel_span_name context_inject; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://127.0.0.1:8081/trace-off; - } - - location /context-propagate { - otel_trace_context propagate; - otel_span_name context_propogate; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://127.0.0.1:8081/trace-off; - } - - location /trace-off { - otel_trace off; - add_header "X-Otel-Traceparent" $http_traceparent; - add_header "X-Otel-Tracestate" $http_tracestate; - return 200 "TRACE-OFF"; - } - } - - server { - listen 127.0.0.1:8083 http2; - server_name localhost; - otel_trace off; - - location / { - mirror /mirror; - grpc_pass 127.0.0.1:8084; - } - - location /mirror { - internal; - grpc_pass 127.0.0.1:%%PORT_4317%%; - } - } - - server { - listen 127.0.0.1:8084 http2; - server_name localhost; - otel_trace off; - - location / { - add_header content-type application/grpc; - add_header grpc-status 0; - add_header grpc-message ""; - return 200; - } - } - -} - -EOF - -$t->write_file('openssl.conf', <<'EOF'); -[ req ] -default_bits = 2048 -encrypt_key = no -distinguished_name = req_distinguished_name -[ req_distinguished_name ] - -EOF - -my $d = $t->testdir(); - -foreach my $name ('localhost') { - system('openssl req -x509 -new ' - . "-config $d/openssl.conf -subj /CN=$name/ " - . "-out $d/$name.crt -keyout $d/$name.key " - . ">>$d/openssl.out 2>&1") == 0 - or die "Can't create certificate for $name: $!\n"; -} - -$t->try_run('no OTel module')->plan(69); - -############################################################################### - -my $p = port(4317); -my $f = grpc(); - -#do requests -(undef, my $t_off_resp) = http2_get('/trace-off'); - -#batch0 (10 requests) -my ($tp_headers, $tp_resp) = http2_get('/trace-on', trace_headers => 1); -my ($t_headers, $t_resp) = http2_get('/trace-on', port => 8082, ssl => 1); - -(my $t_headers_ignore, undef) = http2_get('/context-ignore'); -(my $tp_headers_ignore, undef) = http2_get('/context-ignore', - trace_headers => 1); -(my $t_headers_extract, undef) = http2_get('/context-extract'); -(my $tp_headers_extract, undef) = http2_get('/context-extract', - trace_headers => 1); -(my $t_headers_inject, undef) = http2_get('/context-inject'); -(my $tp_headers_inject, undef) = http2_get('/context-inject', - trace_headers => 1); -(my $t_headers_propagate, undef) = http2_get('/context-propagate'); -(my $tp_headers_propagate, undef) = - http2_get('/context-propagate', trace_headers => 1); - -my ($frame) = grep { $_->{type} eq "DATA" } @{$f->{http_start}()}; -my $batch0 = to_hash(decode_protobuf(substr $frame->{data}, 8)); -my $spans = $$batch0{scope_spans}; - -#batch1 (5 reqeusts) -http2_get('/trace-on') for (1..5); - -($frame) = grep { $_->{type} eq "DATA" } @{$f->{http_start}()}; -my $batch1 = to_hash(decode_protobuf(substr $frame->{data}, 8)); - -#validate responses -like($tp_resp, qr/TRACE-ON/, 'http request1 - trace on'); -like($t_resp, qr/TRACE-ON/, 'http request2 - trace on'); -like($t_off_resp, qr/TRACE-OFF/, 'http request - trace off'); - -#validate batch size -delete $$spans{scope}; #remove 'scope' entry -is(scalar keys %{$spans}, 10, 'batch0 size - trace on'); -delete $$batch1{scope_spans}{scope}; #remove 'scope' entry -is(scalar keys %{$$batch1{scope_spans}}, 5, 'batch1 size - trace on'); - -#validate general attributes -is(get_attr("service.name", "string_value", - $$batch0{resource}), - 'test_server', 'service.name - trace on'); -is($$spans{span0}{name}, '"default_location"', 'span.name - trace on'); - -#validate http metrics -is(get_attr("http.method", "string_value", $$spans{span0}), 'GET', - 'http.method metric - trace on'); -is(get_attr("http.target", "string_value", $$spans{span0}), '/trace-on', - 'http.target metric - trace on'); -is(get_attr("http.route", "string_value", $$spans{span0}), '/trace-on', - 'http.route metric - trace on'); -is(get_attr("http.scheme", "string_value", $$spans{span0}), 'http', - 'http.scheme metric - trace on'); -is(get_attr("http.flavor", "string_value", $$spans{span0}), '2.0', - 'http.flavor metric - trace on'); -is(get_attr("http.user_agent", "string_value", $$spans{span0}), 'nginx-tests', - 'http.user_agent metric - trace on'); -is(get_attr("http.request_content_length", "int_value", $$spans{span0}), 0, - 'http.request_content_length metric - trace on'); -is(get_attr("http.response_content_length", "int_value", $$spans{span0}), 8, - 'http.response_content_length metric - trace on'); -is(get_attr("http.status_code", "int_value", $$spans{span0}), 200, - 'http.status_code metric - trace on'); -is(get_attr("net.host.name", "string_value", $$spans{span0}), 'localhost', - 'net.host.name metric - trace on'); -is(get_attr("net.host.port", "int_value", $$spans{span0}), 8080, - 'net.host.port metric - trace on'); -is(get_attr("net.sock.peer.addr", "string_value", $$spans{span0}), '127.0.0.1', - 'net.sock.peer.addr metric - trace on'); -like(get_attr("net.sock.peer.port", "int_value", $$spans{span0}), qr/\d+/, - 'net.sock.peer.port metric - trace on'); - -#validate https metrics -is(get_attr("http.method", "string_value", $$spans{span1}), 'GET', - 'http.method metric - trace on (https)'); -is(get_attr("http.target", "string_value", $$spans{span1}), '/trace-on', - 'http.target metric - trace on (https)'); -is(get_attr("http.route", "string_value", $$spans{span1}), '/trace-on', - 'http.route metric - trace on (https)'); -is(get_attr("http.scheme", "string_value", $$spans{span1}), 'https', - 'http.scheme metric - trace on (https)'); -is(get_attr("http.flavor", "string_value", $$spans{span1}), '2.0', - 'http.flavor metric - trace on (https)'); -isnt(get_attr("http.user_agent", "string_value", $$spans{span1}), - 'nginx-tests', 'http.user_agent metric - trace on (https)'); -is(get_attr("http.request_content_length", "int_value", $$spans{span1}), 0, - 'http.request_content_length metric - trace on (https)'); -is(get_attr("http.response_content_length", "int_value", $$spans{span1}), 8, - 'http.response_content_length metric - trace on (https)'); -is(get_attr("http.status_code", "int_value", $$spans{span1}), 200, - 'http.status_code metric - trace on (https)'); -is(get_attr("net.host.name", "string_value", $$spans{span1}), 'localhost', - 'net.host.name metric - trace on (https)'); -is(get_attr("net.host.port", "int_value", $$spans{span1}), 8082, - 'net.host.port metric - trace on (https)'); -is(get_attr("net.sock.peer.addr", "string_value", $$spans{span1}), '127.0.0.1', - 'net.sock.peer.addr metric - trace on (https)'); -like(get_attr("net.sock.peer.port", "int_value", $$spans{span1}), qr/\d+/, - 'net.sock.peer.port metric - trace on (https)'); - -#validate custom http metrics -is(${get_attr("http.request.header.completion", "array_value", $$spans{span0})} - {values}{string_value}, '"OK"', - 'http.request.header.completion metric - trace on'); -is(${get_attr( - "http.response.header.content.type", "array_value", $$spans{span0} - )}{values}{string_value}, '"text/plain"', - 'http.response.header.content.type metric - trace on'); -is(get_attr("http.request", "string_value", $$spans{span0}), - 'GET /trace-on HTTP/2.0', 'http.request metric - trace on'); - -#extract trace info -is($$spans{span0}{parent_span_id}, 'b9c7c989f97918e1', - 'traceparent - trace on'); -is($$spans{span0}{trace_state}, '"congo=ucfJifl5GOE,rojo=00f067aa0ba902b7"', - 'tracestate - trace on'); -is($$spans{span1}{parent_span_id}, undef, 'no traceparent - trace on'); -is($$spans{span1}{trace_state}, undef, 'no tracestate - trace on'); - -#variables -is($tp_headers->{'x-otel-trace-id'}, $$spans{span0}{trace_id}, - '$otel_trace_id variable - trace on'); -is($tp_headers->{'x-otel-span-id'}, $$spans{span0}{span_id}, - '$otel_span_id variable - trace on'); -is($tp_headers->{'x-otel-parent-id'}, $$spans{span0}{parent_span_id}, - '$otel_parent_id variable - trace on'); -is($tp_headers->{'x-otel-parent-sampled'}, 1, - '$otel_parent_sampled variable - trace on'); -is($t_headers->{'x-otel-parent-sampled'}, 0, - '$otel_parent_sampled variable - trace on (no traceparent header)'); - -#trace off -is((scalar grep { - get_attr("http.target", "string_value", $$spans{$_}) eq '/trace-off' - } keys %{$spans}), 0, 'no metric in batch0 - trace off'); -is((scalar grep { - get_attr("http.target", "string_value", $$spans{$_}) eq '/trace-off' - } keys %{$$batch1{scope_spans}}), 0, 'no metric in batch1 - trace off'); - -#trace context: ignore -is($t_headers_ignore->{'x-otel-traceparent'}, undef, - 'no traceparent - trace context ignore (no trace headers)'); -is($t_headers_ignore->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context ignore (no trace headers)'); - -is($tp_headers_ignore->{'x-otel-parent-id'}, undef, - 'no parent span id - trace context ignore (trace headers)'); -is($tp_headers_ignore->{'x-otel-traceparent'}, - '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', - 'traceparent - trace context ignore (trace headers)'); -is($tp_headers_ignore->{'x-otel-tracestate'}, - 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7', - 'tracestate - trace context ignore (trace headers)'); - -#trace context: extract -is($t_headers_extract->{'x-otel-traceparent'}, undef, - 'no traceparent - trace context extract (no trace headers)'); -is($t_headers_extract->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context extract (no trace headers)'); - -is($tp_headers_extract->{'x-otel-parent-id'}, 'b9c7c989f97918e1', - 'parent span id - trace context extract (trace headers)'); -is($tp_headers_extract->{'x-otel-traceparent'}, - '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', - 'traceparent - trace context extract (trace headers)'); -is($tp_headers_extract->{'x-otel-tracestate'}, - 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7', - 'tracestate - trace context extract (trace headers)'); - -#trace context: inject -is($t_headers_inject->{'x-otel-traceparent'}, - "00-$$spans{span6}{trace_id}-$$spans{span6}{span_id}-01", - 'traceparent - trace context inject (no trace headers)'); -is($t_headers_inject->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context inject (no trace headers)'); - -is($tp_headers_inject->{'x-otel-parent-id'}, undef, - 'no parent span id - trace context inject (trace headers)'); -is($tp_headers_inject->{'x-otel-traceparent'}, - "00-$$spans{span7}{trace_id}-$$spans{span7}{span_id}-01", - 'traceparent - trace context inject (trace headers)'); -is($tp_headers_inject->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context inject (trace headers)'); - -#trace context: propagate -is($t_headers_propagate->{'x-otel-traceparent'}, - "00-$$spans{span8}{trace_id}-$$spans{span8}{span_id}-01", - 'traceparent - trace context propagate (no trace headers)'); -is($t_headers_propagate->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context propagate (no trace headers)'); - -is($tp_headers_propagate->{'x-otel-parent-id'}, 'b9c7c989f97918e1', - 'parent id - trace context propagate (trace headers)'); -is($tp_headers_propagate->{'x-otel-traceparent'}, - "00-0af7651916cd43dd8448eb211c80319c-$$spans{span9}{span_id}-01", - 'traceparent - trace context propagate (trace headers)'); -is($tp_headers_propagate->{'x-otel-tracestate'}, - 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7', - 'tracestate - trace context propagate (trace headers)'); - -SKIP: { -skip "depends on error log content", 2 unless $ENV{TEST_NGINX_UNSAFE}; - -$t->stop(); -my $log = $t->read_file("error.log"); - -like($log, qr/OTel\/grpc: Error parsing metadata: error=invalid value/, - 'log: error parsing metadata - no protobuf in response'); -unlike($log, qr/OTel export failure: No status received/, - 'log: no export failure'); - -} - -############################################################################### - -sub http2_get { - my ($path, %extra) = @_; - my ($frames, $frame); - - my $port = $extra{port} || 8080; - - my $s = $extra{ssl} - ? Test::Nginx::HTTP2->new( - undef, socket => get_ssl_socket($port, ['h2'])) - : Test::Nginx::HTTP2->new(); - - my $sid = $extra{trace_headers} - ? $s->new_stream({ headers => [ - { name => ':method', value => 'GET' }, - { name => ':scheme', value => 'http' }, - { name => ':path', value => $path }, - { name => ':authority', value => 'localhost' }, - { name => 'user-agent', value => 'nginx-tests', mode => 2 }, - { name => 'traceparent', - value => '00-0af7651916cd43dd8448eb211c80319c-' . - 'b9c7c989f97918e1-01', - mode => 2 - }, - { name => 'tracestate', - value => 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7', - mode => 2 - }]}) - : $s->new_stream({ path => $path }); - $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); - - ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; - my $headers = $frame->{headers}; - - ($frame) = grep { $_->{type} eq "DATA" } @$frames; - my $data = $frame->{data}; - - return $headers, $data; -} - -sub get_ssl_socket { - my ($port, $alpn) = @_; - - return http( - '', PeerAddr => '127.0.0.1:' . port($port), start => 1, - SSL => 1, - SSL_alpn_protocols => $alpn, - SSL_error_trap => sub { die $_[1] } - ); -} - -sub get_attr { - my($attr, $type, $obj) = @_; - - my ($res) = grep { - $_ =~ /^attribute\d+/ && $$obj{$_}{key} eq '"' . $attr . '"' - } keys %{$obj}; - - if (defined $res) { - $$obj{$res}{value}{$type} =~ s/(^\")|(\"$)//g - if $type eq 'string_value'; - - return $$obj{$res}{value}{$type}; - } - - return undef; -} - -sub decode_protobuf { - my ($protobuf) = @_; - - local $/; - open CMD, "echo '" . encode_base64($protobuf) . "' | base64 -d | " . - '$PWD/../build/_deps/grpc-build/third_party/protobuf/protoc '. - '--decode opentelemetry.proto.trace.v1.ResourceSpans -I ' . - '$PWD/../build/_deps/otelcpp-src/third_party/opentelemetry-proto ' . - 'opentelemetry/proto/collector/trace/v1/trace_service.proto |' - or die "Can't decode protobuf: $!\n"; - my $out = ; - close CMD; - - return $out; -} - -sub decode_bytes { - my ($bytes) = @_; - - my $c = sub { return chr oct(shift) }; - - $bytes =~ s/\\(\d{3})/$c->($1)/eg; - $bytes =~ s/(^\")|(\"$)//g; - $bytes =~ s/\\\\/\\/g; - $bytes =~ s/\\r/\r/g; - $bytes =~ s/\\n/\n/g; - $bytes =~ s/\\t/\t/g; - $bytes =~ s/\\"/\"/g; - $bytes =~ s/\\'/\'/g; - - return unpack("H*", unpack("a*", $bytes)); -} - -sub to_hash { - my ($textdata) = @_; - - my %out = (); - push my @stack, \%out; - my ($attr_count, $span_count) = (0, 0); - for my $line (split /\n/, $textdata) { - $line =~ s/(^\s+)|(\s+$)//g; - if ($line =~ /\:/) { - my ($k, $v) = split /\: /, $line; - $v = decode_bytes($v) if ($k =~ /trace_id|span_id|parent_span_id/); - $stack[$#stack]{$k} = $v; - } elsif ($line =~ /\{/) { - $line =~ s/\s\{//; - $line = 'attribute' . $attr_count++ if ($line eq 'attributes'); - if ($line eq 'spans') { - $line = 'span' . $span_count++; - $attr_count = 0; - } - my %new = (); - $stack[$#stack]{$line} = \%new; - push @stack, \%new; - } elsif ($line =~ /\}/) { - pop @stack; - } - } - - return \%out; -} - -sub grpc { - my ($server, $client, $f, $s, $c, $sid, $csid, $uri); - - $server = IO::Socket::INET->new( - Proto => 'tcp', - LocalHost => '127.0.0.1', - LocalPort => $p, - Listen => 5, - Reuse => 1 - ) or die "Can't create listening socket: $!\n"; - - $f->{http_start} = sub { - if (IO::Select->new($server)->can_read(5)) { - $client = $server->accept(); - } else { - # connection could be unexpectedly reused - goto reused if $client; - return undef; - } - - $client->sysread($_, 24) == 24 or return; # preface - - $c = Test::Nginx::HTTP2->new(1, socket => $client, - pure => 1, preface => "") or return; - -reused: - my $frames = $c->read(all => [{ fin => 1 }]); - - $client->close(); - - return $frames; - }; - - return $f; -} - -############################################################################### diff --git a/tests/h3_otel.t b/tests/h3_otel.t deleted file mode 100644 index ef848cb..0000000 --- a/tests/h3_otel.t +++ /dev/null @@ -1,508 +0,0 @@ -#!/usr/bin/perl - -# (C) Nginx, Inc. - -# Tests for OTel exporter in case HTTP/3. - -############################################################################### - -use warnings; -use strict; - -use Test::More; - -BEGIN { use FindBin; chdir($FindBin::Bin); } - -use Test::Nginx; -use Test::Nginx::HTTP2; -use Test::Nginx::HTTP3; -use MIME::Base64; - -############################################################################### - -select STDERR; $| = 1; -select STDOUT; $| = 1; - -my $t = Test::Nginx->new()->has(qw/http_v2 http_v3 rewrite mirror grpc cryptx/) - ->has_daemon(qw/openssl base64/) - ->write_file_expand('nginx.conf', <<'EOF'); - -%%TEST_GLOBALS%% - -daemon off; - -events { -} - -http { - %%TEST_GLOBALS_HTTP%% - - ssl_certificate_key localhost.key; - ssl_certificate localhost.crt; - ssl_protocols TLSv1.3; - - otel_exporter { - endpoint 127.0.0.1:8082; - interval 1s; - batch_size 10; - batch_count 2; - } - - otel_service_name test_server; - otel_trace on; - - server { - listen 127.0.0.1:%%PORT_8980_UDP%% quic; - listen 127.0.0.1:8081; - server_name localhost; - - location /trace-on { - otel_trace_context extract; - otel_span_name default_location; - otel_span_attr http.request.header.completion - $request_completion; - otel_span_attr http.response.header.content.type - $sent_http_content_type; - otel_span_attr http.request $request; - add_header "X-Otel-Trace-Id" $otel_trace_id; - add_header "X-Otel-Span-Id" $otel_span_id; - add_header "X-Otel-Parent-Id" $otel_parent_id; - add_header "X-Otel-Parent-Sampled" $otel_parent_sampled; - return 200 "TRACE-ON"; - } - - location /context-ignore { - otel_trace_context ignore; - otel_span_name context_ignore; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://127.0.0.1:8081/trace-off; - } - - location /context-extract { - otel_trace_context extract; - otel_span_name context_extract; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://127.0.0.1:8081/trace-off; - } - - location /context-inject { - otel_trace_context inject; - otel_span_name context_inject; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://127.0.0.1:8081/trace-off; - } - - location /context-propagate { - otel_trace_context propagate; - otel_span_name context_propogate; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://127.0.0.1:8081/trace-off; - } - - location /trace-off { - otel_trace off; - add_header "X-Otel-Traceparent" $http_traceparent; - add_header "X-Otel-Tracestate" $http_tracestate; - return 200 "TRACE-OFF"; - } - } - - server { - listen 127.0.0.1:8082 http2; - server_name localhost; - otel_trace off; - - location / { - mirror /mirror; - grpc_pass 127.0.0.1:8083; - } - - location /mirror { - internal; - grpc_pass 127.0.0.1:%%PORT_4317%%; - } - } - - server { - listen 127.0.0.1:8083 http2; - server_name localhost; - otel_trace off; - - location / { - add_header content-type application/grpc; - add_header grpc-status 0; - add_header grpc-message ""; - return 200; - } - } - -} - -EOF - -$t->write_file('openssl.conf', <<'EOF'); -[ req ] -default_bits = 2048 -encrypt_key = no -distinguished_name = req_distinguished_name -[ req_distinguished_name ] - -EOF - -my $d = $t->testdir(); - -foreach my $name ('localhost') { - system('openssl req -x509 -new ' - . "-config $d/openssl.conf -subj /CN=$name/ " - . "-out $d/$name.crt -keyout $d/$name.key " - . ">>$d/openssl.out 2>&1") == 0 - or die "Can't create certificate for $name: $!\n"; -} - -$t->try_run('no OTel module')->plan(56); - -############################################################################### - -my $p = port(4317); -my $f = grpc(); - -#do requests -(undef, my $t_off_resp) = http3_get('/trace-off'); - -#batch0 (10 requests) -my ($tp_headers, $tp_resp) = http3_get('/trace-on', trace_headers => 1); -my ($t_headers, $t_resp) = http3_get('/trace-on'); - -(my $t_headers_ignore, undef) = http3_get('/context-ignore'); -(my $tp_headers_ignore, undef) = http3_get('/context-ignore', - trace_headers => 1); -(my $t_headers_extract, undef) = http3_get('/context-extract'); -(my $tp_headers_extract, undef) = http3_get('/context-extract', - trace_headers => 1); -(my $t_headers_inject, undef) = http3_get('/context-inject'); -(my $tp_headers_inject, undef) = http3_get('/context-inject', - trace_headers => 1); -(my $t_headers_propagate, undef) = http3_get('/context-propagate'); -(my $tp_headers_propagate, undef) = - http3_get('/context-propagate', trace_headers => 1); - -my ($frame) = grep { $_->{type} eq "DATA" } @{$f->{http_start}()}; -my $batch0 = to_hash(decode_protobuf(substr $frame->{data}, 8)); -my $spans = $$batch0{scope_spans}; - -#batch1 (5 reqeusts) -http3_get('/trace-on') for (1..5); - -($frame) = grep { $_->{type} eq "DATA" } @{$f->{http_start}()}; -my $batch1 = to_hash(decode_protobuf(substr $frame->{data}, 8)); - -#validate responses -like($tp_resp, qr/TRACE-ON/, 'http request1 - trace on'); -like($t_resp, qr/TRACE-ON/, 'http request2 - trace on'); -like($t_off_resp, qr/TRACE-OFF/, 'http request - trace off'); - -#validate batch size -delete $$spans{scope}; #remove 'scope' entry -is(scalar keys %{$spans}, 10, 'batch0 size - trace on'); -delete $$batch1{scope_spans}{scope}; #remove 'scope' entry -is(scalar keys %{$$batch1{scope_spans}}, 5, 'batch1 size - trace on'); - -#validate general attributes -is(get_attr("service.name", "string_value", - $$batch0{resource}), - 'test_server', 'service.name - trace on'); -is($$spans{span0}{name}, '"default_location"', 'span.name - trace on'); - -#validate metrics -is(get_attr("http.method", "string_value", $$spans{span0}), 'GET', - 'http.method metric - trace on'); -is(get_attr("http.target", "string_value", $$spans{span0}), '/trace-on', - 'http.target metric - trace on'); -is(get_attr("http.route", "string_value", $$spans{span0}), '/trace-on', - 'http.route metric - trace on'); -is(get_attr("http.scheme", "string_value", $$spans{span0}), 'https', - 'http.scheme metric - trace on'); -is(get_attr("http.flavor", "string_value", $$spans{span0}), '3.0', - 'http.flavor metric - trace on'); -is(get_attr("http.user_agent", "string_value", $$spans{span0}), 'nginx-tests', - 'http.user_agent metric - trace on'); -is(get_attr("http.request_content_length", "int_value", $$spans{span0}), 0, - 'http.request_content_length metric - trace on'); -is(get_attr("http.response_content_length", "int_value", $$spans{span0}), 8, - 'http.response_content_length metric - trace on'); -is(get_attr("http.status_code", "int_value", $$spans{span0}), 200, - 'http.status_code metric - trace on'); -is(get_attr("net.host.name", "string_value", $$spans{span0}), 'localhost', - 'net.host.name metric - trace on'); -is(get_attr("net.host.port", "int_value", $$spans{span0}), 8980, - 'net.host.port metric - trace on'); -is(get_attr("net.sock.peer.addr", "string_value", $$spans{span0}), '127.0.0.1', - 'net.sock.peer.addr metric - trace on'); -like(get_attr("net.sock.peer.port", "int_value", $$spans{span0}), qr/\d+/, - 'net.sock.peer.port metric - trace on'); - -#validate custom http metrics -is(${get_attr("http.request.header.completion", "array_value", $$spans{span0})} - {values}{string_value}, '"OK"', - 'http.request.header.completion metric - trace on'); -is(${get_attr( - "http.response.header.content.type", "array_value", $$spans{span0} - )}{values}{string_value}, '"text/plain"', - 'http.response.header.content.type metric - trace on'); -is(get_attr("http.request", "string_value", $$spans{span0}), - 'GET /trace-on HTTP/3.0', 'http.request metric - trace on'); - -#extract trace info -is($$spans{span0}{parent_span_id}, 'b9c7c989f97918e1', - 'traceparent - trace on'); -is($$spans{span0}{trace_state}, '"congo=ucfJifl5GOE,rojo=00f067aa0ba902b7"', - 'tracestate - trace on'); -is($$spans{span1}{parent_span_id}, undef, 'no traceparent - trace on'); -is($$spans{span1}{trace_state}, undef, 'no tracestate - trace on'); - -#variables -is($tp_headers->{'x-otel-trace-id'}, $$spans{span0}{trace_id}, - '$otel_trace_id variable - trace on'); -is($tp_headers->{'x-otel-span-id'}, $$spans{span0}{span_id}, - '$otel_span_id variable - trace on'); -is($tp_headers->{'x-otel-parent-id'}, $$spans{span0}{parent_span_id}, - '$otel_parent_id variable - trace on'); -is($tp_headers->{'x-otel-parent-sampled'}, 1, - '$otel_parent_sampled variable - trace on'); -is($t_headers->{'x-otel-parent-sampled'}, 0, - '$otel_parent_sampled variable - trace on (no traceparent header)'); - -#trace off -is((scalar grep { - get_attr("http.target", "string_value", $$spans{$_}) eq '/trace-off' - } keys %{$spans}), 0, 'no metric in batch0 - trace off'); -is((scalar grep { - get_attr("http.target", "string_value", $$spans{$_}) eq '/trace-off' - } keys %{$$batch1{scope_spans}}), 0, 'no metric in batch1 - trace off'); - -#trace context: ignore -is($t_headers_ignore->{'x-otel-traceparent'}, undef, - 'no traceparent - trace context ignore (no trace headers)'); -is($t_headers_ignore->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context ignore (no trace headers)'); - -is($tp_headers_ignore->{'x-otel-parent-id'}, undef, - 'no parent span id - trace context ignore (trace headers)'); -is($tp_headers_ignore->{'x-otel-traceparent'}, - '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', - 'traceparent - trace context ignore (trace headers)'); -is($tp_headers_ignore->{'x-otel-tracestate'}, - 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7', - 'tracestate - trace context ignore (trace headers)'); - -#trace context: extract -is($t_headers_extract->{'x-otel-traceparent'}, undef, - 'no traceparent - trace context extract (no trace headers)'); -is($t_headers_extract->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context extract (no trace headers)'); - -is($tp_headers_extract->{'x-otel-parent-id'}, 'b9c7c989f97918e1', - 'parent span id - trace context extract (trace headers)'); -is($tp_headers_extract->{'x-otel-traceparent'}, - '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', - 'traceparent - trace context extract (trace headers)'); -is($tp_headers_extract->{'x-otel-tracestate'}, - 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7', - 'tracestate - trace context extract (trace headers)'); - -#trace context: inject -is($t_headers_inject->{'x-otel-traceparent'}, - "00-$$spans{span6}{trace_id}-$$spans{span6}{span_id}-01", - 'traceparent - trace context inject (no trace headers)'); -is($t_headers_inject->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context inject (no trace headers)'); - -is($tp_headers_inject->{'x-otel-parent-id'}, undef, - 'no parent span id - trace context inject (trace headers)'); -is($tp_headers_inject->{'x-otel-traceparent'}, - "00-$$spans{span7}{trace_id}-$$spans{span7}{span_id}-01", - 'traceparent - trace context inject (trace headers)'); -is($tp_headers_inject->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context inject (trace headers)'); - -#trace context: propagate -is($t_headers_propagate->{'x-otel-traceparent'}, - "00-$$spans{span8}{trace_id}-$$spans{span8}{span_id}-01", - 'traceparent - trace context propagate (no trace headers)'); -is($t_headers_propagate->{'x-otel-tracestate'}, undef, - 'no tracestate - trace context propagate (no trace headers)'); - -is($tp_headers_propagate->{'x-otel-parent-id'}, 'b9c7c989f97918e1', - 'parent id - trace context propagate (trace headers)'); -is($tp_headers_propagate->{'x-otel-traceparent'}, - "00-0af7651916cd43dd8448eb211c80319c-$$spans{span9}{span_id}-01", - 'traceparent - trace context propagate (trace headers)'); -is($tp_headers_propagate->{'x-otel-tracestate'}, - 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7', - 'tracestate - trace context propagate (trace headers)'); - -SKIP: { -skip "depends on error log content", 2 unless $ENV{TEST_NGINX_UNSAFE}; - -$t->stop(); -my $log = $t->read_file("error.log"); - -like($log, qr/OTel\/grpc: Error parsing metadata: error=invalid value/, - 'log: error parsing metadata - no protobuf in response'); -unlike($log, qr/OTel export failure: No status received/, - 'log: no export failure'); - -} - -############################################################################### - -sub http3_get { - my ($path, %extra) = @_; - my ($frames, $frame); - - my $s = Test::Nginx::HTTP3->new(); - - my $sid = $extra{trace_headers} - ? $s->new_stream({ headers => [ - { name => ':method', value => 'GET' }, - { name => ':scheme', value => 'http' }, - { name => ':path', value => $path }, - { name => ':authority', value => 'localhost' }, - { name => 'user-agent', value => 'nginx-tests' }, - { name => 'traceparent', - value => '00-0af7651916cd43dd8448eb211c80319c-' . - 'b9c7c989f97918e1-01' - }, - { name => 'tracestate', - value => 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7' - }]}) - : $s->new_stream({ path => $path }); - - $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); - - ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; - my $headers = $frame->{headers}; - - ($frame) = grep { $_->{type} eq "DATA" } @$frames; - my $data = $frame->{data}; - - return $headers, $data; -} - -sub get_attr { - my($attr, $type, $obj) = @_; - - my ($res) = grep { - $_ =~ /^attribute\d+/ && $$obj{$_}{key} eq '"' . $attr . '"' - } keys %{$obj}; - - if (defined $res) { - $$obj{$res}{value}{$type} =~ s/(^\")|(\"$)//g - if $type eq 'string_value'; - - return $$obj{$res}{value}{$type}; - } - - return undef; -} - -sub decode_protobuf { - my ($protobuf) = @_; - - local $/; - open CMD, "echo '" . encode_base64($protobuf) . "' | base64 -d | " . - '$PWD/../build/_deps/grpc-build/third_party/protobuf/protoc '. - '--decode opentelemetry.proto.trace.v1.ResourceSpans -I ' . - '$PWD/../build/_deps/otelcpp-src/third_party/opentelemetry-proto ' . - 'opentelemetry/proto/collector/trace/v1/trace_service.proto |' - or die "Can't decode protobuf: $!\n"; - my $out = ; - close CMD; - - return $out; -} - -sub decode_bytes { - my ($bytes) = @_; - - my $c = sub { return chr oct(shift) }; - - $bytes =~ s/\\(\d{3})/$c->($1)/eg; - $bytes =~ s/(^\")|(\"$)//g; - $bytes =~ s/\\\\/\\/g; - $bytes =~ s/\\r/\r/g; - $bytes =~ s/\\n/\n/g; - $bytes =~ s/\\t/\t/g; - $bytes =~ s/\\"/\"/g; - $bytes =~ s/\\'/\'/g; - - return unpack("H*", unpack("a*", $bytes)); -} - -sub to_hash { - my ($textdata) = @_; - - my %out = (); - push my @stack, \%out; - my ($attr_count, $span_count) = (0, 0); - for my $line (split /\n/, $textdata) { - $line =~ s/(^\s+)|(\s+$)//g; - if ($line =~ /\:/) { - my ($k, $v) = split /\: /, $line; - $v = decode_bytes($v) if ($k =~ /trace_id|span_id|parent_span_id/); - $stack[$#stack]{$k} = $v; - } elsif ($line =~ /\{/) { - $line =~ s/\s\{//; - $line = 'attribute' . $attr_count++ if ($line eq 'attributes'); - if ($line eq 'spans') { - $line = 'span' . $span_count++; - $attr_count = 0; - } - my %new = (); - $stack[$#stack]{$line} = \%new; - push @stack, \%new; - } elsif ($line =~ /\}/) { - pop @stack; - } - } - - return \%out; -} - -sub grpc { - my ($server, $client, $f, $s, $c, $sid, $csid, $uri); - - $server = IO::Socket::INET->new( - Proto => 'tcp', - LocalHost => '127.0.0.1', - LocalPort => $p, - Listen => 5, - Reuse => 1 - ) or die "Can't create listening socket: $!\n"; - - $f->{http_start} = sub { - if (IO::Select->new($server)->can_read(5)) { - $client = $server->accept(); - } else { - # connection could be unexpectedly reused - goto reused if $client; - return undef; - } - - $client->sysread($_, 24) == 24 or return; # preface - - $c = Test::Nginx::HTTP2->new(1, socket => $client, - pure => 1, preface => "") or return; - -reused: - my $frames = $c->read(all => [{ fin => 1 }]); - - $client->close(); - - return $frames; - }; - - return $f; -} - -############################################################################### diff --git a/tests/otel.t b/tests/otel.t deleted file mode 100644 index 9887b2f..0000000 --- a/tests/otel.t +++ /dev/null @@ -1,514 +0,0 @@ -#!/usr/bin/perl - -# (C) Nginx, Inc. - -# Tests for OTel exporter in case HTTP. - -############################################################################### - -use warnings; -use strict; - -use Test::More; - -BEGIN { use FindBin; chdir($FindBin::Bin); } - -use Test::Nginx; -use Test::Nginx::HTTP2; -use MIME::Base64; - -############################################################################### - -select STDERR; $| = 1; -select STDOUT; $| = 1; - -my $t = Test::Nginx->new()->has(qw/http http_ssl http_v2 mirror rewrite/) - ->has_daemon(qw/openssl base64/) - ->write_file_expand('nginx.conf', <<'EOF'); - -%%TEST_GLOBALS%% - -daemon off; - -events { -} - -http { - %%TEST_GLOBALS_HTTP%% - - ssl_certificate_key localhost.key; - ssl_certificate localhost.crt; - - otel_exporter { - endpoint 127.0.0.1:8082; - interval 1s; - batch_size 10; - batch_count 2; - } - - otel_service_name test_server; - otel_trace on; - - server { - listen 127.0.0.1:8080; - listen 127.0.0.1:8081 ssl; - server_name localhost; - - location /trace-on { - otel_trace_context extract; - otel_span_name default_location; - otel_span_attr http.request.header.completion - $request_completion; - otel_span_attr http.response.header.content.type - $sent_http_content_type; - otel_span_attr http.request $request; - add_header "X-Otel-Trace-Id" $otel_trace_id; - add_header "X-Otel-Span-Id" $otel_span_id; - add_header "X-Otel-Parent-Id" $otel_parent_id; - add_header "X-Otel-Parent-Sampled" $otel_parent_sampled; - return 200 "TRACE-ON"; - } - - location /context-ignore { - otel_trace_context ignore; - otel_span_name context_ignore; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://localhost:8080/trace-off; - } - - location /context-extract { - otel_trace_context extract; - otel_span_name context_extract; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://localhost:8080/trace-off; - } - - location /context-inject { - otel_trace_context inject; - otel_span_name context_inject; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://localhost:8080/trace-off; - } - - location /context-propagate { - otel_trace_context propagate; - otel_span_name context_propogate; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://localhost:8080/trace-off; - } - - location /trace-off { - otel_trace off; - add_header "X-Otel-Traceparent" $http_traceparent; - add_header "X-Otel-Tracestate" $http_tracestate; - return 200 "TRACE-OFF"; - } - } - - server { - listen 127.0.0.1:8082 http2; - server_name localhost; - otel_trace off; - - location / { - mirror /mirror; - grpc_pass 127.0.0.1:8083; - } - - location /mirror { - internal; - grpc_pass 127.0.0.1:%%PORT_4317%%; - } - } - - server { - listen 127.0.0.1:8083 http2; - server_name localhost; - otel_trace off; - - location / { - add_header content-type application/grpc; - add_header grpc-status 0; - add_header grpc-message ""; - return 200; - } - } -} - -EOF - -$t->write_file('openssl.conf', <<'EOF'); -[ req ] -default_bits = 2048 -encrypt_key = no -distinguished_name = req_distinguished_name -[ req_distinguished_name ] - -EOF - -my $d = $t->testdir(); - -foreach my $name ('localhost') { - system('openssl req -x509 -new ' - . "-config $d/openssl.conf -subj /CN=$name/ " - . "-out $d/$name.crt -keyout $d/$name.key " - . ">>$d/openssl.out 2>&1") == 0 - or die "Can't create certificate for $name: $!\n"; -} - -$t->try_run('no OTel module')->plan(69); - -############################################################################### - -my $p = port(4317); -my $f = grpc(); - -#do requests -my $t_off_resp = http1_get('/trace-off'); - -#batch0 (10 requests) -my $tp_resp = http1_get('/trace-on', trace_headers => 1); -my $t_resp = http1_get('/trace-on', port => 8081, ssl => 1); - -my $t_resp_ignore = http1_get('/context-ignore'); -my $tp_resp_ignore = http1_get('/context-ignore', trace_headers => 1); -my $t_resp_extract = http1_get('/context-extract'); -my $tp_resp_extract = http1_get('/context-extract', trace_headers => 1); -my $t_resp_inject = http1_get('/context-inject'); -my $tp_resp_inject = http1_get('/context-inject', trace_headers => 1); -my $t_resp_propagate = http1_get('/context-propagate'); -my $tp_resp_propagate = http1_get('/context-propagate', trace_headers => 1); - -my ($frame) = grep { $_->{type} eq "DATA" } @{$f->{http_start}()}; -my $batch0 = to_hash(decode_protobuf(substr $frame->{data}, 8)); -my $spans = $$batch0{scope_spans}; - -#batch1 (5 reqeusts) -http1_get('/trace-on') for (1..5); - -($frame) = grep { $_->{type} eq "DATA" } @{$f->{http_start}()}; -my $batch1 = to_hash(decode_protobuf(substr $frame->{data}, 8)); - -#validate responses -like($tp_resp, qr/TRACE-ON/, 'http request1 - trace on'); -like($t_resp, qr/TRACE-ON/, 'http request2 - trace on'); -like($t_off_resp, qr/TRACE-OFF/, 'http request - trace off'); - -#validate batch size -delete $$spans{scope}; #remove 'scope' entry -is(scalar keys %{$spans}, 10, 'batch0 size - trace on'); -delete $$batch1{scope_spans}{scope}; #remove 'scope' entry -is(scalar keys %{$$batch1{scope_spans}}, 5, 'batch1 size - trace on'); - -#validate general attributes -is(get_attr("service.name", "string_value", - $$batch0{resource}), 'test_server', 'service.name - trace on'); -is($$spans{span0}{name}, '"default_location"', 'span.name - trace on'); - -#validate http metrics -is(get_attr("http.method", "string_value", $$spans{span0}), 'GET', - 'http.method metric - trace on'); -is(get_attr("http.target", "string_value", $$spans{span0}), '/trace-on', - 'http.target metric - trace on'); -is(get_attr("http.route", "string_value", $$spans{span0}), '/trace-on', - 'http.route metric - trace on'); -is(get_attr("http.scheme", "string_value", $$spans{span0}), 'http', - 'http.scheme metric - trace on'); -is(get_attr("http.flavor", "string_value", $$spans{span0}), '1.0', - 'http.flavor metric - trace on'); -is(get_attr("http.user_agent", "string_value", $$spans{span0}), 'nginx-tests', - 'http.user_agent metric - trace on'); -is(get_attr("http.request_content_length", "int_value", $$spans{span0}), 0, - 'http.request_content_length metric - trace on'); -is(get_attr("http.response_content_length", "int_value", $$spans{span0}), 8, - 'http.response_content_length metric - trace on'); -is(get_attr("http.status_code", "int_value", $$spans{span0}), 200, - 'http.status_code metric - trace on'); -is(get_attr("net.host.name", "string_value", $$spans{span0}), 'localhost', - 'net.host.name metric - trace on'); -is(get_attr("net.host.port", "int_value", $$spans{span0}), 8080, - 'net.host.port metric - trace on'); -is(get_attr("net.sock.peer.addr", "string_value", $$spans{span0}), '127.0.0.1', - 'net.sock.peer.addr metric - trace on'); -like(get_attr("net.sock.peer.port", "int_value", $$spans{span0}), qr/\d+/, - 'net.sock.peer.port metric - trace on'); - -#validate https metrics -is(get_attr("http.method", "string_value", $$spans{span1}), 'GET', - 'http.method metric - trace on (https)'); -is(get_attr("http.target", "string_value", $$spans{span1}), '/trace-on', - 'http.target metric - trace on (https)'); -is(get_attr("http.route", "string_value", $$spans{span1}), '/trace-on', - 'http.route metric - trace on (https)'); -is(get_attr("http.scheme", "string_value", $$spans{span1}), 'https', - 'http.scheme metric - trace on (https)'); -is(get_attr("http.flavor", "string_value", $$spans{span1}), '1.0', - 'http.flavor metric - trace on (https)'); -is(get_attr("http.user_agent", "string_value", $$spans{span1}), - 'nginx-tests', 'http.user_agent metric - trace on (https)'); -is(get_attr("http.request_content_length", "int_value", $$spans{span1}), 0, - 'http.request_content_length metric - trace on (https)'); -is(get_attr("http.response_content_length", "int_value", $$spans{span1}), 8, - 'http.response_content_length metric - trace on (https)'); -is(get_attr("http.status_code", "int_value", $$spans{span1}), 200, - 'http.status_code metric - trace on (https)'); -is(get_attr("net.host.name", "string_value", $$spans{span1}), 'localhost', - 'net.host.name metric - trace on (https)'); -is(get_attr("net.host.port", "int_value", $$spans{span1}), 8081, - 'net.host.port metric - trace on (https)'); -is(get_attr("net.sock.peer.addr", "string_value", $$spans{span1}), '127.0.0.1', - 'net.sock.peer.addr metric - trace on (https)'); -like(get_attr("net.sock.peer.port", "int_value", $$spans{span1}), qr/\d+/, - 'net.sock.peer.port metric - trace on (https)'); - -#validate custom http metrics -is(${get_attr("http.request.header.completion", "array_value", $$spans{span0})} - {values}{string_value}, '"OK"', - 'http.request.header.completion metric - trace on'); -is(${get_attr("http.response.header.content.type", - "array_value", $$spans{span0})}{values}{string_value}, '"text/plain"', - 'http.response.header.content.type metric - trace on'); -is(get_attr("http.request", "string_value", $$spans{span0}), - 'GET /trace-on HTTP/1.0', 'http.request metric - trace on'); - -#extract trace info -is($$spans{span0}{parent_span_id}, 'b9c7c989f97918e1', - 'traceparent - trace on'); -is($$spans{span0}{trace_state}, '"congo=ucfJifl5GOE,rojo=00f067aa0ba902b7"', - 'tracestate - trace on'); -is($$spans{span1}{parent_span_id}, undef, 'no traceparent - trace on'); -is($$spans{span1}{trace_state}, undef, 'no tracestate - trace on'); - -#variables -like($tp_resp, qr/X-Otel-Trace-Id: $$spans{span0}{trace_id}/, - '$otel_trace_id variable - trace on'); -like($tp_resp, qr/X-Otel-Span-Id: $$spans{span0}{span_id}/, - '$otel_span_id variable - trace on'); -like($tp_resp, qr/X-Otel-Parent-Id: $$spans{span0}{parent_span_id}/, - '$otel_parent_id variable - trace on'); -like($tp_resp, qr/X-Otel-Parent-Sampled: 1/, - '$otel_parent_sampled variable - trace on'); -like($t_resp, qr/X-Otel-Parent-Sampled: 0/, - '$otel_parent_sampled variable - trace on (no traceparent header)'); - -#trace off -is((scalar grep { - get_attr("http.target", "string_value", $$spans{$_}) eq '/trace-off' - } keys %{$spans}), 0, 'no metric in batch0 - trace off'); -is((scalar grep { - get_attr("http.target", "string_value", $$spans{$_}) eq '/trace-off' - } keys %{$$batch1{scope_spans}}), 0, 'no metric in batch1 - trace off'); - -#trace context: ignore -unlike($t_resp_ignore, qr/X-Otel-Traceparent/, - 'no traceparent - trace context ignore (no trace headers)'); -unlike($t_resp_ignore, qr/X-Otel-Tracestate/, - 'no tracestate - trace context ignore (no trace headers)'); - -unlike($tp_resp_ignore, qr/X-Otel-Parent-Id/, - 'no parent span id - trace context ignore (trace headers)'); -like($tp_resp_ignore, - qr/Traceparent: 00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01/, - 'traceparent - trace context ignore (trace headers)'); -like($tp_resp_ignore, - qr/Tracestate: congo=ucfJifl5GOE,rojo=00f067aa0ba902b7/, - 'tracestate - trace context ignore (trace headers)'); - -#trace context: extract -unlike($t_resp_extract, qr/X-Otel-Traceparent/, - 'no traceparent - trace context extract (no trace headers)'); -unlike($t_resp_extract, qr/X-Otel-Tracestate/, - 'no tracestate - trace context extract (no trace headers)'); - -like($tp_resp_extract, qr/X-Otel-Parent-Id: b9c7c989f97918e1/, - 'parent span id - trace context extract (trace headers)'); -like($tp_resp_extract, - qr/Traceparent: 00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01/, - 'traceparent - trace context extract (trace headers)'); -like($tp_resp_extract, - qr/Tracestate: congo=ucfJifl5GOE,rojo=00f067aa0ba902b7/, - 'tracestate - trace context extract (trace headers)'); - -#trace context: inject -like($t_resp_inject, - qr/Traceparent: 00-$$spans{span6}{trace_id}-$$spans{span6}{span_id}-01/, - 'traceparent - trace context inject (no trace headers)'); -unlike($t_resp_inject, qr/X-Otel-Tracestate/, - 'no tracestate - trace context inject (no trace headers)'); - -unlike($tp_resp_inject, qr/X-Otel-Parent-Id/, - 'no parent span id - trace context inject (trace headers)'); -like($tp_resp_inject, - qr/Traceparent: 00-$$spans{span7}{trace_id}-$$spans{span7}{span_id}-01/, - 'traceparent - trace context inject (trace headers)'); -unlike($tp_resp_inject, qr/Tracestate:/, - 'no tracestate - trace context inject (trace headers)'); - -#trace context: propagate -like($t_resp_propagate, - qr/Traceparent: 00-$$spans{span8}{trace_id}-$$spans{span8}{span_id}-01/, - 'traceparent - trace context propagate (no trace headers)'); -unlike($t_resp_propagate, qr/X-Otel-Tracestate/, - 'no tracestate - trace context propagate (no trace headers)'); - -like($tp_resp_propagate, qr/X-Otel-Parent-Id: b9c7c989f97918e1/, - 'parent id - trace context propagate (trace headers)'); -like($tp_resp_propagate, - qr/parent: 00-0af7651916cd43dd8448eb211c80319c-$$spans{span9}{span_id}-01/, - 'traceparent - trace context propagate (trace headers)'); -like($tp_resp_propagate, - qr/Tracestate: congo=ucfJifl5GOE,rojo=00f067aa0ba902b7/, - 'tracestate - trace context propagate (trace headers)'); - -SKIP: { -skip "depends on error log contents", 2 unless $ENV{TEST_NGINX_UNSAFE}; - -$t->stop(); -my $log = $t->read_file("error.log"); - -like($log, qr/OTel\/grpc: Error parsing metadata: error=invalid value/, - 'log: error parsing metadata - no protobuf in response'); -unlike($log, qr/OTel export failure: No status received/, - 'log: no export failure'); - -} - -############################################################################### - -sub http1_get { - my ($path, %extra) = @_; - - my $port = $extra{port} || 8080; - - my $r = < '127.0.0.1:' . port($port), - SSL => $extra{ssl}); -} - -sub get_attr { - my($attr, $type, $obj) = @_; - - my ($res) = grep { - $_ =~ /^attribute\d+/ && $$obj{$_}{key} eq '"' . $attr . '"' - } keys %{$obj}; - - if (defined $res) { - $$obj{$res}{value}{$type} =~ s/(^\")|(\"$)//g - if $type eq 'string_value'; - - return $$obj{$res}{value}{$type}; - } - - return undef; -} - -sub decode_protobuf { - my ($protobuf) = @_; - - local $/; - open CMD, "echo '" . encode_base64($protobuf) . "' | base64 -d | " . - '$PWD/../build/_deps/grpc-build/third_party/protobuf/protoc '. - '--decode opentelemetry.proto.trace.v1.ResourceSpans -I ' . - '$PWD/../build/_deps/otelcpp-src/third_party/opentelemetry-proto ' . - 'opentelemetry/proto/collector/trace/v1/trace_service.proto |' - or die "Can't decode protobuf: $!\n"; - my $out = ; - close CMD; - - return $out; -} - -sub decode_bytes { - my ($bytes) = @_; - - my $c = sub { return chr oct(shift) }; - - $bytes =~ s/\\(\d{3})/$c->($1)/eg; - $bytes =~ s/(^\")|(\"$)//g; - $bytes =~ s/\\\\/\\/g; - $bytes =~ s/\\r/\r/g; - $bytes =~ s/\\n/\n/g; - $bytes =~ s/\\t/\t/g; - $bytes =~ s/\\"/\"/g; - $bytes =~ s/\\'/\'/g; - - return unpack("H*", unpack("a*", $bytes)); -} - -sub to_hash { - my ($textdata) = @_; - - my %out = (); - push my @stack, \%out; - my ($attr_count, $span_count) = (0, 0); - for my $line (split /\n/, $textdata) { - $line =~ s/(^\s+)|(\s+$)//g; - if ($line =~ /\:/) { - my ($k, $v) = split /\: /, $line; - $v = decode_bytes($v) if ($k =~ /trace_id|span_id|parent_span_id/); - $stack[$#stack]{$k} = $v; - } elsif ($line =~ /\{/) { - $line =~ s/\s\{//; - $line = 'attribute' . $attr_count++ if ($line eq 'attributes'); - if ($line eq 'spans') { - $line = 'span' . $span_count++; - $attr_count = 0; - } - my %new = (); - $stack[$#stack]{$line} = \%new; - push @stack, \%new; - } elsif ($line =~ /\}/) { - pop @stack; - } - } - - return \%out; -} - -sub grpc { - my ($server, $client, $f, $s, $c, $sid, $csid, $uri); - - $server = IO::Socket::INET->new( - Proto => 'tcp', - LocalHost => '127.0.0.1', - LocalPort => $p, - Listen => 5, - Reuse => 1 - ) or die "Can't create listening socket: $!\n"; - - $f->{http_start} = sub { - if (IO::Select->new($server)->can_read(5)) { - $client = $server->accept(); - } else { - # connection could be unexpectedly reused - goto reused if $client; - return undef; - } - - $client->sysread($_, 24) == 24 or return; # preface - - $c = Test::Nginx::HTTP2->new(1, socket => $client, - pure => 1, preface => "") or return; - -reused: - my $frames = $c->read(all => [{ fin => 1 }]); - - $client->close(); - - return $frames; - }; - - return $f; -} - -############################################################################### diff --git a/tests/otel_collector.t b/tests/otel_collector.t deleted file mode 100644 index 158a2fd..0000000 --- a/tests/otel_collector.t +++ /dev/null @@ -1,403 +0,0 @@ -#!/usr/bin/perl - -# (C) Nginx, Inc. - -# Tests for OTel exporter in case HTTP using otelcol. - -############################################################################### - -use warnings; -use strict; - -use Test::More; - -BEGIN { use FindBin; chdir($FindBin::Bin); } - -use Test::Nginx; - -############################################################################### - -select STDERR; $| = 1; -select STDOUT; $| = 1; - -plan(skip_all => "depends on logs content") unless $ENV{TEST_NGINX_UNSAFE}; - -eval { require JSON::PP; }; -plan(skip_all => "JSON::PP not installed") if $@; - -my $t = Test::Nginx->new()->has(qw/http http_ssl rewrite/) - ->write_file_expand('nginx.conf', <<'EOF'); - -%%TEST_GLOBALS%% - -daemon off; - -events { -} - -http { - %%TEST_GLOBALS_HTTP%% - - ssl_certificate_key localhost.key; - ssl_certificate localhost.crt; - - otel_exporter { - endpoint 127.0.0.1:%%PORT_4317%%; - interval 1s; - batch_size 10; - batch_count 2; - } - - otel_service_name test_server; - otel_trace on; - - server { - listen 127.0.0.1:8080; - listen 127.0.0.1:8081 ssl; - server_name localhost; - - location /trace-on { - otel_trace_context extract; - otel_span_name default_location; - otel_span_attr http.request.header.completion - $request_completion; - otel_span_attr http.response.header.content.type - $sent_http_content_type; - otel_span_attr http.request $request; - add_header "X-Otel-Trace-Id" $otel_trace_id; - add_header "X-Otel-Span-Id" $otel_span_id; - add_header "X-Otel-Parent-Id" $otel_parent_id; - add_header "X-Otel-Parent-Sampled" $otel_parent_sampled; - return 200 "TRACE-ON"; - } - - location /context-ignore { - otel_trace_context ignore; - otel_span_name context_ignore; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://localhost:8080/trace-off; - } - - location /context-extract { - otel_trace_context extract; - otel_span_name context_extract; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://localhost:8080/trace-off; - } - - location /context-inject { - otel_trace_context inject; - otel_span_name context_inject; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://localhost:8080/trace-off; - } - - location /context-propagate { - otel_trace_context propagate; - otel_span_name context_propogate; - add_header "X-Otel-Parent-Id" $otel_parent_id; - proxy_pass http://localhost:8080/trace-off; - } - - location /trace-off { - otel_trace off; - add_header "X-Otel-Traceparent" $http_traceparent; - add_header "X-Otel-Tracestate" $http_tracestate; - return 200 "TRACE-OFF"; - } - } -} - -EOF - -$t->write_file_expand('otel-config.yaml', <testdir() }/otel.json - -service: - pipelines: - traces: - receivers: [otlp] - exporters: [logging, file] - metrics: - receivers: [otlp] - exporters: [logging, file] - -EOF - -$t->write_file('openssl.conf', <<'EOF'); -[ req ] -default_bits = 2048 -encrypt_key = no -distinguished_name = req_distinguished_name -[ req_distinguished_name ] - -EOF - -my $d = $t->testdir(); - -foreach my $name ('localhost') { - system('openssl req -x509 -new ' - . "-config $d/openssl.conf -subj /CN=$name/ " - . "-out $d/$name.crt -keyout $d/$name.key " - . ">>$d/openssl.out 2>&1") == 0 - or die "Can't create certificate for $name: $!\n"; -} - -#suppress otel collector output -open OLDERR, ">&", \*STDERR; -open STDERR, ">>" , $^O eq 'MSWin32' ? 'nul' : '/dev/null'; -$t->run_daemon('../otelcol', '--config', $t->testdir().'/otel-config.yaml'); -open STDERR, ">&", \*OLDERR; -$t->waitforsocket('127.0.0.1:' . port(4317)) or - die 'No otel collector open socket'; - -$t->try_run('no OTel module')->plan(69); - -############################################################################### - -#do requests -my $t_off_resp = http1_get('/trace-off'); - -#batch0 (10 requests) -my $tp_resp = http1_get('/trace-on', trace_headers => 1); -my $t_resp = http1_get('/trace-on', port => 8081, ssl => 1); - -my $t_resp_ignore = http1_get('/context-ignore'); -my $tp_resp_ignore = http1_get('/context-ignore', trace_headers => 1); -my $t_resp_extract = http1_get('/context-extract'); -my $tp_resp_extract = http1_get('/context-extract', trace_headers => 1); -my $t_resp_inject = http1_get('/context-inject'); -my $tp_resp_inject = http1_get('/context-inject', trace_headers => 1); -my $t_resp_propagate = http1_get('/context-propagate'); -my $tp_resp_propagate = http1_get('/context-propagate', trace_headers => 1); - -#batch1 (5 reqeusts) -http1_get('/trace-on') for (1..5); - -#waiting batch1 is sent to collector for 1s -select undef, undef, undef, 1; - -my @batches = split /\n/, $t->read_file('otel.json'); -my $batch_json = JSON::PP::decode_json($batches[0]); -my $spans = $$batch_json{"resourceSpans"}[0]{"scopeSpans"}[0]{"spans"}; - -#validate responses -like($tp_resp, qr/TRACE-ON/, 'http request1 - trace on'); -like($t_resp, qr/TRACE-ON/, 'http request2 - trace on'); -like($t_off_resp, qr/TRACE-OFF/, 'http request - trace off'); - -#validate amount of batches -is(scalar @batches, 2, 'amount of batches - trace on'); - -#validate batch size -is(scalar @{$spans}, 10, 'batch0 size - trace on'); -is(scalar @{${JSON::PP::decode_json($batches[1])}{"resourceSpans"}[0] - {"scopeSpans"}[0]{"spans"}}, 5, 'batch1 size - trace on'); - -#validate general attributes -is(get_attr("service.name", "stringValue", - $$batch_json{resourceSpans}[0]{resource}), - 'test_server', 'service.name - trace on'); -is($$spans[0]{name}, 'default_location', 'span.name - trace on'); - -#validate http metrics -is(get_attr("http.method", "stringValue", $$spans[0]), 'GET', - 'http.method metric - trace on'); -is(get_attr("http.target", "stringValue", $$spans[0]), '/trace-on', - 'http.target metric - trace on'); -is(get_attr("http.route", "stringValue", $$spans[0]), '/trace-on', - 'http.route metric - trace on'); -is(get_attr("http.scheme", "stringValue", $$spans[0]), 'http', - 'http.scheme metric - trace on'); -is(get_attr("http.flavor", "stringValue", $$spans[0]), '1.0', - 'http.flavor metric - trace on'); -is(get_attr("http.user_agent", "stringValue", $$spans[0]), 'nginx-tests', - 'http.user_agent metric - trace on'); -is(get_attr("http.request_content_length", "intValue", $$spans[0]), 0, - 'http.request_content_length metric - trace on'); -is(get_attr("http.response_content_length", "intValue", $$spans[0]), 8, - 'http.response_content_length metric - trace on'); -is(get_attr("http.status_code", "intValue", $$spans[0]), 200, - 'http.status_code metric - trace on'); -is(get_attr("net.host.name", "stringValue", $$spans[0]), 'localhost', - 'net.host.name metric - trace on'); -is(get_attr("net.host.port", "intValue", $$spans[0]), 8080, - 'net.host.port metric - trace on'); -is(get_attr("net.sock.peer.addr", "stringValue", $$spans[0]), '127.0.0.1', - 'net.sock.peer.addr metric - trace on'); -like(get_attr("net.sock.peer.port", "intValue", $$spans[0]), qr/\d+/, - 'net.sock.peer.port metric - trace on'); - -#validate custom http metrics -is(${get_attr("http.request.header.completion", "arrayValue", $$spans[0])} - {values}[0]{stringValue}, 'OK', - 'http.request.header.completion metric - trace on'); -is(${get_attr("http.response.header.content.type", "arrayValue",$$spans[0])} - {values}[0]{stringValue}, 'text/plain', - 'http.response.header.content.type metric - trace on'); -is(get_attr("http.request", "stringValue", $$spans[0]), - 'GET /trace-on HTTP/1.0', 'http.request metric - trace on'); - -#validate https metrics -is(get_attr("http.method", "stringValue", $$spans[1]), 'GET', - 'http.method metric - trace on (https)'); -is(get_attr("http.target", "stringValue", $$spans[1]), '/trace-on', - 'http.target metric - trace on (https)'); -is(get_attr("http.route", "stringValue", $$spans[1]), '/trace-on', - 'http.route metric - trace on (https)'); -is(get_attr("http.scheme", "stringValue", $$spans[1]), 'https', - 'http.scheme metric - trace on (https)'); -is(get_attr("http.flavor", "stringValue", $$spans[1]), '1.0', - 'http.flavor metric - trace on (https)'); -is(get_attr("http.user_agent", "stringValue", $$spans[1]), 'nginx-tests', - 'http.user_agent metric - trace on (https)'); -is(get_attr("http.request_content_length", "intValue", $$spans[1]), 0, - 'http.request_content_length metric - trace on (https)'); -is(get_attr("http.response_content_length", "intValue", $$spans[1]), 8, - 'http.response_content_length metric - trace on (https)'); -is(get_attr("http.status_code", "intValue", $$spans[1]), 200, - 'http.status_code metric - trace on (https)'); -is(get_attr("net.host.name", "stringValue", $$spans[1]), 'localhost', - 'net.host.name metric - trace on (https)'); -is(get_attr("net.host.port", "intValue", $$spans[1]), 8081, - 'net.host.port metric - trace on (https)'); -is(get_attr("net.sock.peer.addr", "stringValue", $$spans[1]), '127.0.0.1', - 'net.sock.peer.addr metric - trace on (https)'); -like(get_attr("net.sock.peer.port", "intValue", $$spans[1]), qr/\d+/, - 'net.sock.peer.port metric - trace on (https)'); - -#extract trace info -is($$spans[0]{parentSpanId}, 'b9c7c989f97918e1', 'traceparent - trace on'); -is($$spans[0]{traceState}, 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7', - 'tracestate - trace on'); -is($$spans[1]{parentSpanId}, '', 'no traceparent - trace on'); -is($$spans[1]{traceState}, undef, 'no tracestate - trace on'); - -#variables -like($tp_resp, qr/X-Otel-Trace-Id: $$spans[0]{traceId}/, - '$otel_trace_id variable - trace on'); -like($tp_resp, qr/X-Otel-Span-Id: $$spans[0]{spanId}/, - '$otel_span_id variable - trace on'); -like($tp_resp, qr/X-Otel-Parent-Id: $$spans[0]{parentSpanId}/, - '$otel_parent_id variable - trace on'); -like($tp_resp, qr/X-Otel-Parent-Sampled: 1/, - '$otel_parent_sampled variable - trace on'); -like($t_resp, qr/X-Otel-Parent-Sampled: 0/, - '$otel_parent_sampled variable - trace on (no traceparent header)'); - -#trace off -unlike($batches[0].$batches[1], - qr/\Q{"key":"http.target","value":{"stringValue":"\/trace-off"}}\E/, - 'no metrics - trace off'); - -#trace context: ignore -unlike($t_resp_ignore, qr/X-Otel-Traceparent/, - 'no traceparent - trace context ignore (no trace headers)'); -unlike($t_resp_ignore, qr/X-Otel-Tracestate/, - 'no tracestate - trace context ignore (no trace headers)'); - -unlike($tp_resp_ignore, qr/X-Otel-Parent-Id/, - 'no parent span id - trace context ignore (trace headers)'); -like($tp_resp_ignore, - qr/Traceparent: 00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01/, - 'traceparent - trace context ignore (trace headers)'); -like($tp_resp_ignore, - qr/Tracestate: congo=ucfJifl5GOE,rojo=00f067aa0ba902b7/, - 'tracestate - trace context ignore (trace headers)'); - -#trace context: extract -unlike($t_resp_extract, qr/X-Otel-Traceparent/, - 'no traceparent - trace context extract (no trace headers)'); -unlike($t_resp_extract, qr/X-Otel-Tracestate/, - 'no tracestate - trace context extract (no trace headers)'); - -like($tp_resp_extract, qr/X-Otel-Parent-Id: b9c7c989f97918e1/, - 'parent span id - trace context extract (trace headers)'); -like($tp_resp_extract, - qr/Traceparent: 00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01/, - 'traceparent - trace context extract (trace headers)'); -like($tp_resp_extract, - qr/Tracestate: congo=ucfJifl5GOE,rojo=00f067aa0ba902b7/, - 'tracestate - trace context extract (trace headers)'); - -#trace context: inject -like($t_resp_inject, - qr/X-Otel-Traceparent: 00-$$spans[6]{traceId}-$$spans[6]{spanId}-01/, - 'traceparent - trace context inject (no trace headers)'); -unlike($t_resp_inject, qr/X-Otel-Tracestate/, - 'no tracestate - trace context inject (no trace headers)'); - -unlike($tp_resp_inject, qr/X-Otel-Parent-Id/, - 'no parent span id - trace context inject (trace headers)'); -like($tp_resp_inject, - qr/Traceparent: 00-$$spans[7]{traceId}-$$spans[7]{spanId}-01/, - 'traceparent - trace context inject (trace headers)'); -unlike($tp_resp_inject, qr/Tracestate:/, - 'no tracestate - trace context inject (trace headers)'); - -#trace context: propagate -like($t_resp_propagate, - qr/X-Otel-Traceparent: 00-$$spans[8]{traceId}-$$spans[8]{spanId}-01/, - 'traceparent - trace context propagate (no trace headers)'); -unlike($t_resp_propagate, qr/X-Otel-Tracestate/, - 'no tracestate - trace context propagate (no trace headers)'); - -like($tp_resp_propagate, qr/X-Otel-Parent-Id: b9c7c989f97918e1/, - 'parent id - trace context propagate (trace headers)'); -like($tp_resp_propagate, - qr/Traceparent: 00-0af7651916cd43dd8448eb211c80319c-$$spans[9]{spanId}-01/, - 'traceparent - trace context propagate (trace headers)'); -like($tp_resp_propagate, - qr/Tracestate: congo=ucfJifl5GOE,rojo=00f067aa0ba902b7/, - 'tracestate - trace context propagate (trace headers)'); - -$t->stop(); -my $log = $t->read_file("error.log"); - -unlike($log, qr/OTel\/grpc: Error parsing metadata: error=invalid value/, - 'log: no error parsing metadata'); -unlike($log, qr/OTel export failure: No status received/, - 'log: no export failure'); - -############################################################################### - -sub http1_get { - my ($path, %extra) = @_; - - my $port = $extra{port} || 8080; - - my $r = < '127.0.0.1:' . port($port), - SSL => $extra{ssl}); -} - -sub get_attr { - my($attr, $type, $obj) = @_; - - my ($res) = grep { $$_{"key"} eq $attr } @{$$obj{"attributes"}}; - - return defined $res ? $res->{"value"}{$type} : undef; -} - -############################################################################### diff --git a/tests/otelcol_fixtures.py b/tests/otelcol_fixtures.py new file mode 100644 index 0000000..e279aec --- /dev/null +++ b/tests/otelcol_fixtures.py @@ -0,0 +1,44 @@ +from concurrent import futures +import grpc +from opentelemetry.proto.collector.trace.v1 import trace_service_pb2 +from opentelemetry.proto.collector.trace.v1 import trace_service_pb2_grpc +import pytest + + +# Spans +_spans = [] + + +class TraceService(trace_service_pb2_grpc.TraceServiceServicer): + def Export(self, request, context): + collect(request.resource_spans) + return trace_service_pb2.ExportTracePartialSuccess() + + +def collect(spans): + _spans.append(spans) + + +def clear(): + _spans.clear() + + +@pytest.fixture(scope="module") +def _otelcollector(logger): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + trace_service_pb2_grpc.add_TraceServiceServicer_to_server( + TraceService(), server + ) + listen_addr = "localhost:4317" + server.add_insecure_port(listen_addr) + server.start() + logger.info(f"Starting otelcol mock at {listen_addr}...") + yield + logger.info("Stopping otelcol mock...") + server.stop(grace=None) + clear() + + +@pytest.fixture(scope="module") +def spans(): + return _spans diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..9fa77bf --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,8 @@ +pytest==8.1.1 +pytest-depends==1.0.1 +pytest-order==1.2.0 +jinja2==3.1.3 +pyopenssl==24.1.0 +niquests==3.5.5 +grpcio==1.62.1 +opentelemetry-proto==1.24.0 diff --git a/tests/test_otel.py b/tests/test_otel.py new file mode 100644 index 0000000..35cb31f --- /dev/null +++ b/tests/test_otel.py @@ -0,0 +1,722 @@ +from binascii import hexlify +import niquests +import pytest +import socket +import ssl +import subprocess +import time +from urllib.parse import urlparse +import urllib3 + + +CERT_GEN = "self_signed_cert" + +NGINX_CONFIG = """ +{{ test_globals }} + +daemon off; + +events { +} + +http { + {{ test_globals_http }} + + ssl_certificate_key localhost.key; + ssl_certificate localhost.crt; + + otel_exporter { + endpoint 127.0.0.1:{{ otel_port }}; + interval 1s; + batch_size 10; + batch_count 2; + } + + otel_service_name {{ otel_service }}; + otel_trace on; + + server { + listen 127.0.0.1:8443 {{ mode }}; + listen 127.0.0.1:8080; + server_name localhost; + + location /trace-on { + otel_trace_context extract; + otel_span_name default_location; + otel_span_attr http.request.completion + $request_completion; + otel_span_attr http.response.header.content.type + $sent_http_content_type; + otel_span_attr http.request $request; + add_header "X-Otel-Trace-Id" $otel_trace_id; + add_header "X-Otel-Span-Id" $otel_span_id; + add_header "X-Otel-Parent-Id" $otel_parent_id; + add_header "X-Otel-Parent-Sampled" $otel_parent_sampled; + return 200 "TRACE-ON"; + } + + location /context-ignore { + otel_trace_context ignore; + otel_span_name context_ignore; + add_header "X-Otel-Parent-Id" $otel_parent_id; + proxy_pass http://127.0.0.1:8080/trace-off; + } + + location /context-extract { + otel_trace_context extract; + otel_span_name context_extract; + add_header "X-Otel-Parent-Id" $otel_parent_id; + proxy_pass http://127.0.0.1:8080/trace-off; + } + + location /context-inject { + otel_trace_context inject; + otel_span_name context_inject; + add_header "X-Otel-Parent-Id" $otel_parent_id; + proxy_pass http://127.0.0.1:8080/trace-off; + } + + location /context-propagate { + otel_trace_context propagate; + otel_span_name context_propagate; + add_header "X-Otel-Parent-Id" $otel_parent_id; + proxy_pass http://127.0.0.1:8080/trace-off; + } + + location /trace-off { + otel_trace off; + add_header "X-Otel-Traceparent" $http_traceparent; + add_header "X-Otel-Tracestate" $http_tracestate; + return 200 "TRACE-OFF"; + } + } +} + +""" + +trace_headers = { + "Traceparent": "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01", + "Tracestate": "congo=ucfJifl5GOE,rojo=00f067aa0ba902b7", +} + +# Spans +_spans = [] + +# Headers from responses +_headers = {} + + +def span_attr(span, attr, atype): + for value in (_.value for _ in span.attributes if _.key == attr): + return getattr(value, atype) + + +def collect_headers(headers, conf): + if conf not in _headers: + _headers[conf] = [] + _headers[conf].append(headers) + + +def h_str(bstr): + return hexlify(bstr).decode("utf-8") + + +@pytest.fixture(scope="class") +def _copy_spans(spans): + yield + time.sleep(3) # wait for the last batch + _spans.extend(spans) + + +@pytest.fixture() +def case_spans(http_ver, otel_mode): + _ = 6 * http_ver + 3 * otel_mode + return _spans[_ : _ + 3] + + +@pytest.fixture() +def span_list(case_spans): + spans = [] + for batch in case_spans: + spans.extend(batch[0].scope_spans[0].spans) + return spans + + +@pytest.fixture() +def case_headers(http_ver, otel_mode): + return _headers.get(f"{http_ver}{otel_mode}") + + +@pytest.fixture(scope="module") +def _otelcol(testdir, logger): + (testdir / "otel-config.yaml").write_text( + """receivers: + otlp: + protocols: + grpc: + endpoint: 127.0.0.1:8317 + +exporters: + otlp/auth: + endpoint: 127.0.0.1:4317 + tls: + insecure: true + +service: + pipelines: + traces: + receivers: + - otlp + exporters: + - otlp/auth""" + ) + logger.info("Starting otelcol at 127.0.0.1:8317...") + proc = subprocess.Popen( + [ + "../otelcol", + "--config", + testdir / "otel-config.yaml", + ] + ) + if proc.poll() is not None: + raise subprocess.SubprocessError("Can't start otelcol") + yield + logger.info("Stopping otelcol...") + proc.terminate() + try: + proc.wait(timeout=15) + except subprocess.TimeoutExpired: + proc.kill() + + +@pytest.fixture() +def session(http_ver, url): + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + with niquests.Session(multiplexed=True) as s: + if http_ver == 3: + parsed = urlparse(url) + assert parsed.scheme == "https", "Only https:// URLs are supported." + port = parsed.port if parsed.port is not None else 8443 + s.quic_cache_layer.add_domain(parsed.hostname, port) + yield s + + +@pytest.fixture() +def simple_client(url, logger): + def do_get(sock, path): + http_send = f"GET {path}\n".encode() + logger.debug(f"{http_send=}") + sock.sendall(http_send) + http_recv = sock.recv(1024) + logger.debug(f"{http_recv=}") + return http_recv.decode("utf-8") + + parsed = urlparse(url) + _ = 8443 if parsed.scheme == "https" else 8080 + port = parsed.port if parsed.port is not None else _ + with socket.create_connection((parsed.hostname, port)) as sock: + if parsed.scheme == "https": + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + with ctx.wrap_socket( + sock, server_hostname=parsed.hostname + ) as ssock: + yield do_get(ssock, parsed.path) + else: + yield do_get(sock, parsed.path) + + +@pytest.mark.usefixtures("_otelcollector", "_otelcol", "nginx") +@pytest.mark.order(1) +@pytest.mark.parametrize( + "nginx_config", + [ + {"otel_port": 4317, "otel_service": "test_http0", "mode": "ssl"}, + {"otel_port": 8317, "otel_service": "test_http0", "mode": "ssl"}, + ], + indirect=True, + ids=["https 0.9-to mock", "https 0.9-to otelcol"], +) +class TestOTelGenerateSpansSimpleClient: + @pytest.mark.parametrize( + ("url", "response"), + [ + pytest.param( + "https://127.0.0.1:8443/trace-off", + "TRACE-OFF", + id="trace-off", + ), + pytest.param( + "https://127.0.0.1:8443/trace-on", + "TRACE-ON", + id="trace-on", + ), + pytest.param( + "https://127.0.0.1:8443/context-ignore", + "TRACE-OFF", + id="context-ignore", + ), + pytest.param( + "https://127.0.0.1:8443/context-extract", + "TRACE-OFF", + id="context-extract", + ), + pytest.param( + "https://127.0.0.1:8443/context-inject", + "TRACE-OFF", + id="context-inject", + ), + pytest.param( + "https://127.0.0.1:8443/context-propagate", + "TRACE-OFF", + id="context-propagate", + ), + ] + + [ + pytest.param( + "https://127.0.0.1:8443/trace-on", + "TRACE-ON", + id=f"bulk request {_}", + ) + for _ in range(1, 26) + ], + ) + def test_do_request(self, simple_client, url, response): + assert response == simple_client + + +@pytest.mark.usefixtures("_otelcollector", "_otelcol", "nginx", "_copy_spans") +@pytest.mark.order(2) +@pytest.mark.parametrize( + ("nginx_config", "http_ver", "otel_mode"), + [ + ( + {"otel_port": 4317, "otel_service": "test_http1", "mode": "ssl"}, + 1, + 0, + ), + ( + {"otel_port": 8317, "otel_service": "test_http1", "mode": "ssl"}, + 1, + 1, + ), + ( + { + "otel_port": 4317, + "otel_service": "test_http2", + "mode": "ssl http2", + }, + 2, + 0, + ), + ( + { + "otel_port": 8317, + "otel_service": "test_http2", + "mode": "ssl http2", + }, + 2, + 1, + ), + ( + {"otel_port": 4317, "otel_service": "test_http3", "mode": "quic"}, + 3, + 0, + ), + ( + {"otel_port": 8317, "otel_service": "test_http3", "mode": "quic"}, + 3, + 1, + ), + ], + indirect=["nginx_config"], + ids=[ + "https-to mock", + "https-to otelcol", + "http2-to mock", + "http2-to otelcol", + "quic-to mock", + "quic-to otelcol", + ], + scope="module", +) +class TestOTelGenerateSpans: + @pytest.mark.parametrize( + ("url", "headers", "response"), + [ + pytest.param( + "https://127.0.0.1:8443/trace-off", + None, + "TRACE-OFF", + id="trace-off", + ), + pytest.param( + "https://127.0.0.1:8443/trace-on", + trace_headers, + "TRACE-ON", + id="trace-on with trace headers", + ), + pytest.param( + "https://127.0.0.1:8443/trace-on", + None, + "TRACE-ON", + id="trace-on no trace headers", + ), + pytest.param( + "https://127.0.0.1:8443/context-ignore", + None, + "TRACE-OFF", + id="context-ignore no trace headers", + ), + pytest.param( + "https://127.0.0.1:8443/context-ignore", + trace_headers, + "TRACE-OFF", + id="context-ignore with trace headers", + ), + pytest.param( + "https://127.0.0.1:8443/context-extract", + None, + "TRACE-OFF", + id="context-extract no trace headers", + ), + pytest.param( + "https://127.0.0.1:8443/context-extract", + trace_headers, + "TRACE-OFF", + id="context-extract with trace headers", + ), + pytest.param( + "https://127.0.0.1:8443/context-inject", + None, + "TRACE-OFF", + id="context-inject no trace headers", + ), + pytest.param( + "https://127.0.0.1:8443/context-inject", + trace_headers, + "TRACE-OFF", + id="context-inject with trace headers", + ), + pytest.param( + "https://127.0.0.1:8443/context-propagate", + None, + "TRACE-OFF", + id="context-propagate no trace headers", + ), + pytest.param( + "https://127.0.0.1:8443/context-propagate", + trace_headers, + "TRACE-OFF", + id="context-propagate with trace headers", + ), + ] + + [ + pytest.param( + "https://127.0.0.1:8443/trace-on", + None, + "TRACE-ON", + id=f"bulk request {_}", + ) + for _ in range(1, 21) + ], + ) + def test_do_request( + self, session, http_ver, otel_mode, url, headers, response, spans + ): + r = session.get(url, headers=headers, verify=False) + collect_headers(r.headers, f"{http_ver}{otel_mode}") + assert r.status_code == 200 + assert r.text == response + + +@pytest.mark.parametrize("otel_mode", [0, 1], ids=["to mock", "to otelcol"]) +@pytest.mark.parametrize( + "http_ver", [0, 1, 2, 3], ids=["https 0.9", "https", "http2", "quic"] +) +class TestOTelSpans: + @pytest.mark.parametrize( + ("batch", "size"), + [(_, 10) for _ in range(3)], + ids=[f"batch {_}" for _ in range(3)], + ) + def test_batch_size(self, http_ver, case_spans, batch, size, otel_mode): + assert size == len(case_spans[batch][0].scope_spans[0].spans) + + @pytest.mark.depends(on=["test_batch_size"]) + @pytest.mark.parametrize( + "batch", + [_ for _ in range(3)], + ids=[f"batch {_}" for _ in range(3)], + ) + def test_service_name(self, http_ver, case_spans, batch, otel_mode): + assert f"test_http{http_ver}" == span_attr( + case_spans[batch][0].resource, + "service.name", + "string_value", + ) + + @pytest.mark.depends(on=["test_batch_size"]) + def test_trace_off(self, http_ver, span_list, otel_mode): + assert "/trace-off" not in [ + span_attr(_, "http.target", "string_value") for _ in span_list + ] + + @pytest.mark.depends(on=["test_batch_size"]) + @pytest.mark.parametrize( + ("location", "span_name", "idx"), + [ + ("/trace-on", "default_location", 0), + ("/context-ignore", "context_ignore", 2), + ("/context-extract", "context_extract", 4), + ("/context-inject", "context_inject", 6), + ("/context-propagate", "context_propagate", 8), + ], + ids=[ + "default_location", + "context_ignore", + "context_extract", + "context_inject", + "context_propagate", + ], + ) + def test_span_name( + self, http_ver, span_list, location, span_name, idx, logger, otel_mode + ): + span = span_list[idx if http_ver else idx // 2] + assert span_name == span.name + assert location == span_attr(span, "http.target", "string_value") + + @pytest.mark.depends(on=["test_batch_size"]) + @pytest.mark.parametrize( + ("attr_name", "attr_value", "attr_type"), + [ + ("http.method", "GET", "string_value"), + ("http.target", "/trace-on", "string_value"), + ("http.route", "/trace-on", "string_value"), + ("http.scheme", "https", "string_value"), + ("http.flavor", [None, "1.1", "2.0", "3.0"], "string_value"), + ( + "http.user_agent", + [None] + [f"niquests/{niquests.__version__}"] * 3, + "string_value", + ), + ("http.request_content_length", 0, "int_value"), + ("http.response_content_length", 8, "int_value"), + ("http.status_code", 200, "int_value"), + ("net.host.name", "localhost", "string_value"), + ("net.host.port", 8443, "int_value"), + ("net.sock.peer.addr", "127.0.0.1", "string_value"), + ("net.sock.peer.port", range(1024, 65536), "int_value"), + ], + ids=[ + "http.method", + "http.target", + "http.route", + "http.scheme", + "http.flavor", + "http.user_agent", + "http.request_content_length", + "http.response_content_length", + "http.status_code", + "net.host.name", + "net.host.port", + "net.sock.peer.addr", + "net.sock.peer.port", + ], + ) + def test_metrics( + self, http_ver, span_list, attr_name, attr_value, attr_type, otel_mode + ): + value = span_attr(span_list[0], attr_name, attr_type) + if attr_name in ["http.flavor", "http.user_agent"]: + attr_value = attr_value[http_ver] + if attr_name == "net.sock.peer.port": + assert value in attr_value + else: + assert value == attr_value + + @pytest.mark.depends(on=["test_batch_size"]) + @pytest.mark.parametrize( + ("attr_name", "attr_value", "attr_type"), + [ + ("http.request.completion", "OK", "string_value"), + ("http.response.header.content.type", "text/plain", "array_value"), + ( + "http.request", + [ + "GET /trace-on", + "GET /trace-on HTTP/1.1", + "GET /trace-on HTTP/2.0", + "GET /trace-on HTTP/3.0", + ], + "string_value", + ), + ], + ids=[ + "http.request.completion", + "http.response.header.content.type", + "http.request", + ], + ) + def test_custom_metrics( + self, http_ver, span_list, attr_name, attr_value, attr_type, otel_mode + ): + value = span_attr(span_list[0], attr_name, attr_type) + if attr_type == "array_value": + value = value.values[0].string_value + if type(attr_value) is list: + attr_value = attr_value[http_ver] + assert attr_value == value + + @pytest.mark.depends(on=["test_batch_size"]) + @pytest.mark.parametrize( + ("name", "value", "idx"), + [ + ("X-Otel-Trace-Id", "span_list[idx - 1].trace_id", 1), + ("X-Otel-Span-Id", "span_list[idx - 1].span_id", 1), + ("X-Otel-Parent-Id", "span_list[idx - 1].parent_span_id", 1), + ("X-Otel-Parent-Sampled", "1", 1), + ("X-Otel-Parent-Sampled", "0", 2), + ], + ids=[ + "otel_trace_id", + "otel_span_id", + "otel_parent_id", + "otel_parent_sampled - 1", + "otel_parent_sampled - 0", + ], + ) + def test_variables( + self, http_ver, span_list, case_headers, name, value, idx, otel_mode + ): + value = h_str(eval(value)) if "span_list" in value else value + if http_ver == 0: + if "Parent" in name: + pytest.skip("no headers support") + assert len(value) == (16 if "Span-Id" in name else 32) + else: + assert case_headers[idx].get(name) == value + + @pytest.mark.depends(on=["test_batch_size"]) + @pytest.mark.parametrize( + ("name", "value", "idx"), + [ + ("X-Otel-Traceparent", None, 3), + ("X-Otel-Tracestate", None, 3), + ("X-Otel-Parent-Id", None, 4), + ( + "X-Otel-Traceparent", + "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01", + 4, + ), + ("X-Otel-Tracestate", "congo=ucfJifl5GOE,rojo=00f067aa0ba902b7", 4), + ] + + [ + ("X-Otel-Traceparent", None, 5), + ("X-Otel-Tracestate", None, 5), + ("X-Otel-Parent-Id", "b9c7c989f97918e1", 6), + ( + "X-Otel-Traceparent", + "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01", + 6, + ), + ("X-Otel-Tracestate", "congo=ucfJifl5GOE,rojo=00f067aa0ba902b7", 6), + ] + + [ + ( + "X-Otel-Traceparent", + [ + "00-", + "span_list[idx - 1].trace_id", + "-", + "span_list[idx - 1].span_id", + "-01", + ], + 7, + ), + ("X-Otel-Tracestate", None, 7), + ("X-Otel-Parent-Id", None, 8), + ( + "X-Otel-Traceparent", + [ + "00-", + "span_list[idx - 1].trace_id", + "-", + "span_list[idx - 1].span_id", + "-01", + ], + 8, + ), + ("X-Otel-Tracestate", None, 8), + ] + + [ + ( + "X-Otel-Traceparent", + [ + "00-", + "span_list[idx - 1].trace_id", + "-", + "span_list[idx - 1].span_id", + "-01", + ], + 9, + ), + ("X-Otel-Tracestate", None, 9), + ("X-Otel-Parent-Id", "b9c7c989f97918e1", 10), + ( + "X-Otel-Traceparent", + [ + "00-0af7651916cd43dd8448eb211c80319c-", + "span_list[idx - 1].span_id", + "-01", + ], + 10, + ), + ( + "X-Otel-Tracestate", + "congo=ucfJifl5GOE,rojo=00f067aa0ba902b7", + 10, + ), + ], + ids=[ + "ignore-no traceparent-no headers", + "ignore-no tracestate-no headers", + "ignore-no parent id-trace headers", + "ignore-traceparent-trace headers", + "ignore-tracestate-trace headers", + ] + + [ + "extract-no traceparent-no headers", + "extract-no tracestate-no headers", + "extract-parent id-trace headers", + "extract-traceparent-trace headers", + "extract-tracestate-trace headers", + ] + + [ + "inject-traceparent-no headers", + "inject-no tracestate-no headers", + "inject-no parent id-trace headers", + "inject-traceparent-trace headers", + "inject-tracestate-trace headers", + ] + + [ + "propagate-traceparent-no headers", + "propagate-no tracestate-no headers", + "propagate-parent id-trace headers", + "propagate-traceparent-trace headers", + "propagate-tracestate-trace headers", + ], + ) + def test_trace_context( + self, http_ver, span_list, case_headers, name, value, idx, otel_mode + ): + if http_ver == 0: + pytest.skip("no headers support") + if type(value) is list: + acc = "" + for _ in value: + acc += h_str(eval(_)) if "span_list" in _ else _ + value = acc + assert case_headers[idx].get(name) == value