You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[processor/spanpruning] preserve outlier subtrees with multi-level detection (#49324)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description
This updates how outliers are calculated to detect non-leaf outliers,
such as an entire slow request that also has subspans, and preserve it.
For example, given a trace that looks something like this:
```
root (1000ms)
handler (50ms)
query (5ms)
query (5ms)
query (5ms)
query (5ms)
handler (50ms)
query (5ms)
query (5ms)
query (5ms)
query (5ms)
handler (50ms)
query (5ms)
query (5ms)
query (5ms)
query (5ms)
handler (50ms)
query (5ms)
query (5ms)
query (5ms)
query (5ms)
handler (50ms)
query (5ms)
query (5ms)
query (5ms)
query (5ms)
handler (50ms)
query (5ms)
query (5ms)
query (5ms)
query (5ms)
handler (50ms) label=handler-with-slow-query
query (5ms)
query (5ms)
query (5ms)
query (45ms) label=slow-query
handler (600ms) label=slow-handler
query (5ms)
query (5ms)
query (5ms)
query (550ms) label=slow-query
```
The current algorithm would produce a result that looks like this:
```
root (1000ms)
handler (600ms) [SUMMARY n=8] label=slow-handler
query (5ms) [SUMMARY n=30] preserved_outlier_count=2
query (45ms) [PRESERVED_OUTLIER] label=slow-query
query (550ms) [PRESERVED_OUTLIER] label=slow-query
```
This entirely misses the context around the slow handler vs just two
slow queries. You can imagine this being especially bad if it wasn't the
query that was slow, but just something else that was not traced, then
no outlier would be found at all.
Now the result will look like:
```
root (1000ms)
handler (600ms) [PRESERVED_OUTLIER] label=slow-handler
query (5ms)
query (5ms)
query (5ms)
query (550ms) label=slow-query
handler (50ms) [SUMMARY n=7] preserved_outlier_count=1
query (5ms) [SUMMARY n=27] preserved_outlier_count=1
query (45ms) [PRESERVED_OUTLIER] label=slow-query
```
So we can see that there is both a very slow handler caused by an
excessively slow query, and another outlier for just a slow query, but
which didn't cause the entire handler to be abnormally slow.
This is done by changing how the processor works a bit, but I think it
is a clearer workflow as well. First the aggregation groups are planned
without outlier influence, then outlier detection runs and protects
various subtrees, then finally the aggregation work is done and spans
are marked for removal.
<!--Describe what testing was performed and which tests were added.-->
#### Testing
All existing unit tests continue to pass, and a new test was added for
the intermediate-span outlier case. In addition, I ran through an
example trace as described above.
<!--Authorship attestation. See AGENTS.md for details. AI agents must
not check this box on behalf
of the user; the human author must check it themselves before the PR is
ready for review.-->
#### Authorship
- [x] I, a human, wrote this pull request description myself.
<!--Please delete paragraphs that you did not use before submitting.-->
# Use this changelog template to create an entry for release notes.
2
+
3
+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4
+
change_type: enhancement
5
+
6
+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
7
+
component: processor/span_pruning
8
+
9
+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10
+
note: Preserve whole outlier subtrees instead of individual spans and detect outliers at every aggregation level, so a slow interior span (e.g. one slow handler among many) keeps its entire subtree.
11
+
12
+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13
+
issues: [49324]
14
+
15
+
# (Optional) One or more lines of additional information to render under the primary note.
16
+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17
+
# Use pipe (|) for multiline entries.
18
+
subtext:
19
+
20
+
# If your change doesn't affect end users or the exported elements of any package,
21
+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22
+
# Optional: The change log or logs in which this entry should be included.
23
+
# e.g. '[user]' or '[user, api]'
24
+
# Include 'user' if the change is relevant to end users.
25
+
# Include 'api' if there is a change to a library API.
Copy file name to clipboardExpand all lines: processor/spanpruningprocessor/README.md
+28-22Lines changed: 28 additions & 22 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -28,7 +28,7 @@ Spans are grouped by:
28
28
29
29
Parent spans are eligible for aggregation when all of their children are aggregated, they share the same name, kind, and status code, and they are not root spans.
30
30
31
-
Optionally, the processor can detect **duration outliers** using statistical methods (IQR or MAD) and either annotate summary spans with outlier correlations or **preserve outlier spans**as individual spans for debugging while still aggregating normal spans.
31
+
Optionally, the processor can detect **duration outliers** using statistical methods (IQR or MAD) and either annotate summary spans with outlier correlations or **preserve outlier subtrees** for debugging while still aggregating normal spans. Detection runs at every aggregation level, so a slow interior span (for example one slow `handler` among many) is caught and its whole subtree is kept intact.
32
32
33
33
This processor is useful for reducing trace data volume while preserving meaningful information about repeated operations.
34
34
@@ -141,13 +141,15 @@ processors:
141
141
# Default: 5
142
142
max_correlated_attributes: 5
143
143
144
-
# Preserve outlier spans as individual spans instead of aggregating
145
-
# When true, only normal spans are aggregated; outliers remain in the trace
144
+
# Preserve outlier subtrees instead of aggregating them
145
+
# When true, each detected outlier and its whole subtree are kept; only
146
+
# normal spans are aggregated. Detection runs at every level, so slow
147
+
# interior spans (not just leaves) are preserved with their subtree.
146
148
# Default: false
147
149
preserve_outliers: false
148
150
149
-
# Maximum number of outlier spans to preserve per aggregation group
150
-
#Spans are selected by most extreme duration first
151
+
# Maximum number of outlier subtrees to preserve per aggregation group
152
+
#Outliers are selected by most extreme duration first
| `outlier_analysis.max_preserved_outliers` | int | 2 | Max outlier subtrees to preserve per group (0=preserve all) |
189
191
| `outlier_analysis.preserve_only_with_correlation` | bool | false | Only preserve outliers if a strong correlation is found |
190
192
| `outlier_analysis.min_outlier_threshold_percent` | float64 | 0.1 | Minimum percentage above median required before a span is considered an outlier |
191
193
@@ -296,10 +298,10 @@ Preserved outlier spans are annotated with:
296
298
| `<prefix>is_preserved_outlier` | bool | Identifies span as a preserved outlier |
297
299
| `<prefix>summary_span_id` | string | SpanID of the associated summary span |
298
300
299
-
A preserved outlier becomes a sibling of its summary span. Grouping is
300
-
depth-aware — leaves and parents are never grouped with same-named ancestors at
301
-
a different depth — so an outlier stays at its original depth and the summary it
302
-
links to (via `summary_span_id`) sits where its group was.
301
+
A preserved outlier (the root of its subtree) becomes a sibling of its summary
302
+
span, and its whole subtree moves with it. The outlier keeps everything beneath it,
303
+
but its aggregated ancestors are replaced by the summary it now hangs from
304
+
(linked via `summary_span_id`).
303
305
304
306
### Histogram Buckets
305
307
@@ -401,13 +403,15 @@ This helps identify root causes of latency issues:
401
403
- **Minimal when disabled**: Zero overhead (no sorting or calculations)
402
404
- **Recommended**: Use `min_group_size: 7` or higher to skip analysis on small groups
403
405
404
-
### Preserving Outlier Spans (Optional)
406
+
### Preserving Outlier Subtrees (Optional)
405
407
406
-
When `outlier_analysis.preserve_outliers: true`, detected outlier spans are **kept as individual spans** instead of being aggregated. This provides:
408
+
When `outlier_analysis.preserve_outliers: true`, each detected outlier is **kept along with its whole subtree** instead of being aggregated. A leaf outlier (e.g. a slow query) is the degenerate single-span case; an interior outlier (e.g. a slow `handler`) keeps every span beneath it. This provides:
407
409
408
-
- **Full visibility** into slow operations for debugging
409
-
- **Preserved context**: Original attributes, events, and links remain intact
410
-
- **Selective aggregation**: Only prune repetitive normal spans
410
+
- **Full visibility** into slow operations for debugging, including everything the slow span did
411
+
- **Preserved context**: original attributes, events, links, and tree structure remain intact
412
+
- **Selective aggregation**: only prune repetitive normal spans
413
+
414
+
Preservation keeps everything **beneath** an outlier, not above it: the outlier's subtree is kept, but its ancestors still aggregate normally. The preserved subtree is reparented as a sibling of its summary span (the whole subtree moves with its root), so a slow leaf ends up under the same summary its normal siblings collapsed into, and a slow interior span hangs off the summary that replaced its peers.
411
415
412
416
#### Configuration
413
417
@@ -425,8 +429,8 @@ processors:
425
429
426
430
| Field | Type | Default | Description |
427
431
|-------|------|---------|-------------|
428
-
|`preserve_outliers`| bool | false | Keep outliers as individual spans instead of aggregating |
429
-
|`max_preserved_outliers`| int | 2 | Max outliers to preserve per group (0=preserve all detected) |
|`max_preserved_outliers`| int | 2 | Max outlier subtrees to preserve per group (0=preserve all detected) |
430
434
|`preserve_only_with_correlation`| bool | false | Only preserve outliers if a strong attribute correlation is found |
431
435
432
436
#### Example Output
@@ -464,9 +468,11 @@ handler
464
468
465
469
#### Behavior Notes
466
470
467
-
-**Parent aggregation**: Parents can still be aggregated if all their children are either aggregated or preserved as outliers
468
-
-**Skip aggregation**: If preserving outliers leaves too few normal spans (below `min_spans_to_aggregate`), the entire group is left unchanged
469
-
-**Selection order**: Outliers are preserved starting with the most extreme (longest duration) first
471
+
-**Multi-level detection**: outliers are detected within each sibling group at every level (leaf groups and eligible parent groups) that meets `min_group_size`.
472
+
-**Subtree, not ancestors**: a preserved outlier keeps everything beneath it (its whole subtree), but its ancestors still aggregate; the subtree is reparented as a sibling of its summary.
473
+
-**Nested outliers**: an outlier already inside a preserved subtree is not preserved (or counted) again, since the enclosing subtree already keeps it.
474
+
-**Skip aggregation**: if protection leaves a group below `min_spans_to_aggregate`, that group is left unchanged.
475
+
-**Selection order**: outliers are preserved starting with the most extreme (longest duration) first, capped by `max_preserved_outliers` subtrees per group.
470
476
471
477
## Pipeline Placement
472
478
@@ -617,7 +623,7 @@ The processor emits the following metrics to help monitor its operation:
617
623
| `otelcol_processor_spanpruning_aggregations_created` | Total number of aggregation summary spans created |
618
624
| `otelcol_processor_spanpruning_traces_processed` | Total number of traces processed |
619
625
| `otelcol_processor_spanpruning_outliers_detected` | Total spans identified as outliers by analysis (when `enable_outlier_analysis: true`) |
620
-
| `otelcol_processor_spanpruning_outliers_preserved` | Total outlier spans kept as individual spans (when `preserve_outliers: true`) |
| `otelcol_processor_spanpruning_outliers_correlations_detected` | Total aggregation groups where outliers had correlated attributes |
622
628
| `otelcol_processor_spanpruning_bytes_received` | Total bytes of serialized traces received before pruning (when `enable_bytes_metrics: true`) |
623
629
| `otelcol_processor_spanpruning_bytes_processed_input` | Total bytes of serialized traces in the matched subset, measured before pruning (when `enable_bytes_metrics: true`) |
0 commit comments