Otel
DevOps & SecurityotelOtel is an Agent Skill for devops & security published in the posit-dev/py-shiny repository on GitHub: a SKILL.md instruction file an AI agent can load to do this task.
Use when adding observability to a Shiny for Python (py-shiny) app with OpenTelemetry - tracing reactive execution, profiling slow outputs or update cycles, monitoring sessions in production, exporting spans to a backend (Jaeger, Logfire, Honeycomb, Datadog, OTLP), suppressing telemetry for sensitive code, or when tempted to call trace.set_tracer_provider() inside app code to set up instrumentation.
How to use this skill
Bring this guide into your coding agent with a prompt tailored to the tool you use.
- Open your project in Codex.
- Copy and paste the prompt below.
- Review files and risks before approving any action.
Inspect the Agent Skill at https://github.com/posit-dev/py-shiny/blob/HEAD/shiny/.agents/skills/otel/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/otel, but wait for my approval before writing files or running scripts.
Copying this prompt does not install or run the skill. Review third-party files before use.
OpenTelemetry for Shiny for Python apps
Overview
Shiny has built-in OpenTelemetry instrumentation: it emits spans for session
lifecycle, reactive update cycles, and individual calc/effect/output executions.
Enable it with zero-code auto-instrumentation — launch the app under
opentelemetry-instrument. Do NOT configure providers inside app code
(trace.set_tracer_provider(...)): providers install once per process, so
in-code setup is silently ignored under external instrumentation.
Setup and run
uv pip install "shiny[otel]" # includes opentelemetry-distro[otlp]
Print spans to the console while developing:
opentelemetry-instrument --traces_exporter console --logs_exporter console \
--metrics_exporter none shiny run app.py
Export to a backend (OTLP to http://localhost:4317 is the default; any
OTLP-compatible backend works — Jaeger, Logfire, Honeycomb, Datadog, ...):
OTEL_SERVICE_NAME=my-shiny-app opentelemetry-instrument shiny run app.py
All standard OTEL_* environment variables apply (OTEL_EXPORTER_OTLP_ENDPOINT,
OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_PROTOCOL, sampling via
OTEL_TRACES_SAMPLER). --reload is compatible. The app needs no OTel code at
all. Shiny resolves the tracer provider lazily at span-creation time, so the
wrapper's provider is picked up automatically.
At the all level the span hierarchy looks like:
session_start
└─ reactive_update
├─ reactive.calc filtered_data
└─ output result
Collection levels: SHINY_OTEL_COLLECT
Controls how much Shiny telemetry is emitted (default all):
| Level | Emits | Use |
|---|---|---|
none | nothing from Shiny | disable Shiny spans, keep your own |
session | session lifecycle only | low-overhead production |
reactive_update | + one span per flush cycle | balanced production |
reactivity | + per calc/effect/output spans, value-update logs | development, debugging |
all | everything (currently = reactivity) | maximum detail |
SHINY_OTEL_COLLECT=session opentelemetry-instrument shiny run app.py
Read the current level in code with shiny.otel.get_level().
Per-object control: otel.suppress / otel.collect
Disable Shiny telemetry for sensitive reactives (secrets, PII), or force it on when the global level is low. Both work as decorators and context managers:
from shiny import otel, reactive, render
@render.text
@otel.suppress # must be BELOW @render/@reactive (closer to the function)
def result_private():
return authenticate(input.username(), input.password())
with otel.suppress():
@reactive.calc # everything created in this block is suppressed
def sensitive_calc():
return load_secrets()
with otel.collect():
@reactive.calc # re-enabled despite the outer suppress
def public_calc():
return load_public_data()
Key semantics:
- The setting is captured when the reactive object is created, not when it
runs.
with otel.suppress():inside a reactive function body does nothing to that reactive's spans. suppress/collectare absolute per-object overrides: they beatSHINY_OTEL_COLLECTin both directions. Infrastructure spans (session_start,session_end,reactive_update) follow only the env var.- Only Shiny's internal telemetry is affected — spans you create manually are always recorded.
Custom spans for business logic
Use the standard OpenTelemetry API; no Shiny-specific setup needed:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@reactive.calc
def expensive_computation():
with tracer.start_as_current_span("database_query") as span:
result = run_query()
span.set_attribute("query.rows", len(result))
return result
Common mistakes
- Calling
trace.set_tracer_provider()in app code underopentelemetry-instrument→ logsOverriding of current TracerProvider is not allowedand is ignored. Configure via the wrapper + env vars instead. (Exception: SDKs that manage OTel themselves, e.g.logfire.configure()— then runshiny run app.pydirectly, without the wrapper.) @otel.suppressplaced above@reactive.calc/@render.*→TypeError. It must wrap the plain function, not the reactive object.- Suppressing at runtime with
with otel.suppress():inside a reactive body → no effect on that reactive's Shiny spans; the level was captured at creation. - Traces show
service.name: unknown_service→ setOTEL_SERVICE_NAME. - No Shiny spans at all → check the app was launched under
opentelemetry-instrument, and thatSHINY_OTEL_COLLECTis notnone. - Too much overhead in production → lower
SHINY_OTEL_COLLECTtosessionorreactive_update, and/or sample withOTEL_TRACES_SAMPLER=parentbased_traceidratio+OTEL_TRACES_SAMPLER_ARG=0.1.
See shiny/otel/__init__.py's module docs and examples/open-telemetry/ in
the shiny repo for backend-specific configuration (Jaeger, Logfire, Honeycomb,
Datadog, New Relic).