Skip to content

Commit 25d33a3

Browse files
authored
Merge pull request #333 from ojung/ojung/feat/symdb-probe-signatures
Improve symdb probe-locations reliability and output
2 parents 8303f31 + 627fac8 commit 25d33a3

8 files changed

Lines changed: 473 additions & 98 deletions

File tree

docs/EXAMPLES.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,13 @@ pup debugger probes create \
437437
--probe-location com.example.MyClass:myMethod \
438438
--capture "request.id" --capture "user.name"
439439

440+
# Use a method signature from --view probe-locations output
441+
pup debugger probes create \
442+
--service my-service \
443+
--env staging \
444+
--probe-location "com.example.MyClass:myMethod(String, int)" \
445+
--capture "request.id"
446+
440447
# Increase capture depth for nested objects (default: 1)
441448
pup debugger probes create \
442449
--service my-service \
@@ -507,6 +514,7 @@ pup debugger probes watch "probe-id" --wait 30
507514
### Pipeline: Create and Watch
508515
```bash
509516
# Search for a method, create a probe, and watch events
517+
# Note: --view probe-locations may output signatures like TYPE:METHOD(args)
510518
pup symdb search --service my-service --query MyController --view probe-locations \
511519
| head -1 \
512520
| xargs -I{} pup debugger probes create --service my-service --env staging --probe-location {} --capture --ttl 1h \
@@ -536,7 +544,7 @@ pup symdb search --service my-service --query MyController --view full
536544
# Scope names only
537545
pup symdb search --service my-service --query MyController --view names
538546

539-
# Probe locations (type:method format)
547+
# Probe locations (type:method or type:method(args) format)
540548
pup symdb search --service my-service --query MyController --view probe-locations
541549
```
542550

skills/dd-debugger/SKILL.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ pup debugger probes delete <PROBE_ID>
7070

7171
## Service Context
7272

73-
**Always run this before creating probes.** It returns JSON by default, showing environments with active instances, tracer versions, and supported probe features. If the service runs in multiple environments, ask the user which one to target — don't guess.
73+
**Always run this before creating probes.** It returns JSON by default (like all other commands) showing environments with active instances, tracer versions, and supported probe features. If the service runs in multiple environments, ask the user which one to target — don't guess.
7474

7575
```bash
76-
# Full JSON output (default, avoid)
76+
# Full JSON output (default)
7777
pup debugger context my-service
7878

79-
# Compact: just the fields you need (preferably use this to avoid context bloat)
79+
# Compact: just the fields you need
8080
pup debugger context my-service --fields service,language,envs
8181

8282
# Filter to a specific environment
@@ -117,13 +117,23 @@ pup debugger probes create \
117117
--capture "order.items[0].price"
118118
```
119119

120+
To disambiguate overloaded methods, pass a signature with argument types:
121+
122+
```bash
123+
pup debugger probes create \
124+
--service my-service \
125+
--env staging \
126+
--probe-location "com.example.MyClass:myMethod(int, java.lang.String)" \
127+
--capture "user.name"
128+
```
129+
120130
**Options:**
121131

122132
| Flag | Description | Default |
123133
|------|-------------|---------|
124134
| `--service` | Service name (required) ||
125135
| `--env` | Environment (required) ||
126-
| `--probe-location` | `TYPE:METHOD` (required) ||
136+
| `--probe-location` | `TYPE:METHOD` or `TYPE:METHOD(args)` (required). The signature form disambiguates overloaded methods. ||
127137
| `--language` | `java`, `python`, `dotnet`, `go` | Auto-detected from symdb |
128138
| `--capture EXPR` | Capture expression (repeatable). Use dot notation for fields, brackets for indexing. | None |
129139
| `--capture` | Without value: enable full snapshot (capture everything). | No snapshot |
@@ -300,7 +310,7 @@ pup debugger probes watch <ID> --limit 1 \
300310
| No events appearing | Check `--from` (default is `now`); probe may need time to instrument |
301311
| Instrumentation errors | Check stderr output from watch for status errors |
302312
| Auth error | Run `pup auth login` or set `DD_API_KEY` + `DD_APP_KEY` + `DD_SITE` |
303-
| Wrong method signature | Use the `dd-symdb` skill to find exact `TYPE:METHOD` values |
313+
| Wrong method signature | Use the `dd-symdb` skill to find exact `TYPE:METHOD` or `TYPE:METHOD(args)` values |
304314

305315
## References
306316

skills/dd-pup/SKILL.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Pup CLI for Datadog API operations. Supports OAuth2 and API key auth.
2727
| Check SLOs | `pup slos list` |
2828
| On-call teams | `pup on-call teams list` |
2929
| Security signals | `pup security signals list --query "*" --from 24h` |
30-
| Inspect runtime values | `pup debugger probes create --service my-svc --env prod --probe-location com.example.MyClass:myMethod` |
30+
| Inspect runtime values | `pup debugger probes create --service my-svc --env prod --probe-location "com.example.MyClass:myMethod"` or `"com.example.MyClass:myMethod(String, int)"` |
3131
| Find probe-able methods | `pup symdb search --service my-svc --query MyController --view probe-locations` |
3232
| Check auth | `pup auth status` |
3333
| Refresh token | `pup auth refresh` |
@@ -192,11 +192,17 @@ pup debugger context my-svc --env prod
192192
pup symdb search --service my-svc --query MyController --view probe-locations
193193

194194
# Place a log probe with capture expressions
195+
# --probe-location accepts TYPE:METHOD or TYPE:METHOD(arg1, arg2, ...) with optional signature
195196
pup debugger probes create --service my-svc --env prod \
196197
--probe-location "com.example.MyController:handleRequest" \
197198
--capture "request.id" --capture "request.headers" \
198199
--ttl 1h
199200

201+
# With method signature (useful when the method is overloaded)
202+
pup debugger probes create --service my-svc --env prod \
203+
--probe-location "com.example.MyController:handleRequest(String, HttpHeaders)" \
204+
--capture "request.id" --ttl 1h
205+
200206
# Watch probe events — compact output
201207
pup debugger probes watch <PROBE_ID> --fields "message,captures,timestamp" --timeout 60 --limit 10 --wait 5
202208

skills/dd-symdb/SKILL.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pup symdb search --service my-service --query "Controller" --view names
4646

4747
### Probe Locations View
4848

49-
`TYPE:METHOD` pairs suitable for `--probe-location` in `pup debugger probes create`.
49+
`TYPE:METHOD(arg1, arg2, ...)` signatures suitable for `--probe-location` in `pup debugger probes create`. Falls back to `TYPE:METHOD` when no argument info is available.
5050

5151
```bash
5252
pup symdb search --service my-service --query "VetController" --view probe-locations
@@ -81,6 +81,13 @@ pup debugger probes create \
8181
--probe-location "com.example.MyController:handleRequest" \
8282
--template "handleRequest called with id={id}"
8383

84+
# Or use the full signature when multiple overloads exist
85+
pup debugger probes create \
86+
--service my-service \
87+
--env production \
88+
--probe-location "com.example.MyController:handleRequest(int, java.lang.String)" \
89+
--template "handleRequest called with id={id}"
90+
8491
# 3. Stream events
8592
pup debugger probes watch <PROBE_ID> --timeout 60 --limit 10
8693
```

src/client.rs

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ use reqwest_middleware::{Middleware, Next};
99

1010
use crate::config::Config;
1111

12+
/// HTTP error with the status code preserved for programmatic matching.
13+
#[derive(Debug)]
14+
pub struct HttpError {
15+
pub status: u16,
16+
pub method: String,
17+
pub url: String,
18+
pub body: String,
19+
}
20+
21+
impl std::fmt::Display for HttpError {
22+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23+
write!(
24+
f,
25+
"{} {} failed (HTTP {}): {}",
26+
self.method, self.url, self.status, self.body
27+
)
28+
}
29+
}
30+
31+
impl std::error::Error for HttpError {}
1232
#[cfg(not(target_arch = "wasm32"))]
1333
struct BearerAuthMiddleware {
1434
token: String,
@@ -727,7 +747,7 @@ pub async fn raw_request(
727747
let client = reqwest::Client::new();
728748
let method_name = method.to_uppercase();
729749
let method = reqwest::Method::from_bytes(method_name.as_bytes())
730-
.map_err(|_| anyhow::anyhow!("unsupported HTTP method: {method}"))?;
750+
.map_err(|_| anyhow::anyhow!("unsupported HTTP method: {method_name}"))?;
731751
let mut req = client.request(method, &url);
732752
if !query.is_empty() {
733753
req = req.query(query);
@@ -754,7 +774,13 @@ pub async fn raw_request(
754774
if !resp.status().is_success() {
755775
let status = resp.status();
756776
let text = resp.text().await.unwrap_or_default();
757-
anyhow::bail!("API error (HTTP {status}): {text}");
777+
return Err(HttpError {
778+
status: status.as_u16(),
779+
method: method_name,
780+
url,
781+
body: text,
782+
}
783+
.into());
758784
}
759785

760786
let resp_ct = resp
@@ -804,7 +830,13 @@ pub async fn raw_get(
804830
if !resp.status().is_success() {
805831
let status = resp.status();
806832
let body = resp.text().await.unwrap_or_default();
807-
anyhow::bail!("GET {url} failed (HTTP {status}): {body}");
833+
return Err(HttpError {
834+
status: status.as_u16(),
835+
method: "GET".into(),
836+
url,
837+
body,
838+
}
839+
.into());
808840
}
809841
Ok(resp.json().await?)
810842
}
@@ -833,7 +865,13 @@ pub async fn raw_patch(
833865
if !resp.status().is_success() {
834866
let status = resp.status();
835867
let body = resp.text().await.unwrap_or_default();
836-
anyhow::bail!("PATCH {url} failed (HTTP {status}): {body}");
868+
return Err(HttpError {
869+
status: status.as_u16(),
870+
method: "PATCH".into(),
871+
url,
872+
body,
873+
}
874+
.into());
837875
}
838876
Ok(resp.json().await?)
839877
}
@@ -882,7 +920,13 @@ async fn raw_post_impl(
882920
if !resp.status().is_success() {
883921
let status = resp.status();
884922
let body = resp.text().await.unwrap_or_default();
885-
anyhow::bail!("POST {url} failed (HTTP {status}): {body}");
923+
return Err(HttpError {
924+
status: status.as_u16(),
925+
method: "POST".into(),
926+
url: url.to_string(),
927+
body,
928+
}
929+
.into());
886930
}
887931
Ok(resp.json().await?)
888932
}
@@ -1008,7 +1052,13 @@ pub async fn raw_delete(cfg: &Config, path: &str) -> anyhow::Result<()> {
10081052
if !resp.status().is_success() {
10091053
let status = resp.status();
10101054
let body = resp.text().await.unwrap_or_default();
1011-
anyhow::bail!("DELETE {url} failed (HTTP {status}): {body}");
1055+
return Err(HttpError {
1056+
status: status.as_u16(),
1057+
method: "DELETE".into(),
1058+
url,
1059+
body,
1060+
}
1061+
.into());
10121062
}
10131063
Ok(())
10141064
}

src/commands/debugger.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ struct ResolvedProbe<'a> {
9191
env: &'a str,
9292
type_name: &'a str,
9393
method_name: &'a str,
94+
signature: Option<&'a str>,
9495
language: &'a str,
9596
template_str: String,
9697
segments: serde_json::Value,
@@ -207,13 +208,17 @@ fn build_probe_payload(p: &ResolvedProbe<'_>) -> serde_json::Value {
207208
"language": p.language,
208209
"where": {
209210
"type_name": p.type_name,
210-
"method_name": p.method_name
211+
"method_name": p.method_name,
211212
},
212213
"evaluate_at": "EXIT",
213214
"tags": [],
214215
"version": 0
215216
});
216217

218+
if let Some(sig) = p.signature {
219+
probe["where"]["signature"] = serde_json::json!(sig);
220+
}
221+
217222
if let Some(w) = &p.when {
218223
probe["when"] = w.clone();
219224
}
@@ -286,10 +291,16 @@ pub async fn probes_create(cfg: &Config, params: ProbeCreateParams<'_>) -> Resul
286291
None
287292
};
288293

289-
let (type_name, method_name) = probe_location
294+
let (type_name, method_part) = probe_location
290295
.rsplit_once(':')
291296
.ok_or_else(|| anyhow::anyhow!("probe_location must be in format TYPE:METHOD"))?;
292297

298+
// Split optional signature: "myMethod(int,int)" → method="myMethod", sig="(int,int)"
299+
let (method_name, signature) = match method_part.find('(') {
300+
Some(i) => (&method_part[..i], Some(&method_part[i..])),
301+
None => (method_part, None),
302+
};
303+
293304
// Parse condition if provided
294305
let when = if let Some(cond) = condition {
295306
let body = serde_json::json!({
@@ -340,6 +351,7 @@ pub async fn probes_create(cfg: &Config, params: ProbeCreateParams<'_>) -> Resul
340351
env,
341352
type_name,
342353
method_name,
354+
signature,
343355
language,
344356
template_str,
345357
segments,
@@ -730,6 +742,7 @@ mod tests {
730742
env: "staging",
731743
type_name: "com.example.MyClass",
732744
method_name: "myMethod",
745+
signature: None,
733746
language: "java",
734747
template_str,
735748
segments,
@@ -996,6 +1009,61 @@ mod tests {
9961009
assert!(out.get("bogus").is_none());
9971010
}
9981011

1012+
#[test]
1013+
fn test_probe_location_parsing_plain() {
1014+
let loc = "com.example.MyClass:myMethod";
1015+
let (_, method_part) = loc.rsplit_once(':').unwrap();
1016+
let (method_name, signature) = match method_part.find('(') {
1017+
Some(i) => (&method_part[..i], Some(&method_part[i..])),
1018+
None => (method_part, None),
1019+
};
1020+
assert_eq!(method_name, "myMethod");
1021+
assert_eq!(signature, None);
1022+
}
1023+
1024+
#[test]
1025+
fn test_probe_location_parsing_with_signature() {
1026+
let loc = "com.example.MyClass:myMethod(int,java.lang.String)";
1027+
let (type_name, method_part) = loc.rsplit_once(':').unwrap();
1028+
let (method_name, signature) = match method_part.find('(') {
1029+
Some(i) => (&method_part[..i], Some(&method_part[i..])),
1030+
None => (method_part, None),
1031+
};
1032+
assert_eq!(type_name, "com.example.MyClass");
1033+
assert_eq!(method_name, "myMethod");
1034+
assert_eq!(signature, Some("(int,java.lang.String)"));
1035+
}
1036+
1037+
#[test]
1038+
fn test_probe_location_parsing_empty_args() {
1039+
let loc = "com.example.MyClass:myMethod()";
1040+
let (_, method_part) = loc.rsplit_once(':').unwrap();
1041+
let (method_name, signature) = match method_part.find('(') {
1042+
Some(i) => (&method_part[..i], Some(&method_part[i..])),
1043+
None => (method_part, None),
1044+
};
1045+
assert_eq!(method_name, "myMethod");
1046+
assert_eq!(signature, Some("()"));
1047+
}
1048+
1049+
#[test]
1050+
fn test_build_probe_payload_no_signature() {
1051+
let payload = test_resolved_probe(|_| {});
1052+
let where_block = &payload["data"]["attributes"]["probe"]["where"];
1053+
assert!(where_block.get("signature").is_none());
1054+
}
1055+
1056+
#[test]
1057+
fn test_build_probe_payload_with_signature() {
1058+
let payload = test_resolved_probe(|rp| {
1059+
rp.signature = Some("(int,java.lang.String)");
1060+
});
1061+
let where_block = &payload["data"]["attributes"]["probe"]["where"];
1062+
assert_eq!(where_block["type_name"], "com.example.MyClass");
1063+
assert_eq!(where_block["method_name"], "myMethod");
1064+
assert_eq!(where_block["signature"], "(int,java.lang.String)");
1065+
}
1066+
9991067
fn sample_context_response() -> serde_json::Value {
10001068
serde_json::json!({
10011069
"data": {

0 commit comments

Comments
 (0)