Skip to content

Commit c8f152b

Browse files
committed
Implement endpoint renaming
1 parent 5baca70 commit c8f152b

File tree

10 files changed

+365
-1
lines changed

10 files changed

+365
-1
lines changed

src/datadog_conf.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ struct datadog_main_conf_t {
107107
// `agent_url` is set by the `datadog_agent_url` directive.
108108
std::optional<std::string> agent_url;
109109

110+
// DD_APM_RESOURCE_RENAMING_ENABLED
111+
// Whether generation of http.endpoint is enabled.
112+
ngx_flag_t resource_renaming_enabled{NGX_CONF_UNSET};
113+
114+
// DD_APM_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT
115+
// Whether http.endpoint is always calculated, even when http.route is
116+
// present.
117+
ngx_flag_t resource_renaming_always_simplified_endpoint{NGX_CONF_UNSET};
118+
110119
#ifdef WITH_WAF
111120
// DD_APPSEC_ENABLED
112121
ngx_flag_t appsec_enabled{NGX_CONF_UNSET};

src/tracing/directives.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,25 @@ constexpr datadog::nginx::directive tracing_directives[] = {
165165
nullptr,
166166
},
167167

168+
{
169+
"datadog_resource_renaming_enabled",
170+
NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1,
171+
ngx_conf_set_flag_slot,
172+
NGX_HTTP_MAIN_CONF_OFFSET,
173+
offsetof(datadog_main_conf_t, resource_renaming_enabled),
174+
nullptr,
175+
},
176+
177+
{
178+
"datadog_resource_renaming_always_simplified_endpoint",
179+
NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1,
180+
ngx_conf_set_flag_slot,
181+
NGX_HTTP_MAIN_CONF_OFFSET,
182+
offsetof(datadog_main_conf_t,
183+
resource_renaming_always_simplified_endpoint),
184+
nullptr,
185+
},
186+
168187
// aliases opentracing (legacy)
169188
ALIAS_COMMAND("datadog_tracing", "opentracing", anywhere | NGX_CONF_TAKE1),
170189
ALIAS_COMMAND("datadog_operation_name", "opentracing_operation_name",

src/tracing_library.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,29 @@ dd::Expected<dd::Tracer> TracingLibrary::make_tracer(
6868
config.agent.url = nginx_conf.agent_url;
6969
}
7070

71+
if (nginx_conf.resource_renaming_enabled != NGX_CONF_UNSET) {
72+
config.resource_renaming_enabled = {nginx_conf.resource_renaming_enabled ==
73+
1};
74+
}
75+
#ifdef WITH_WAF
76+
else {
77+
// we don't have it, in config
78+
// if we also don't have in the environment it defaults to whether appsec
79+
// is enabled
80+
const char *env_renaming =
81+
std::getenv("DD_TRACE_RESOURCE_RENAMING_ENABLED");
82+
if (!env_renaming || !env_renaming[0]) {
83+
config.resource_renaming_enabled = {nginx_conf.appsec_enabled == 1};
84+
}
85+
}
86+
#endif
87+
88+
if (nginx_conf.resource_renaming_always_simplified_endpoint !=
89+
NGX_CONF_UNSET) {
90+
config.resource_renaming_always_simplified_endpoint = {
91+
nginx_conf.resource_renaming_always_simplified_endpoint == 1};
92+
}
93+
7194
// Set sampling rules based on any `datadog_sample_rate` directives.
7295
std::vector<sampling_rule_t> rules = nginx_conf.sampling_rules;
7396
// Sort by descending depth, so that rules in a `location` block come before
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Endpoint renaming integration tests
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Test configuration for endpoint renaming always mode
2+
# This configuration tests that endpoint renaming works even when http.route is present
3+
4+
load_module /datadog-tests/ngx_http_datadog_module.so;
5+
6+
events {
7+
worker_connections 1024;
8+
}
9+
10+
http {
11+
datadog_agent_url http://agent:8126;
12+
datadog_service_name test-service;
13+
14+
datadog_resource_renaming_enabled on;
15+
datadog_resource_renaming_always_simplified_endpoint on;
16+
17+
server {
18+
listen 80;
19+
server_name localhost;
20+
21+
location / {
22+
return 200 "$datadog_config_json";
23+
}
24+
25+
location /api/users {
26+
return 200 "User endpoint";
27+
}
28+
29+
location /api/products {
30+
return 200 "Product endpoint";
31+
}
32+
33+
location /api/orders {
34+
datadog_tag "http.route" "/api/orders/:id";
35+
return 200 "Order endpoint";
36+
}
37+
}
38+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Test configuration for endpoint renaming with appsec enabled
2+
# This configuration tests that endpoint renaming defaults to enabled (fallback mode)
3+
# when appsec is enabled, without explicit resource_renaming_enabled directive
4+
5+
thread_pool waf_thread_pool threads=2 max_queue=5;
6+
7+
load_module /datadog-tests/ngx_http_datadog_module.so;
8+
9+
events {
10+
worker_connections 1024;
11+
}
12+
13+
http {
14+
datadog_agent_url http://agent:8126;
15+
datadog_service_name test-service;
16+
17+
# Enable appsec - this should enable endpoint renaming by default
18+
datadog_appsec_enabled on;
19+
datadog_waf_thread_pool_name waf_thread_pool;
20+
21+
# No explicit datadog_resource_renaming_enabled directive
22+
# Should default to on (fallback mode) because appsec is enabled
23+
24+
server {
25+
listen 80;
26+
server_name localhost;
27+
28+
location / {
29+
return 200 "$datadog_config_json";
30+
}
31+
32+
location /api/users {
33+
return 200 "User endpoint";
34+
}
35+
36+
location /api/products {
37+
return 200 "Product endpoint";
38+
}
39+
40+
location /api/orders {
41+
datadog_tag "http.route" "/api/orders/:id";
42+
return 200 "Order endpoint";
43+
}
44+
}
45+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Test configuration for endpoint renaming disabled (default)
2+
# This configuration tests that endpoint renaming is disabled by default
3+
4+
load_module /datadog-tests/ngx_http_datadog_module.so;
5+
6+
events {
7+
worker_connections 1024;
8+
}
9+
10+
http {
11+
datadog_agent_url http://agent:8126;
12+
datadog_service_name test-service;
13+
14+
# Endpoint renaming should be disabled by default
15+
# No datadog_resource_renaming_enabled directive
16+
17+
server {
18+
listen 80;
19+
server_name localhost;
20+
21+
location / {
22+
return 200 "$datadog_config_json";
23+
}
24+
25+
location /api/users {
26+
return 200 "User endpoint";
27+
}
28+
29+
location /api/products {
30+
return 200 "Product endpoint";
31+
}
32+
}
33+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Test configuration for endpoint renaming enabled
2+
# This configuration tests that endpoint renaming works when enabled
3+
4+
load_module /datadog-tests/ngx_http_datadog_module.so;
5+
6+
events {
7+
worker_connections 1024;
8+
}
9+
10+
http {
11+
datadog_agent_url http://agent:8126;
12+
datadog_service_name test-service;
13+
14+
# Enable endpoint renaming (fallback mode)
15+
datadog_resource_renaming_enabled on;
16+
17+
server {
18+
listen 80;
19+
server_name localhost;
20+
21+
location / {
22+
return 200 "$datadog_config_json";
23+
}
24+
25+
location /api/users {
26+
return 200 "User endpoint";
27+
}
28+
29+
location /api/products {
30+
return 200 "Product endpoint";
31+
}
32+
33+
location /api/orders {
34+
datadog_tag "http.route" "/api/orders/:id";
35+
return 200 "Order endpoint";
36+
}
37+
}
38+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from .. import case
2+
from .. import formats
3+
4+
from pathlib import Path
5+
6+
7+
class TestEndpointRenaming(case.TestCase):
8+
"""Test cases for endpoint renaming (http.endpoint tag generation).
9+
10+
The endpoint renaming feature generates http.endpoint tags by analyzing
11+
URL paths and replacing dynamic segments with patterns like {param:int},
12+
{param:hex}, etc.
13+
"""
14+
15+
last_config = ''
16+
17+
def replace_config(self, new_config):
18+
"""Replace nginx configuration if different from last config."""
19+
if new_config != TestEndpointRenaming.last_config:
20+
conf_path = Path(__file__).parent / "conf" / new_config
21+
conf_text = conf_path.read_text()
22+
status, log_lines = self.orch.nginx_replace_config(
23+
conf_text, conf_path.name)
24+
self.assertEqual(0, status, log_lines)
25+
TestEndpointRenaming.last_config = new_config
26+
27+
def send_request_and_get_span(self, url, config_name):
28+
self.replace_config(config_name)
29+
self.orch.sync_service('agent')
30+
31+
status, _, _ = self.orch.send_nginx_http_request(url)
32+
33+
self.orch.reload_nginx()
34+
log_lines = self.orch.sync_service('agent')
35+
36+
spans = formats.parse_spans(log_lines)
37+
nginx_spans = [s for s in spans if s.get('service') == 'test-service']
38+
39+
self.assertEquals(len(nginx_spans), 1, "Expected exactly one span")
40+
41+
return status, nginx_spans[0]
42+
43+
def test_endpoint_renaming_disabled_by_default(self):
44+
"""Verify that endpoint renaming is disabled by default.
45+
46+
When disabled, no http.endpoint tag should be added to spans.
47+
"""
48+
status, span = self.send_request_and_get_span("/api/users/123",
49+
"disabled.conf")
50+
self.assertEqual(200, status)
51+
52+
meta = span.get('meta', {})
53+
self.assertNotIn(
54+
'http.endpoint', meta,
55+
f"http.endpoint should not be present when disabled: {meta}")
56+
57+
def test_endpoint_renaming_fallback_mode(self):
58+
"""Verify endpoint renaming in fallback mode.
59+
60+
When enabled in fallback mode, http.endpoint should be added
61+
when http.route is not present. For this endpoint, it's not present
62+
"""
63+
status, span = self.send_request_and_get_span("/api/users/123",
64+
"fallback.conf")
65+
self.assertEqual(200, status)
66+
67+
meta = span.get('meta', {})
68+
self.assertIn('http.endpoint', meta,
69+
"http.endpoint should be present in fallback mode")
70+
self.assertEqual(
71+
meta['http.endpoint'], '/api/users/{param:int}',
72+
f"Expected /api/users/{{param:int}}, got {meta['http.endpoint']}")
73+
74+
def test_fallback_mode_respects_http_route(self):
75+
"""Verify fallback mode does NOT calculate endpoint when http.route is set."""
76+
status, span = self.send_request_and_get_span("/api/orders/789",
77+
"fallback.conf")
78+
self.assertEqual(200, status)
79+
80+
meta = span.get('meta', {})
81+
# In fallback mode with http.route set, http.endpoint should NOT be present
82+
self.assertIn('http.route', meta, "http.route should be present")
83+
self.assertEqual(meta['http.route'], '/api/orders/:id')
84+
self.assertNotIn(
85+
'http.endpoint', meta,
86+
"http.endpoint should NOT be set when http.route exists in fallback mode"
87+
)
88+
89+
def test_endpoint_renaming_always_mode(self):
90+
"""Verify endpoint renaming in always mode.
91+
92+
When enabled in always mode, http.endpoint should always be calculated,
93+
even when http.route is present.
94+
"""
95+
status, span = self.send_request_and_get_span(
96+
"/api/products/abc-123-def", "always.conf")
97+
self.assertEqual(200, status)
98+
99+
meta = span.get('meta', {})
100+
self.assertIn('http.endpoint', meta,
101+
"http.endpoint should be present in always mode")
102+
# Verify the pattern: /api/products/abc-123-def -> /api/products/{param:hex_id}
103+
self.assertEqual(
104+
meta['http.endpoint'], '/api/products/{param:hex_id}',
105+
f"Expected /api/products/{{param:hex_id}}, got {meta['http.endpoint']}"
106+
)
107+
108+
def test_always_mode_ignores_http_route(self):
109+
"""Verify always mode calculates endpoint even when http.route is set."""
110+
status, span = self.send_request_and_get_span("/api/orders/999",
111+
"always.conf")
112+
self.assertEqual(200, status)
113+
114+
meta = span.get('meta', {})
115+
# In always mode, both http.route and http.endpoint should be present
116+
self.assertIn('http.route', meta, "http.route should be present")
117+
self.assertIn('http.endpoint', meta,
118+
"http.endpoint should be present in always mode")
119+
self.assertEqual(meta['http.route'], '/api/orders/:id')
120+
self.assertEqual(meta['http.endpoint'], '/api/orders/{param:int}')
121+
122+
def test_appsec_enables_fallback_mode_by_default(self):
123+
"""Verify that when appsec is enabled, endpoint renaming defaults to fallback mode.
124+
125+
With appsec enabled and no explicit resource_renaming_enabled directive,
126+
the feature should be enabled in fallback mode:
127+
- http.endpoint calculated when http.route not present
128+
- http.endpoint NOT calculated when http.route is present
129+
"""
130+
if self.waf_disabled:
131+
self.skipTest("WAF is disabled - appsec test requires WAF support")
132+
133+
# Test without http.route - should calculate endpoint
134+
status, span = self.send_request_and_get_span("/api/users/456",
135+
"appsec_enabled.conf")
136+
self.assertEqual(200, status)
137+
138+
meta = span.get('meta', {})
139+
self.assertIn(
140+
'http.endpoint', meta,
141+
"http.endpoint should be present when appsec is enabled (fallback mode)"
142+
)
143+
self.assertEqual(
144+
meta['http.endpoint'], '/api/users/{param:int}',
145+
f"Expected /api/users/{{param:int}}, got {meta['http.endpoint']}")
146+
147+
# Test with http.route - should NOT calculate endpoint (fallback mode)
148+
status, span = self.send_request_and_get_span("/api/orders/555",
149+
"appsec_enabled.conf")
150+
self.assertEqual(200, status)
151+
152+
meta = span.get('meta', {})
153+
self.assertIn('http.route', meta, "http.route should be present")
154+
self.assertEqual(meta['http.route'], '/api/orders/:id')
155+
self.assertNotIn(
156+
'http.endpoint', meta,
157+
"http.endpoint should NOT be set when http.route exists in fallback mode (appsec enabled)"
158+
)

0 commit comments

Comments
 (0)