|
| 1 | +import pytest |
| 2 | + |
| 3 | +from ddtrace._trace.processor.resource_renaming import ResourceRenamingProcessor |
| 4 | +from ddtrace.ext import http |
| 5 | +from ddtrace.trace import Context |
| 6 | +from ddtrace.trace import Span |
| 7 | +from tests.utils import override_global_config |
| 8 | + |
| 9 | + |
| 10 | +class TestResourceRenaming: |
| 11 | + @pytest.mark.parametrize( |
| 12 | + "elem,expected", |
| 13 | + [ |
| 14 | + # Integer patterns |
| 15 | + ("123", "{param:int}"), |
| 16 | + ("10", "{param:int}"), |
| 17 | + ("12345", "{param:int}"), |
| 18 | + ("0", "0"), |
| 19 | + ("01", "01"), |
| 20 | + # Integer ID patterns |
| 21 | + ("123.456", "{param:int_id}"), |
| 22 | + ("123-456-789", "{param:int_id}"), |
| 23 | + ("0123", "{param:int_id}"), |
| 24 | + # Hex patterns (require at least one digit) |
| 25 | + ("123ABC", "{param:hex}"), |
| 26 | + ("a1b2c3", "{param:hex}"), |
| 27 | + ("abcdef", "abcdef"), |
| 28 | + ("ABCDEF", "ABCDEF"), |
| 29 | + ("abcde", "abcde"), |
| 30 | + # Hex ID patterns |
| 31 | + ("123.ABC", "{param:hex_id}"), |
| 32 | + ("a1b2-c3d4", "{param:hex_id}"), |
| 33 | + ("abc-def", "abc-def"), |
| 34 | + # String patterns |
| 35 | + ("this_is_a_very_long_string", "{param:str}"), |
| 36 | + ("with%special&chars", "{param:str}"), |
| 37 | + ("[email protected]", "{param:str}"), |
| 38 | + ("file.with.dots", "file.with.dots"), |
| 39 | + # No match cases |
| 40 | + ("users", "users"), |
| 41 | + ("short", "short"), |
| 42 | + ("xyz123", "xyz123"), |
| 43 | + ], |
| 44 | + ) |
| 45 | + def test_compute_simplified_endpoint_path_element(self, elem, expected): |
| 46 | + processor = ResourceRenamingProcessor() |
| 47 | + result = processor._compute_simplified_endpoint_path_element(elem) |
| 48 | + assert result == expected |
| 49 | + |
| 50 | + @pytest.mark.parametrize( |
| 51 | + "url,expected", |
| 52 | + [ |
| 53 | + # Basic cases |
| 54 | + ("", "/"), |
| 55 | + ("http://example.com", ""), |
| 56 | + ("http://example.com/", "/"), |
| 57 | + ("/users", "/users"), |
| 58 | + ("https://example.com/users", "/users"), |
| 59 | + # Query and fragment handling |
| 60 | + ("http://example.com/api/users?id=123", "/api/users"), |
| 61 | + ("https://example.com/users/123#section", "/users/123#section"), |
| 62 | + ("https://example.com/users/123?filter=active#top", "/users/{param:int}"), |
| 63 | + # Parameter replacement |
| 64 | + ("/users/123", "/users/{param:int}"), |
| 65 | + ("/users/5", "/users/5"), |
| 66 | + ("/users/0123", "/users/{param:int_id}"), |
| 67 | + ("/items/123-456", "/items/{param:int_id}"), |
| 68 | + ("/commits/abc123", "/commits/{param:hex}"), |
| 69 | + ("/sessions/deadbeef", "/sessions/deadbeef"), |
| 70 | + ("/items/abc123-def", "/items/{param:hex_id}"), |
| 71 | + ("/files/verylongfilename12345", "/files/{param:str}"), |
| 72 | + ("/users/user@example", "/users/{param:str}"), |
| 73 | + # Path limits and edge cases |
| 74 | + ("/a/b/c/d/e/f/g/h/i/j/k", "/a/b/c/d/e/f/g/h"), |
| 75 | + ("/api//v1///users//123", "/api/v1/users/{param:int}"), |
| 76 | + ("///////////////////////", "/"), |
| 77 | + # Complex mixed cases |
| 78 | + ( |
| 79 | + "/api/v2/users/123/posts/abc123/comments/hello%20world", |
| 80 | + "/api/v2/users/{param:int}/posts/{param:hex}/comments/{param:str}", |
| 81 | + ), |
| 82 | + ( |
| 83 | + "/12/123-456/abc123/abc-def-123/longstringthathastoomanycharacters", |
| 84 | + "/{param:int}/{param:int_id}/{param:hex}/{param:hex_id}/{param:str}", |
| 85 | + ), |
| 86 | + # Error cases |
| 87 | + (None, "/"), |
| 88 | + ("invalid-url", ""), |
| 89 | + ("://malformed", ""), |
| 90 | + ], |
| 91 | + ) |
| 92 | + def test_compute_simplified_endpoint(self, url, expected): |
| 93 | + processor = ResourceRenamingProcessor() |
| 94 | + result = processor._compute_simplified_endpoint(url) |
| 95 | + assert result == expected |
| 96 | + |
| 97 | + def test_processor_with_route(self): |
| 98 | + processor = ResourceRenamingProcessor() |
| 99 | + span = Span("test", context=Context()) |
| 100 | + span.set_tag(http.ROUTE, "/api/users/{id}") |
| 101 | + span.set_tag(http.URL, "https://example.com/api/users/123") |
| 102 | + |
| 103 | + processor.on_span_finish(span) |
| 104 | + assert span.get_tag(http.ENDPOINT) == "/api/users/{id}" |
| 105 | + |
| 106 | + def test_processor_without_route(self): |
| 107 | + processor = ResourceRenamingProcessor() |
| 108 | + span = Span("test", context=Context()) |
| 109 | + span.set_tag(http.URL, "https://example.com/api/users/123") |
| 110 | + |
| 111 | + processor.on_span_finish(span) |
| 112 | + assert span.get_tag(http.ENDPOINT) == "/api/users/{param:int}" |
| 113 | + |
| 114 | + @override_global_config(dict(_trace_resource_renaming_always_simplified_endpoint=True)) |
| 115 | + def test_processor_always_simplified_endpoint(self): |
| 116 | + processor = ResourceRenamingProcessor() |
| 117 | + span = Span("test", context=Context()) |
| 118 | + span.set_tag(http.ROUTE, "/api/users/{id}") |
| 119 | + span.set_tag(http.URL, "https://example.com/api/users/123") |
| 120 | + |
| 121 | + processor.on_span_finish(span) |
| 122 | + # Should use simplified endpoint even when route exists |
| 123 | + assert span.get_tag(http.ENDPOINT) == "/api/users/{id}" |
| 124 | + |
| 125 | + def test_processor_no_url_no_route(self): |
| 126 | + processor = ResourceRenamingProcessor() |
| 127 | + span = Span("test", context=Context()) |
| 128 | + |
| 129 | + processor.on_span_finish(span) |
| 130 | + assert span.get_tag(http.ENDPOINT) == "/" |
| 131 | + |
| 132 | + def test_processor_empty_url(self): |
| 133 | + processor = ResourceRenamingProcessor() |
| 134 | + span = Span("test", context=Context()) |
| 135 | + span.set_tag(http.URL, "") |
| 136 | + |
| 137 | + processor.on_span_finish(span) |
| 138 | + assert span.get_tag(http.ENDPOINT) == "/" |
| 139 | + |
| 140 | + def test_processor_malformed_url(self): |
| 141 | + processor = ResourceRenamingProcessor() |
| 142 | + span = Span("test", context=Context()) |
| 143 | + span.set_tag(http.URL, "not-a-valid-url") |
| 144 | + |
| 145 | + processor.on_span_finish(span) |
| 146 | + assert span.get_tag(http.ENDPOINT) == "" |
| 147 | + |
| 148 | + def test_regex_patterns(self): |
| 149 | + processor = ResourceRenamingProcessor() |
| 150 | + |
| 151 | + # Integer pattern |
| 152 | + assert processor._INT_RE.fullmatch("123") |
| 153 | + assert not processor._INT_RE.fullmatch("0") |
| 154 | + assert not processor._INT_RE.fullmatch("01") |
| 155 | + |
| 156 | + # Hex pattern (requires at least one digit) |
| 157 | + assert processor._HEX_RE.fullmatch("123ABC") |
| 158 | + assert not processor._HEX_RE.fullmatch("ABCDEF") |
| 159 | + assert not processor._HEX_RE.fullmatch("deadbeef") |
| 160 | + |
| 161 | + def test_path_limit(self): |
| 162 | + processor = ResourceRenamingProcessor() |
| 163 | + span = Span("test", context=Context()) |
| 164 | + long_path = "/" + "/".join([f"segment{i}" for i in range(20)]) |
| 165 | + span.set_tag(http.URL, f"https://example.com{long_path}") |
| 166 | + processor.on_span_finish(span) |
| 167 | + endpoint = span.get_tag(http.ENDPOINT) |
| 168 | + segments = [s for s in endpoint.split("/") if s] |
| 169 | + assert len(segments) == 8 |
| 170 | + |
| 171 | + def test_realistic_urls(self): |
| 172 | + processor = ResourceRenamingProcessor() |
| 173 | + test_cases = [ |
| 174 | + ("https://api.github.com/repos/user/repo/issues/123", "/repos/user/repo/issues/{param:int}"), |
| 175 | + ("https://shop.example.com/products/12345/reviews", "/products/{param:int}/reviews"), |
| 176 | + ("https://files.example.com/uploads/documents/verylongdocumentname", "/uploads/documents/{param:str}"), |
| 177 | + ] |
| 178 | + |
| 179 | + for url, expected in test_cases: |
| 180 | + span = Span("test", context=Context()) |
| 181 | + span.set_tag(http.URL, url) |
| 182 | + processor.on_span_finish(span) |
| 183 | + assert span.get_tag(http.ENDPOINT) == expected |
0 commit comments