OpenTelemetry
Observability and telemetry instrumentation for Shiny applications
otel
otel
OpenTelemetry instrumentation for Shiny applications.
OpenTelemetry support for observing Shiny application behavior, performance, and reactive execution.
Quick Start Example
from shiny import App, ui, render, reactive
from shiny import otel
app_ui = ui.page_fluid(
ui.input_slider("n", "N", 1, 100, 50),
ui.output_text("result"),
ui.output_text("result_private"),
ui.output_text("result_instrumented"),
)
def server(input, output, session):
@render.text
def result():
# Full Shiny telemetry for this output
return f"Value: {input.n()}"
@render.text
@otel.suppress # Disables Shiny's internal telemetry for sensitive operations
def result_private():
return f"Private value: {input.n()}"
@render.text
@otel.collect # Enables Shiny's internal telemetry even when default is suppressed
def result_instrumented():
return f"Instrumented value: {input.n()}"
app = App(app_ui, server)Run with:
export SHINY_OTEL_COLLECT=all
python app.pyWatch the console output to see Shiny's spans for result and result_instrumented but not for result_private.
Table of Contents
What is OpenTelemetry?
OpenTelemetry is an open-source observability framework that provides a standardized way to collect telemetry data (traces, metrics, and logs) from applications. It's vendor-neutral and widely supported by observability platforms.
Key concepts:
- Traces: Records of requests flowing through your application, showing timing and dependencies
- Spans: Individual units of work within a trace (e.g., a function execution)
- Logs: Structured log events with context
- Attributes: Key-value metadata attached to spans and logs
Why Use OpenTelemetry with Shiny?
Shiny applications have complex reactive execution flows that can be difficult to debug and optimize. OpenTelemetry provides:
1. Reactive Flow Visualization
See exactly how reactive computations propagate through your app: - Which calcs and effects execute during each update cycle - Parent-child relationships between reactive components - Execution timing and ordering
2. Performance Analysis
Identify bottlenecks in your application: - Which outputs take the longest to render - Which reactive computations are slow - How many reactive invalidations occur per user interaction
3. Debugging Aid
Understand unexpected behavior: - Why certain reactive computations run (or don't run) - Execution order when multiple things invalidate - Async operation context propagation
4. Production Monitoring
Track application health in production: - Session lifecycle and user behavior patterns - Error rates and types - Performance over time
Getting Started
Installation
Install Shiny with OpenTelemetry support:
pip install "shiny[otel]"This installs both the OpenTelemetry API (required) and SDK (for exporters).
Basic Setup
Configure OpenTelemetry before running your app:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from shiny import App, ui, render, reactive
# Configure OpenTelemetry
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
# Your app code...Note: Shiny uses lazy initialization for its OpenTelemetry tracer, so you can configure it anywhere in your code before the app runs.
Quick Example
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from shiny import App, ui, render, reactive
# Configure OTel
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
app_ui = ui.page_fluid(
ui.input_slider("n", "N", 1, 100, 50),
ui.output_text("result"),
)
def server(input, output, session):
@render.text
def result():
return f"Value: {input.n()}"
app = App(app_ui, server)Run with:
export SHINY_OTEL_COLLECT=all
python app.pyYou'll see OpenTelemetry spans printed to the console showing Shiny's internal execution.
Collection Levels
Shiny provides five collection levels to control the granularity of telemetry:
none
No Shiny telemetry collected. Use when you want to completely disable Shiny's instrumentation while keeping your own custom spans.
Overhead: None Use case: Disabling telemetry entirely
session
Only session lifecycle spans (session start/end, HTTP/WebSocket connections).
Overhead: Minimal (1-2 spans per session) Use case: Basic session tracking in production
reactive_update
Session spans + reactive update cycle spans (one span per flush cycle).
Overhead: Low (1 span per reactive flush) Use case: Understanding how many update cycles occur
reactivity
Everything from reactive_update + individual reactive execution spans (calcs, effects, outputs, extended tasks) + value update logs.
Overhead: Moderate (1 span per reactive computation) Use case: Detailed debugging and development
all
All available telemetry (currently equivalent to reactivity). Reserved for future expansion.
Overhead: Moderate Use case: Maximum observability
Setting Collection Level
Via environment variable:
export SHINY_OTEL_COLLECT=session # or: none, reactive_update, reactivity, all
python app.pyThe default level is all if not specified.
Configuration
Environment Variables
SHINY_OTEL_COLLECT
Sets the default collection level for the application.
# Minimal overhead - session lifecycle only
export SHINY_OTEL_COLLECT=session
# Balanced - update cycles tracked
export SHINY_OTEL_COLLECT=reactive_update
# Full detail - all reactive executions
export SHINY_OTEL_COLLECT=reactivity
# Maximum (same as reactivity currently)
export SHINY_OTEL_COLLECT=allOpenTelemetry SDK Configuration
The OpenTelemetry SDK itself supports many configuration options via environment variables:
# Service name
export OTEL_SERVICE_NAME=my-shiny-app
# OTLP exporter endpoint
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# Resource attributes
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0
# Trace sampling
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1 # Sample 10% of tracesSee OpenTelemetry SDK Configuration for full details.
Programmatic Control
Important: The suppression setting for a reactive object (calc, effect, output) is captured at initialization time – when the reactive object is created – not when the reactive function executes. This means otel.suppress affects whether telemetry is suppressed on the reactive object during its definition, and that setting is used for all subsequent executions.
Decorator
Use otel.suppress as a decorator to disable Shiny telemetry for a reactive function. The decorator stamps the suppression setting on the function, and the reactive object reads it when it is created:
from shiny import otel
@reactive.calc
@otel.suppress
def sensitive_computation():
"""This entire calc runs without Shiny telemetry on every execution."""
api_key = input.api_key()
return validate_api_key(api_key)Important: When decorating reactive objects, apply otel.suppress before (i.e., closer to the function than) the reactive decorator:
# Correct order -- otel.suppress is applied to the function first,
# then @reactive.calc reads the stamped setting at initialization time
@reactive.calc
@otel.suppress
def my_calc():
pass
# Incorrect - will raise TypeError
@otel.suppress # Cannot wrap a reactive object
@reactive.calc
def my_calc():
passContext Manager (Initialization Time Only)
Use otel.suppress() as a context manager to suppress telemetry during reactive object creation. Any reactive objects defined inside the with block will have telemetry suppressed:
from shiny import otel
with otel.suppress():
# Reactive objects created here are never instrumented
@reactive.calc
def sensitive_calc():
return load_secrets()
# Reactive objects created outside use the default level
@reactive.calc
def normal_calc():
return load_public_data()Does NOT work at runtime: Using with otel.suppress() inside a reactive function body has no effect on Shiny's internal telemetry for that reactive object, because the suppression setting was already captured when the object was created:
@reactive.calc
def load_secrets():
... # This part is instrumented with Shiny telemetry
@reactive.calc
def my_calc():
# THIS DOES NOT WORK as intended for Shiny telemetry.
# `load_secrets()` will still generate spans/logs because
# reactive objects are captured at initialization time.
with otel.suppress():
sensitive_data = load_secrets()
return sensitive_dataotel.collect Decorator
Use otel.collect as a decorator to enable Shiny's internal telemetry for a reactive function when the default level is suppressed:
from shiny import otel
@reactive.calc
@otel.collect
def instrumented_computation():
"""This calc always runs with Shiny telemetry, regardless of context."""
return load_public_data()otel.collect Context Manager (Initialization Time Only)
Use otel.collect() as a context manager to enable telemetry during reactive object creation. Any reactive objects defined inside the with block will have telemetry enabled:
from shiny import otel
with otel.suppress():
# Reactive objects created here have telemetry suppressed
with otel.collect():
@reactive.calc
def public_calc():
# This calc has telemetry enabled despite the outer suppress
return load_public_data()
@reactive.calc
def private_calc():
# Back to suppressed
return load_private_data()Best Practices
1. Use Batch Processor in Production
Replace SimpleSpanProcessor with BatchSpanProcessor for better performance:
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# Instead of:
# provider.add_span_processor(SimpleSpanProcessor(exporter))
# Use:
provider.add_span_processor(BatchSpanProcessor(exporter))Batching reduces overhead by buffering spans and sending them in batches.
2. Choose Appropriate Collection Level
Development:
export SHINY_OTEL_COLLECT=reactivity # Full detail for debuggingProduction:
export SHINY_OTEL_COLLECT=session # Minimal overhead
# or
export SHINY_OTEL_COLLECT=reactive_update # Balanced3. Add Resource Attributes
Include service metadata in your traces:
from opentelemetry.sdk.resources import Resource
resource = Resource.create({
"service.name": "my-shiny-app",
"service.version": "1.2.3",
"deployment.environment": "production",
"service.namespace": "analytics-team",
})
provider = TracerProvider(resource=resource)4. Protect Sensitive Data
Use otel.suppress for operations involving sensitive data:
from shiny import otel
@reactive.calc
@otel.suppress
def process_credentials():
"""Disable telemetry for credential handling."""
username = input.username()
password = input.password()
return authenticate(username, password)Remember: otel.suppress only disables Shiny's internal telemetry. Your own custom OpenTelemetry spans are unaffected.
5. Enable Error Sanitization
When app.sanitize_errors=True, Shiny automatically sanitizes error messages in spans to prevent leaking sensitive information:
app = App(app_ui, server, sanitize_errors=True)6. Use Sampling in High-Traffic Apps
For high-traffic applications, use trace sampling to reduce overhead:
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
# Sample 10% of traces
sampler = ParentBasedTraceIdRatio(0.1)
provider = TracerProvider(sampler=sampler)7. Add Custom Spans for Business Logic
Complement Shiny's spans with your own for business-critical operations:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@reactive.calc
def expensive_computation():
with tracer.start_as_current_span("database_query") as span:
span.set_attribute("query.type", "analytics")
result = run_query()
span.set_attribute("query.rows", len(result))
return resultObservability Backends
Shiny's OpenTelemetry integration works with any OTLP-compatible backend.
Jaeger (Open Source)
Perfect for local development and self-hosted monitoring.
Setup:
docker run -d --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
jaegertracing/all-in-one:latestConfiguration:
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from shiny import App, ui, render, reactive
# Configure OpenTelemetry
resource = Resource.create({
"service.name": "my-shiny-app",
"deployment.environment": "development",
})
provider = TracerProvider(resource=resource)
# Use OTLP exporter to send traces to Jaeger
otlp_exporter = OTLPSpanExporter(
endpoint="http://localhost:4317", # Jaeger's OTLP gRPC port
insecure=True, # For local development
)
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(provider)
# Now build your Shiny app
app_ui = ui.page_fluid(
ui.h2("My Shiny App"),
ui.input_slider("n", "N", 1, 100, 50),
ui.output_text("result"),
)
def server(input, output, session):
@render.text
def result():
import time
time.sleep(0.1) # Simulate work
return f"Value: {input.n()}"
app = App(app_ui, server)UI: http://localhost:16686
Open the Jaeger UI to explore your Shiny app's traces. You'll see: - Session lifecycle spans - Reactive update cycles - Individual calc/effect/output executions - Timing and nesting relationships
Pydantic Logfire (Managed)
Modern observability platform with excellent Python support.
Setup:
pip install logfireConfiguration:
import logfire
# Configure BEFORE importing Shiny
logfire.configure(
token=os.environ["LOGFIRE_TOKEN"],
service_name="my-shiny-app",
)
# Now import Shiny
from shiny import App, uiUI: https://logfire.pydantic.dev
Honeycomb (Managed)
Powerful observability platform focused on trace analysis.
Configuration:
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
exporter = OTLPSpanExporter(
endpoint="https://api.honeycomb.io/v1/traces",
headers={"x-honeycomb-team": os.environ["HONEYCOMB_API_KEY"]},
)Datadog (Managed)
Enterprise observability platform with APM features.
Configuration:
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
exporter = OTLPSpanExporter(
endpoint="https://http-intake.logs.datadoghq.com/v1/traces",
headers={
"DD-API-KEY": os.environ["DD_API_KEY"],
},
)New Relic (Managed)
Full-stack observability platform.
Configuration:
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
exporter = OTLPSpanExporter(
endpoint="https://otlp.nr-data.net:4317",
headers={"api-key": os.environ["NEW_RELIC_LICENSE_KEY"]},
)Console (Development)
Simple console output for debugging. See the open-telemetry example for a complete working demonstration of console exporters with collection control.
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
exporter = ConsoleSpanExporter()Troubleshooting
No Spans Appearing
Problem: Console/backend shows no spans from Shiny.
Solutions: 1. Verify OpenTelemetry is configured correctly
Check collection level:
# Make sure it's not "none" export SHINY_OTEL_COLLECT=allVerify exporter endpoint is correct and reachable
Check for error messages in console
Too Much Overhead
Problem: Application performance degraded with OpenTelemetry enabled.
Solutions: 1. Use BatchSpanProcessor instead of SimpleSpanProcessor: python from opentelemetry.sdk.trace.export import BatchSpanProcessor provider.add_span_processor(BatchSpanProcessor(exporter))
Lower collection level:
export SHINY_OTEL_COLLECT=session # MinimalEnable sampling:
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio provider = TracerProvider(sampler=ParentBasedTraceIdRatio(0.1))Use
@otel.suppressfor high-frequency operations:@reactive.calc @otel.suppress def high_frequency_calc(): pass
Sensitive Data in Traces
Problem: Passwords or API keys appearing in span attributes.
Solutions: 1. Use @otel.suppress decorator on sensitive reactive functions: python @reactive.calc @otel.suppress def process_api_keys(): api_key = input.api_key() return validate(api_key)
Use
with otel.suppress():when defining reactive objects that handle sensitive data (the setting is captured at initialization time):with otel.suppress(): @reactive.calc def handle_password(): password = input.password() return hash_password(password)Enable error sanitization:
app = App(app_ui, server, sanitize_errors=True)Use
otel.collectto re-enable telemetry for specific calcs inside a broad suppress block:with otel.suppress(): # Most of the app has telemetry suppressed with otel.collect(): @reactive.calc def public_calc(): return load_public_data()
Spans Not Nested Correctly
Problem: Parent-child relationships incorrect in traces.
Solutions: 1. Ensure you're using async context propagation correctly 2. Check that custom spans use start_as_current_span(): ```python # CORRECT with tracer.start_as_current_span(“my_span”): pass
# WRONG - breaks context chain span = tracer.start_span(“my_span”) ```
Backend Not Receiving Traces
Problem: OpenTelemetry configured but backend shows no data.
Solutions: 1. Check exporter endpoint URL and authentication 2. Verify network connectivity to backend 3. Check backend-specific requirements (headers, format) 4. Use ConsoleSpanExporter first to verify spans are generated: python from opentelemetry.sdk.trace.export import ConsoleSpanExporter provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
ImportError: No module named 'opentelemetry'
Problem: OpenTelemetry not installed.
Solution:
pip install "shiny[otel]"Next Steps
- Examples: Check out
examples/open-telemetry/for working example apps - API Reference: See API documentation for
shiny.otelmodule - OpenTelemetry Docs: https://opentelemetry.io/docs/languages/python/
- Shiny Docs: https://shiny.posit.co/py/
Getting Help
- GitHub Issues: https://github.com/posit-dev/py-shiny/issues
- Community: https://forum.posit.co/c/shiny
- OpenTelemetry Community: https://cloud-native.slack.com (#otel-python channel)
otel.suppress
otel.suppress(func=None)Disable Shiny's internal OTel instrumentation for a function or block.
Serves a dual purpose depending on how it is called:
- As a no-parens decorator (
@otel.suppress): Stamps the plain function withOtelCollectLevel.NONEat definition time. Reactive objects created from the function will not emit Shiny internal spans or logs. - As a context manager (
with otel.suppress():): Sets the collection level toNONEfor the duration of the block. Reactive objects created inside the block captureNONEas their level.
Parameters
func : Any = None-
The plain function to suppress. Only provided when used as a decorator (
@otel.suppress, no parens). Must be a plain callable — passing areactive.calc,reactive.effect, or renderer object raisesTypeErrorwith instructions for the correct decorator ordering.
Returns
: Any-
When used as a decorator: the original function, unchanged except for the
_shiny_otel_collect_levelattribute being set. When used as a context manager: an_OtelContextinstance whose__exit__restores the previous level viaContextVar.reset.
Raises
: TypeError-
If applied to a
reactive.calc,reactive.effect, or renderer object (@otel.suppressmust come before those decorators), or to any non-callable.
Note
Only affects spans and logs created by Shiny itself (reactive calculations and value updates). User-defined OpenTelemetry spans are unaffected.
Collection level is captured at initialization time for reactive objects — when reactive.calc, reactive.effect, or reactive.value is instantiated. Changing the context variable after initialization has no effect on already-created reactive objects.
Both otel.suppress and otel.collect are backed by a ContextVar and are async-safe: concurrent tasks each see their own level independently.
Examples
Decorator (no parens):
from shiny import reactive, otel
@reactive.calc
@otel.suppress
def sensitive_calc():
return load_api_key()Context manager (parens required):
from shiny import reactive, otel
with otel.suppress():
private_counter = reactive.value(0)
@reactive.calc
def private_calc():
return private_counter() * 2Nested with otel.collect to re-enable for one object:
from shiny import reactive, otel
with otel.suppress():
@reactive.calc
def private_calc(): # suppressed
return load_private_data()
with otel.collect():
@reactive.calc
def public_calc(): # re-enabled
return load_public_data()See Also
otel.collect
otel.collect(func=None)Enable Shiny's internal OTel instrumentation for a function or block.
Counterpart to suppress. Useful when the global default has been lowered via SHINY_OTEL_COLLECT or when inside a with otel.suppress(): block and a specific reactive object needs telemetry re-enabled.
Serves a dual purpose depending on how it is called:
- As a no-parens decorator (
@otel.collect): Stamps the plain function withOtelCollectLevel.ALLat definition time. Reactive objects created from the function will emit Shiny internal spans and logs regardless of the surrounding context. - As a context manager (
with otel.collect():): Sets the collection level toALLfor the duration of the block. Reactive objects created inside the block captureALLas their level.
Parameters
func : Any = None-
The plain function to enable collection for. Only provided when used as a decorator (
@otel.collect, no parens). Must be a plain callable — passing areactive.calc,reactive.effect, or renderer object raisesTypeErrorwith instructions for the correct decorator ordering.
Returns
: Any-
When used as a decorator: the original function, unchanged except for the
_shiny_otel_collect_levelattribute being set. When used as a context manager: an_OtelContextinstance whose__exit__restores the previous level viaContextVar.reset.
Raises
: TypeError-
If applied to a
reactive.calc,reactive.effect, or renderer object (@otel.collectmust come before those decorators), or to any non-callable.
Note
Only affects spans and logs created by Shiny itself. User-defined OpenTelemetry spans are unaffected.
Collection level is captured at initialization time for reactive objects. otel.collect overrides the surrounding context level — including a SHINY_OTEL_COLLECT=none environment variable — for reactive objects created within its scope.
Both otel.collect and otel.suppress are backed by a ContextVar and are async-safe: concurrent tasks each see their own level independently.
Examples
Decorator (no parens) — override a low global default:
from shiny import reactive, otel
# Even with SHINY_OTEL_COLLECT=none, this calc is always instrumented
@reactive.calc
@otel.collect
def public_calc():
return load_public_data()Context manager — re-enable within a suppress block:
from shiny import reactive, otel
with otel.suppress():
@reactive.calc
def private_calc():
return load_private_data() # suppressed
with otel.collect():
@reactive.calc
def public_calc():
return load_public_data() # re-enabledSee Also
otel.get_level
otel.get_level()Get the current OpenTelemetry collect level.
The collect level is determined in the following order: 1. Context variable (set via otel.suppress() context manager) 2. SHINY_OTEL_COLLECT environment variable 3. Default: ALL
Returns
:OtelCollectLevel-
The current collect level.
Examples
Check the current collection level:
from shiny import otel
# Get the current level
level = otel.get_level()
print(f"Current level: {level.name}") # e.g., "ALL", "SESSION", "NONE"Use with suppress context manager:
from shiny import otel
print(otel.get_level().name) # "ALL" (default)
with otel.suppress():
print(otel.get_level().name) # "NONE"
print(otel.get_level().name) # "ALL" (restored)