Skip to content

Commit 18a7ece

Browse files
committed
Implement endpoint renaming
1 parent 5baca70 commit 18a7ece

File tree

10 files changed

+340
-1
lines changed

10 files changed

+340
-1
lines changed

src/datadog_conf.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ 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 present.
116+
ngx_flag_t resource_renaming_always_simplified_endpoint{NGX_CONF_UNSET};
117+
110118
#ifdef WITH_WAF
111119
// DD_APPSEC_ENABLED
112120
ngx_flag_t appsec_enabled{NGX_CONF_UNSET};

src/tracing/directives.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,24 @@ 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, resource_renaming_always_simplified_endpoint),
183+
nullptr,
184+
},
185+
168186
// aliases opentracing (legacy)
169187
ALIAS_COMMAND("datadog_tracing", "opentracing", anywhere | NGX_CONF_TAKE1),
170188
ALIAS_COMMAND("datadog_operation_name", "opentracing_operation_name",

src/tracing_library.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@ 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 == 1};
73+
}
74+
#ifdef WITH_WAF
75+
else {
76+
// we don't have it, in config
77+
// if we also don't have in the environment it defaults to whether appsec
78+
// is enabled
79+
const char* env_renaming = std::getenv("DD_TRACE_RESOURCE_RENAMING_ENABLED");
80+
if (!env_renaming || !env_renaming[0]) {
81+
config.resource_renaming_enabled = {nginx_conf.appsec_enabled == 1};
82+
}
83+
}
84+
#endif
85+
86+
if (nginx_conf.resource_renaming_always_simplified_endpoint != NGX_CONF_UNSET) {
87+
config.resource_renaming_always_simplified_endpoint = {
88+
nginx_conf.resource_renaming_always_simplified_endpoint == 1};
89+
}
90+
7191
// Set sampling rules based on any `datadog_sample_rate` directives.
7292
std::vector<sampling_rule_t> rules = nginx_conf.sampling_rules;
7393
// 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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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", "disabled.conf")
49+
self.assertEqual(200, status)
50+
51+
meta = span.get('meta', {})
52+
self.assertNotIn('http.endpoint', meta,
53+
f"http.endpoint should not be present when disabled: {meta}")
54+
55+
def test_endpoint_renaming_fallback_mode(self):
56+
"""Verify endpoint renaming in fallback mode.
57+
58+
When enabled in fallback mode, http.endpoint should be added
59+
when http.route is not present. For this endpoint, it's not present
60+
"""
61+
status, span = self.send_request_and_get_span("/api/users/123", "fallback.conf")
62+
self.assertEqual(200, status)
63+
64+
meta = span.get('meta', {})
65+
self.assertIn('http.endpoint', meta, "http.endpoint should be present in fallback mode")
66+
self.assertEqual(meta['http.endpoint'], '/api/users/{param:int}',
67+
f"Expected /api/users/{{param:int}}, got {meta['http.endpoint']}")
68+
69+
def test_fallback_mode_respects_http_route(self):
70+
"""Verify fallback mode does NOT calculate endpoint when http.route is set."""
71+
status, span = self.send_request_and_get_span("/api/orders/789", "fallback.conf")
72+
self.assertEqual(200, status)
73+
74+
meta = span.get('meta', {})
75+
# In fallback mode with http.route set, http.endpoint should NOT be present
76+
self.assertIn('http.route', meta, "http.route should be present")
77+
self.assertEqual(meta['http.route'], '/api/orders/:id')
78+
self.assertNotIn('http.endpoint', meta,
79+
"http.endpoint should NOT be set when http.route exists in fallback mode")
80+
81+
82+
def test_endpoint_renaming_always_mode(self):
83+
"""Verify endpoint renaming in always mode.
84+
85+
When enabled in always mode, http.endpoint should always be calculated,
86+
even when http.route is present.
87+
"""
88+
status, span = self.send_request_and_get_span("/api/products/abc-123-def", "always.conf")
89+
self.assertEqual(200, status)
90+
91+
meta = span.get('meta', {})
92+
self.assertIn('http.endpoint', meta, "http.endpoint should be present in always mode")
93+
# Verify the pattern: /api/products/abc-123-def -> /api/products/{param:hex_id}
94+
self.assertEqual(meta['http.endpoint'], '/api/products/{param:hex_id}',
95+
f"Expected /api/products/{{param:hex_id}}, got {meta['http.endpoint']}")
96+
97+
def test_always_mode_ignores_http_route(self):
98+
"""Verify always mode calculates endpoint even when http.route is set."""
99+
status, span = self.send_request_and_get_span("/api/orders/999", "always.conf")
100+
self.assertEqual(200, status)
101+
102+
meta = span.get('meta', {})
103+
# In always mode, both http.route and http.endpoint should be present
104+
self.assertIn('http.route', meta, "http.route should be present")
105+
self.assertIn('http.endpoint', meta, "http.endpoint should be present in always mode")
106+
self.assertEqual(meta['http.route'], '/api/orders/:id')
107+
self.assertEqual(meta['http.endpoint'], '/api/orders/{param:int}')
108+
109+
def test_appsec_enables_fallback_mode_by_default(self):
110+
"""Verify that when appsec is enabled, endpoint renaming defaults to fallback mode.
111+
112+
With appsec enabled and no explicit resource_renaming_enabled directive,
113+
the feature should be enabled in fallback mode:
114+
- http.endpoint calculated when http.route not present
115+
- http.endpoint NOT calculated when http.route is present
116+
"""
117+
if self.waf_disabled:
118+
self.skipTest("WAF is disabled - appsec test requires WAF support")
119+
120+
# Test without http.route - should calculate endpoint
121+
status, span = self.send_request_and_get_span("/api/users/456", "appsec_enabled.conf")
122+
self.assertEqual(200, status)
123+
124+
meta = span.get('meta', {})
125+
self.assertIn('http.endpoint', meta,
126+
"http.endpoint should be present when appsec is enabled (fallback mode)")
127+
self.assertEqual(meta['http.endpoint'], '/api/users/{param:int}',
128+
f"Expected /api/users/{{param:int}}, got {meta['http.endpoint']}")
129+
130+
# Test with http.route - should NOT calculate endpoint (fallback mode)
131+
status, span = self.send_request_and_get_span("/api/orders/555", "appsec_enabled.conf")
132+
self.assertEqual(200, status)
133+
134+
meta = span.get('meta', {})
135+
self.assertIn('http.route', meta, "http.route should be present")
136+
self.assertEqual(meta['http.route'], '/api/orders/:id')
137+
self.assertNotIn('http.endpoint', meta,
138+
"http.endpoint should NOT be set when http.route exists in fallback mode (appsec enabled)")

0 commit comments

Comments
 (0)