Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ pup infrastructure hosts list
- **network** - Network monitoring (flows list, devices list/get/interfaces/tags, interfaces list/update)
- **tags** - Host tag management (list, get, add, update, delete)
- **profiling** - Continuous Profiler search/download (profiles list/download, services list, profile-types list, explore flamegraph/callgraph/timeline)
- Uses unstable `/api/unstable/profiling/pup/...` endpoints with a reduced support guarantee: the latest pup release, plus 30 days for older versions (see [EXAMPLES.md](EXAMPLES.md#continuous-profiler))

### Security & Compliance
- **security** - Security monitoring (rules, signals, findings, content-packs, risk-scores)
Expand Down
7 changes: 6 additions & 1 deletion docs/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ pup dbm samples search \

`pup profiling` wraps a small, pup-CLI-scoped Continuous Profiler API.

> **Reduced support guarantee:** `pup profiling` commands call unstable
> (`/api/unstable/profiling/pup/...`) endpoints. These are supported for the latest pup
> release, with older pup versions kept working for 30 days after a newer release. Upgrade
> pup regularly if you depend on these commands.

### Search Profile Events
```bash
pup profiling profiles list --query="service:my-service" --from="1h" --to="now" --limit=20
Expand All @@ -296,7 +301,7 @@ pup profiling services list --query="env:prod" --from="1h" --to="now"
pup profiling profile-types list --query="service:my-service" --from="1h" --to="now"

# Scoped to a specific trace
pup profiling profile-types list --trace-id="trace-abc" --span-id="span-123" --from="1h" --to="now"
pup profiling profile-types list --trace-id="trace-abc" --span-id="span-123" --time-hint="1700000000" --from="1h" --to="now"
```

### Explore a Flame Graph / Top Stack Traces
Expand Down
138 changes: 120 additions & 18 deletions src/commands/profiling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,13 @@ pub async fn profile_types_list(
to: String,
trace_id: Option<String>,
span_id: Option<String>,
time_hint: Option<String>,
extra_headers: &[(&str, &str)],
) -> Result<()> {
let trace_context = trace_context_json(trace_id, span_id, time_hint)?;
let mut body = json!({ "filter": filter_json(&query, &from, &to)? });
if let Some(trace_id) = trace_id {
body["traceContext"] = json!({
"traceId": trace_id,
"spanId": span_id,
"timeHint": null,
});
if let Some(trace_context) = trace_context {
body["traceContext"] = trace_context;
}
let resp = raw_client::raw_post_with_headers(
cfg,
Expand All @@ -205,6 +203,7 @@ pub async fn explore_flamegraph(
to: String,
trace_id: Option<String>,
span_id: Option<String>,
time_hint: Option<String>,
profile_id: Option<String>,
event_id: Option<String>,
attribute: Option<String>,
Expand All @@ -227,6 +226,7 @@ pub async fn explore_flamegraph(
if profile_id.is_some() != event_id.is_some() {
anyhow::bail!("--profile-id and --event-id must be used together");
}
let trace_context = trace_context_json(trace_id, span_id, time_hint)?;

let mut body = json!({
"filter": filter_json(&query, &from, &to)?,
Expand All @@ -242,12 +242,8 @@ pub async fn explore_flamegraph(
"frameGrouping": if frame_grouping == "line" { "LINE" } else { "METHOD" },
"bypassKindTruncation": bypass_kind_truncation,
});
if let Some(trace_id) = trace_id {
body["traceContext"] = json!({
"traceId": trace_id,
"spanId": span_id,
"timeHint": null,
});
if let Some(trace_context) = trace_context {
body["traceContext"] = trace_context;
}
if let Some(profile_id) = profile_id {
body["profileContext"] = json!({
Expand Down Expand Up @@ -927,6 +923,7 @@ mod tests {
"now".into(),
None,
None,
None,
&[],
)
.await;
Expand All @@ -950,9 +947,17 @@ mod tests {
let body = r#"{"data":[],"meta":{"emptyStateReason":{"reason":"NO_DATA","description":"no profiles found"}}}"#;
let _mock = mock_any(&mut server, "POST", body).await;

let result =
super::profile_types_list(&cfg, "".into(), "1h".into(), "now".into(), None, None, &[])
.await;
let result = super::profile_types_list(
&cfg,
"".into(),
"1h".into(),
"now".into(),
None,
None,
None,
&[],
)
.await;
assert!(
result.is_ok(),
"profile_types_list failed: {:?}",
Expand All @@ -975,6 +980,7 @@ mod tests {
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::Regex(r#""traceId":"trace-abc""#.into()),
mockito::Matcher::Regex(r#""spanId":"span-123""#.into()),
mockito::Matcher::Regex(r#""timeHint":"1700000000""#.into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
Expand All @@ -989,6 +995,7 @@ mod tests {
"now".into(),
Some("trace-abc".into()),
Some("span-123".into()),
Some("1700000000".into()),
&[],
)
.await;
Expand Down Expand Up @@ -1016,9 +1023,17 @@ mod tests {
.create_async()
.await;

let result =
super::profile_types_list(&cfg, "".into(), "1h".into(), "now".into(), None, None, &[])
.await;
let result = super::profile_types_list(
&cfg,
"".into(),
"1h".into(),
"now".into(),
None,
None,
None,
&[],
)
.await;
assert!(
result.is_err(),
"expected error but got ok: {:?}",
Expand All @@ -1042,6 +1057,7 @@ mod tests {
Option<String>,
Option<String>,
Option<String>,
Option<String>,
f64,
i32,
i32,
Expand All @@ -1064,6 +1080,7 @@ mod tests {
None,
None,
None,
None,
0.0,
0,
0,
Expand Down Expand Up @@ -1093,6 +1110,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand All @@ -1114,6 +1132,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand Down Expand Up @@ -1152,6 +1171,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand All @@ -1173,6 +1193,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand Down Expand Up @@ -1223,6 +1244,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand All @@ -1244,6 +1266,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand Down Expand Up @@ -1286,6 +1309,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand All @@ -1307,6 +1331,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand Down Expand Up @@ -1353,6 +1378,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand All @@ -1374,6 +1400,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand Down Expand Up @@ -1412,6 +1439,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand All @@ -1433,6 +1461,7 @@ mod tests {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand All @@ -1454,6 +1483,79 @@ mod tests {
std::env::remove_var("DD_TOKEN_STORAGE");
}

#[tokio::test]
async fn test_profiling_explore_flamegraph_trace_context_sends_time_hint() {
let _lock = lock_env().await;
std::env::set_var("DD_TOKEN_STORAGE", "file");
let mut server = mockito::Server::new_async().await;
let cfg = test_config(&server.url());

let _mock = server
.mock("POST", "/api/unstable/profiling/pup/explore/flamegraph")
.match_body(mockito::Matcher::PartialJson(serde_json::json!({
"traceContext": {"traceId": "trace-abc", "spanId": "span-123", "timeHint": "1700000000"}
})))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"sortedStacktracesWithValues":[],"visualizationLink":{"title":"","url":""}}"#)
.create_async()
.await;

let (
profile_type,
_query,
from,
to,
_trace_id,
_span_id,
_time_hint,
profile_id,
event_id,
attribute,
percent_cutoff,
limit_top_stacktraces,
max_stack_trace_size,
frame_regex_filter,
endpoint_regex_filter,
attribute_values_regex_filter,
frame_format,
frame_grouping,
bypass_kind_truncation,
) = flamegraph_args();
let result = super::explore_flamegraph(
&cfg,
profile_type,
"".into(),
from,
to,
Some("trace-abc".into()),
Some("span-123".into()),
Some("1700000000".into()),
profile_id,
event_id,
attribute,
percent_cutoff,
limit_top_stacktraces,
max_stack_trace_size,
frame_regex_filter,
endpoint_regex_filter,
attribute_values_regex_filter,
frame_format,
frame_grouping,
bypass_kind_truncation,
&[],
)
.await;
assert!(
result.is_ok(),
"explore_flamegraph with trace context failed: {:?}",
result.err()
);

cleanup_env();
std::env::remove_var("DD_TOKEN_STORAGE");
}

// ---- trace context ----

#[test]
Expand Down
14 changes: 14 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4749,6 +4749,11 @@ enum ProfilingProfileTypesActions {
trace_id: Option<String>,
#[arg(long, help = "Span ID (used with --trace-id)")]
span_id: Option<String>,
#[arg(
long,
help = "Approximate span time, e.g. a Unix timestamp or RFC3339 (required with --trace-id)"
)]
time_hint: Option<String>,
},
}

Expand Down Expand Up @@ -4838,6 +4843,11 @@ enum ProfilingExploreActions {
trace_id: Option<String>,
#[arg(long, help = "Span ID (used with --trace-id)")]
span_id: Option<String>,
#[arg(
long,
help = "Approximate span time, e.g. a Unix timestamp or RFC3339 (required with --trace-id)"
)]
time_hint: Option<String>,
#[arg(
long,
help = "Existing profile ID to scope the query instead of --query (used together with --event-id)"
Expand Down Expand Up @@ -19486,6 +19496,7 @@ async fn main_inner() -> anyhow::Result<()> {
to,
trace_id,
span_id,
time_hint,
} => {
commands::profiling::profile_types_list(
&cfg,
Expand All @@ -19494,6 +19505,7 @@ async fn main_inner() -> anyhow::Result<()> {
to,
trace_id,
span_id,
time_hint,
&extra_headers,
)
.await?;
Expand All @@ -19507,6 +19519,7 @@ async fn main_inner() -> anyhow::Result<()> {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand All @@ -19528,6 +19541,7 @@ async fn main_inner() -> anyhow::Result<()> {
to,
trace_id,
span_id,
time_hint,
profile_id,
event_id,
attribute,
Expand Down
Loading