315 lines
9 KiB
Python
315 lines
9 KiB
Python
from collections import namedtuple
|
|
import niquests
|
|
import pytest
|
|
import socket
|
|
import time
|
|
import urllib3
|
|
|
|
|
|
NGINX_CONFIG = """
|
|
{{ globals }}
|
|
|
|
daemon off;
|
|
|
|
events {
|
|
}
|
|
|
|
http {
|
|
{{ http_globals }}
|
|
|
|
ssl_certificate localhost.crt;
|
|
ssl_certificate_key localhost.key;
|
|
|
|
otel_exporter {
|
|
endpoint {{ scheme }}127.0.0.1:14317;
|
|
interval {{ interval or "1ms" }};
|
|
batch_size 3;
|
|
batch_count 3;
|
|
|
|
{{ exporter_opts }}
|
|
}
|
|
|
|
otel_trace on;
|
|
{{ resource_attrs }}
|
|
|
|
server {
|
|
listen 127.0.0.1:18443 ssl;
|
|
listen 127.0.0.1:18443 quic;
|
|
listen 127.0.0.1:18080;
|
|
|
|
http2 on;
|
|
|
|
server_name localhost;
|
|
|
|
location /ok {
|
|
return 200 "OK";
|
|
}
|
|
|
|
location /err {
|
|
return 500 "ERR";
|
|
}
|
|
|
|
location /custom {
|
|
otel_span_name custom_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;
|
|
return 200 "OK";
|
|
}
|
|
|
|
location /vars {
|
|
otel_trace_context extract;
|
|
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 204;
|
|
}
|
|
|
|
location /ignore {
|
|
proxy_pass http://127.0.0.1:18080/notrace;
|
|
}
|
|
|
|
location /extract {
|
|
otel_trace_context extract;
|
|
proxy_pass http://127.0.0.1:18080/notrace;
|
|
}
|
|
|
|
location /inject {
|
|
otel_trace_context inject;
|
|
proxy_pass http://127.0.0.1:18080/notrace;
|
|
}
|
|
|
|
location /propagate {
|
|
otel_trace_context propagate;
|
|
proxy_pass http://127.0.0.1:18080/notrace;
|
|
}
|
|
|
|
location /notrace {
|
|
otel_trace off;
|
|
add_header "X-Otel-Traceparent" $http_traceparent;
|
|
add_header "X-Otel-Tracestate" $http_tracestate;
|
|
return 204;
|
|
}
|
|
}
|
|
}
|
|
|
|
"""
|
|
|
|
TraceContext = namedtuple("TraceContext", ["trace_id", "span_id", "state"])
|
|
|
|
parent_ctx = TraceContext(
|
|
trace_id="0af7651916cd43dd8448eb211c80319c",
|
|
span_id="b9c7c989f97918e1",
|
|
state="congo=ucfJifl5GOE,rojo=00f067aa0ba902b7",
|
|
)
|
|
|
|
|
|
def trace_headers(ctx):
|
|
return (
|
|
{
|
|
"Traceparent": f"00-{ctx.trace_id}-{ctx.span_id}-01",
|
|
"Tracestate": ctx.state,
|
|
}
|
|
if ctx
|
|
else {"Traceparent": None, "Tracestate": None}
|
|
)
|
|
|
|
|
|
def get_attr(span, name):
|
|
for value in (a.value for a in span.attributes if a.key == name):
|
|
return getattr(value, value.WhichOneof("value"))
|
|
|
|
|
|
@pytest.fixture
|
|
def client(nginx):
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
with niquests.Session(multiplexed=True) as s:
|
|
yield s
|
|
|
|
|
|
def test_http09(trace_service, nginx):
|
|
|
|
def get_http09(host, port, path):
|
|
with socket.create_connection((host, port)) as sock:
|
|
sock.sendall(f"GET {path}\n".encode())
|
|
resp = sock.recv(1024).decode("utf-8")
|
|
return resp
|
|
|
|
assert get_http09("127.0.0.1", 18080, "/ok") == "OK"
|
|
|
|
span = trace_service.get_span()
|
|
assert span.name == "/ok"
|
|
|
|
|
|
@pytest.mark.parametrize("http_ver", ["1.1", "2.0", "3.0"])
|
|
@pytest.mark.parametrize(
|
|
("path", "status"),
|
|
[("/ok", 200), ("/err", 500)],
|
|
)
|
|
def test_default_attributes(client, trace_service, http_ver, path, status):
|
|
scheme, port = ("http", 18080) if http_ver == "1.1" else ("https", 18443)
|
|
if http_ver == "3.0":
|
|
client.quic_cache_layer.add_domain("127.0.0.1", port)
|
|
r = client.get(f"{scheme}://127.0.0.1:{port}{path}", verify=False)
|
|
|
|
span = trace_service.get_span()
|
|
assert span.name == path
|
|
|
|
assert get_attr(span, "http.method") == "GET"
|
|
assert get_attr(span, "http.target") == path
|
|
assert get_attr(span, "http.route") == path
|
|
assert get_attr(span, "http.scheme") == scheme
|
|
assert get_attr(span, "http.flavor") == http_ver
|
|
assert get_attr(span, "http.user_agent") == (
|
|
f"niquests/{niquests.__version__}"
|
|
)
|
|
assert get_attr(span, "http.request_content_length") == 0
|
|
assert get_attr(span, "http.response_content_length") == len(r.text)
|
|
assert get_attr(span, "http.status_code") == status
|
|
assert get_attr(span, "net.host.name") == "localhost"
|
|
assert get_attr(span, "net.host.port") == port
|
|
assert get_attr(span, "net.sock.peer.addr") == "127.0.0.1"
|
|
assert get_attr(span, "net.sock.peer.port") in range(1024, 65536)
|
|
|
|
|
|
def test_custom_attributes(client, trace_service):
|
|
assert client.get("http://127.0.0.1:18080/custom").status_code == 200
|
|
|
|
span = trace_service.get_span()
|
|
assert span.name == "custom_location"
|
|
|
|
assert get_attr(span, "http.request.completion") == "OK"
|
|
value = get_attr(span, "http.response.header.content.type")
|
|
assert value.values[0].string_value == "text/plain"
|
|
assert get_attr(span, "http.request") == "GET /custom HTTP/1.1"
|
|
|
|
|
|
def test_trace_off(client, trace_service):
|
|
assert client.get("http://127.0.0.1:18080/notrace").status_code == 204
|
|
|
|
time.sleep(0.01) # wait for spans
|
|
assert len(trace_service.batches) == 0
|
|
|
|
|
|
@pytest.mark.parametrize("parent", [None, parent_ctx])
|
|
def test_variables(client, trace_service, parent):
|
|
r = client.get("http://127.0.0.1:18080/vars", headers=trace_headers(parent))
|
|
|
|
span = trace_service.get_span()
|
|
|
|
if parent:
|
|
assert span.trace_id.hex() == parent.trace_id
|
|
assert span.parent_span_id.hex() == parent.span_id
|
|
assert span.trace_state == parent.state
|
|
|
|
assert r.headers.get("X-Otel-Trace-Id") == span.trace_id.hex()
|
|
assert r.headers.get("X-Otel-Span-Id") == span.span_id.hex()
|
|
assert r.headers.get("X-Otel-Parent-Id") or "" == span.parent_span_id.hex()
|
|
assert r.headers.get("X-Otel-Parent-Sampled") == ("1" if parent else "0")
|
|
|
|
|
|
@pytest.mark.parametrize("parent", [None, parent_ctx])
|
|
@pytest.mark.parametrize(
|
|
"path", ["/ignore", "/extract", "/inject", "/propagate"]
|
|
)
|
|
def test_context(client, trace_service, parent, path):
|
|
headers = trace_headers(parent)
|
|
|
|
r = client.get(f"http://127.0.0.1:18080{path}", headers=headers)
|
|
|
|
span = trace_service.get_span()
|
|
|
|
if path in ["/extract", "/propagate"] and parent:
|
|
assert span.trace_id.hex() == parent.trace_id
|
|
assert span.parent_span_id.hex() == parent.span_id
|
|
assert span.trace_state == parent.state
|
|
|
|
if path in ["/inject", "/propagate"]:
|
|
headers = trace_headers(
|
|
TraceContext(
|
|
span.trace_id.hex(),
|
|
span.span_id.hex(),
|
|
span.trace_state or None,
|
|
)
|
|
)
|
|
|
|
assert r.headers.get("X-Otel-Traceparent") == headers["Traceparent"]
|
|
assert r.headers.get("X-Otel-Tracestate") == headers["Tracestate"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"nginx_config",
|
|
[{"interval": "200ms", "scheme": "http://"}],
|
|
indirect=True,
|
|
)
|
|
@pytest.mark.parametrize("batch_count", [1, 3])
|
|
def test_batches(client, trace_service, batch_count):
|
|
batch_size = 3
|
|
|
|
for _ in range(
|
|
batch_count * batch_size + 1
|
|
): # +1 request to trigger batch sending
|
|
assert client.get("http://127.0.0.1:18080/ok").status_code == 200
|
|
|
|
time.sleep(0.01)
|
|
|
|
assert len(trace_service.batches) == batch_count
|
|
|
|
for batch in trace_service.batches:
|
|
assert (
|
|
get_attr(batch[0].resource, "service.name")
|
|
== "unknown_service:nginx"
|
|
)
|
|
assert len(batch[0].scope_spans[0].spans) == batch_size
|
|
|
|
time.sleep(0.3) # wait for +1 request to be flushed
|
|
trace_service.batches.clear()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"nginx_config",
|
|
[
|
|
{
|
|
"resource_attrs": """
|
|
otel_service_name "test_service";
|
|
otel_resource_attr my.name "my name";
|
|
otel_resource_attr my.service "my service";
|
|
""",
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_custom_resource_attributes(client, trace_service):
|
|
assert client.get("http://127.0.0.1:18080/ok").status_code == 200
|
|
|
|
batch = trace_service.get_batch()
|
|
|
|
assert get_attr(batch.resource, "service.name") == "test_service"
|
|
assert get_attr(batch.resource, "my.name") == "my name"
|
|
assert get_attr(batch.resource, "my.service") == "my service"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"nginx_config",
|
|
[
|
|
{
|
|
"exporter_opts": """
|
|
header X-API-TOKEN api.value;
|
|
header Authorization "Basic value";
|
|
""",
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
@pytest.mark.parametrize("trace_service", ["skip_otelcol"], indirect=True)
|
|
def test_exporter_headers(client, trace_service):
|
|
assert client.get("http://127.0.0.1:18080/ok").status_code == 200
|
|
|
|
assert trace_service.get_span().name == "/ok"
|
|
|
|
headers = dict(trace_service.last_metadata)
|
|
assert headers["x-api-token"] == "api.value"
|
|
assert headers["authorization"] == "Basic value"
|