grafana / grafana/otel-profiling-ruby
on_finish() lacks explicit root_span_only guard — relies on fragile attribute-absence check
- Dominant language
- Ruby
- Stars
- 9
- Forks
- 5
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
`on_finish()` does not explicitly check `root_span_only` like `on_start()` does. Instead, it relies on checking if `span.attributes["pyroscope.profile.id"]` is nil and returning early. While this works today because child spans never get that attribute set when `root_span_only=true`, the design is fragile.
## The Bug Pattern (found in Java)
The Java OTel-Pyroscope integration (`grafana/otel-profiling-java`) had a bug where `onEnd()` unconditionally cleared the profiling/tracing context for ALL spans, but `onStart()` only set context for root spans when `rootSpanOnly=true`. When a child span ended during a root span's lifetime, it prematurely cleared the profiling context, causing all subsequent samples to be untagged.
Fixed in https://github.com/grafana/otel-profiling-java/pull/75
## Ruby's Situation
In the Ruby integration:
- `on_start` has the `root_span_only` guard and only sets the `pyroscope.profile.id` attribute on root spans
- `on_finish` checks if `span.attributes["pyroscope.profile.id"]` is nil and returns early if so
This works **by accident** — it relies on the implicit invariant that child spans never have the `pyroscope.profile.id` attribute. If any other code or future change sets that attribute on a child span, the same bug as Java would surface.
## Suggested Fix
Add an explicit `root_span_only` guard to `on_finish()`, mirroring the guard in `on_start()`. This is how the Python integration handles it correctly:
```python
def on_start(self, span, parent_context=None):
if _is_root_span(span): # ← explicit guard
pyroscope.add_thread_tag(...)
def on_end(self, span):
if _is_root_span(span): # ← same explicit guard
pyroscope.remove_thread_tag(...)
```
## Cross-Language Audit
| Language | Same Bug? | Notes |
|----------|-----------|-------|
| **Java** | YES (fixed) | `onEnd()` unconditionally cleared context — fixed in PR #75 |
| **Go** | No | Different architecture prevents it structurally |
| **Python** | No | Symmetric explicit root-span guards |
| **Ruby** | Fragile | Works by accident via attribute-absence proxy |
| **.NET** | Related bug | Symmetric guards exist, but `IsRootSpan()` heuristic is flawed (issue #66) |
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.