Skip to content

otel: Reactive Execution Instrumentation #2135

@schloerke

Description

@schloerke

Goal: Instrument individual reactive computations (calcs, effects, outputs) with descriptive labels and source attribution.

Status: ✅ Complete

PR #2149 implements all Phase 4 functionality with additional improvements from code review.

Completed Tasks

  • ✅ Created shiny/otel/_labels.py with:
    • generate_reactive_label(func, label_type, namespace, modifier) - Generate descriptive labels
    • Extract function name, handle anonymous functions
    • Add module namespace prefix (e.g., "reactive my-module:myValue")
    • Support modifiers like "cache", "event"
  • ✅ Extended shiny/otel/_attributes.py with:
    • extract_source_ref(func) - Extract source location using inspect module
    • Return dict with code.filepath, code.lineno, code.function
    • Improvement: Separate try/catch blocks maximize attribute extraction
  • ✅ Modified Calc_.update_value() to wrap execution in span with label "reactive <name>"
    • Improvement: Lazy name generation eliminates code duplication
  • ✅ Modified Effect_._run() to wrap execution in span with label "observe <name>"
    • Improvement: Lazy name generation eliminates code duplication
  • ✅ Modified Outputs.__call__ to wrap renderer execution in span with label "output <name>"
    • Improvement: Lazy callables for both name and attributes
  • ✅ Store OTel attributes (source refs) on reactive objects at creation time
  • ✅ Enhanced with_otel_span_async() to accept callable name parameter
  • ✅ Moved OTel imports to top-level in _reactives.py and all test files for optimal performance
  • Added internal test helpers to shiny/otel/_core.py:
    • reset_tracing_state(*, tracing_enabled=None) - Reset cached tracing state
    • patch_tracing_state(*, tracing_enabled) - Context manager for temporary patching
    • Both use keyword-only arguments, not exported in public API
    • Replaced 22+ occurrences across 4 test files

Acceptance Criteria

  • ✅ Reactive computations create spans when SHINY_OTEL_COLLECT >= reactivity
  • ✅ Spans have descriptive labels (function names, not just "reactive")
  • ✅ Source location attributes (code.filepath, code.lineno) included when available
  • ✅ Module namespaces reflected in span names
  • ✅ Async context propagates correctly (child spans nest under reactive.update)
  • ✅ Spans include parent reactive.update span context
  • Bonus: Code duplication eliminated through lazy evaluation (~150 lines reduced)
  • Bonus: Strong typing with no suppressions (except known OTel SDK limitation)
  • Bonus: Test code improved with helper utilities for better encapsulation

Implementation Improvements

  1. Callable Name Parameter - Added support for name: str | Callable[[], str] in span wrappers

    • Enables lazy evaluation of expensive name generation
    • Only called if collection is enabled
    • Eliminates if/else duplication
  2. Top-Level Imports - Moved OTel imports to module level

    • Removes repeated inline import overhead during execution
    • Cleaner code structure in both production and test code
  3. Separate Try/Catch - extract_source_ref() uses separate blocks

    • Maximizes attribute extraction even if some operations fail
    • More robust error handling
  4. Test Helper Utilities - Internal functions for test isolation

    • reset_tracing_state(*, tracing_enabled=None) - Replaces direct state manipulation
    • patch_tracing_state(*, tracing_enabled) - Replaces unittest.mock.patch
    • Better encapsulation, cleaner test code, easier refactoring
  5. Code Reduction - ~150 lines eliminated:

    • Calc: 67% reduction (40 → 12 lines)
    • Effect: 50% reduction (90 → 45 lines)
    • Output: 48% reduction (25 → 13 lines)

Files Created

  • shiny/otel/_labels.py
  • tests/pytest/test_otel_reactive_execution.py (16 tests, all passing)

Files Modified

  • shiny/otel/_core.py (added test helpers)
  • shiny/otel/_attributes.py
  • shiny/otel/_span_wrappers.py (added callable name support)
  • shiny/reactive/_reactives.py
  • shiny/session/_session.py
  • tests/pytest/test_otel_foundation.py (updated to use test helpers)
  • tests/pytest/test_otel_session.py (updated to use test helpers)
  • tests/pytest/test_otel_reactive_flush.py (updated to use test helpers)

Test Results

  • ✅ All 408 tests pass (407 passed + 1 skipped)
  • ✅ Pyright: 0 errors, 0 warnings
  • ✅ Flake8: Clean
  • ✅ Black: Formatted
  • ✅ isort: Sorted

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions