Nothing
# ## Use when otel v0.3.0 is released: https://github.com/r-lib/otel/commit/43e59c45a7de50cfd8af95f73d45f9899f957b44
# # attributes = as_attributes(attributes)
# attributes = if (!is.null(attributes)) {
# as_attributes(attributes)
# }
NULL
#' @importFrom otel
#' start_span
#' end_span
#' get_active_span
#' is_tracing_enabled
#' with_active_span
NULL
# Fixed variable named required for `otel::default_tracer_name()`
# which is used in `otel::get_tracer()` within `promises_otel_tracer()`
otel_tracer_name <- "co.posit.r-package.promises"
# Using local scope avoids an environment object lookup on each call.
# Inspired by httr2:::get_tracer(). Taken from Shiny
# Benchmark: https://github.com/rstudio/shiny/pull/4269/files#diff-0cc4a76032b57fcc125d41dfa3fb0f0c39976bb00a1d84bb56a0b77c331ce2d1R42
testthat__is_testing <- function() {
identical(Sys.getenv("TESTTHAT"), "true")
}
promises_otel_tracer <- local({
tracer <- NULL
function() {
if (!is.null(tracer)) {
return(tracer)
}
if (testthat__is_testing()) {
# Don't cache the tracer in unit tests. It interferes with tracer provider
# injection in otelsdk::with_otel_record().
return(otel::get_tracer())
}
tracer <<- otel::get_tracer()
tracer
}
})
#' OpenTelemetry integration
#'
#' @description
#' \pkg{otel} provides tools for integrating with OpenTelemetry, a framework for
#' observability and tracing in distributed systems.
#'
#' These methods are intended to enhance the framework to be used with the
#' \pkg{promises} package, not as a generic replacement.
#'
#' Developer note - Barret 2025/09: This otel span handoff promise domain topic is
#' complex and has been discussed over many hours. Many advanced Shiny/R
#' developers are not even aware of promise domains (very reasonable!),
#' therefore this topic requires more in-depth documentation and examples.
#'
#' @section Definitions:
#'
#' * Promise domain: An environment in which has setup/teardown methods. These
#' environments can be composed together to facilitate execution context for
#' promises. In normal R execution, this can be achieved with scope / stack.
#' But for complex situations, such as the currently open graphics device,
#' async operations require promise domains to setup/teardown these contexts
#' to function properly. Otherwise a multi-stage promise that adds to the
#' graphics device at each stage will only ever print to the most recently
#' created graphics device, not the associated graphics device. These promise
#' domains are not automatically created, they must be manually added to the
#' execution stack, for example `with_otel_promise_domain()` does this for
#' OpenTelemetry spans.
#' * Promise domain restoration: When switching from one promise chain to
#' another, the execution context is torn down and then re-established. This
#' re-establishment is called "promise domain restoration". During this
#' process, the promise domains are restored in their previously established
#' combination order.
#' * Promise chain: A set of promise objects to execute over multiple async
#' ticks.
#' * Async tick: the number of times an event loop must run to move computation
#' forward. (Similar to a JavaScript event loop tick.)
#' * `then()` promise domain capture: When `then()` is called, it will capture
#' the current promise domain. This promise domain is restored (only if
#' needed) when evaluating the given `onFulfilled` and `onRejected` callbacks.
#' This captured promise domain does not go into any downstream promise chain
#' objects. The only way the promise domain is captured is exactly when the
#' `then()` method is called.
#'
#' `with_otel_promise_domain()` creates a promise domain that restores the
#' currently active OpenTelemetry span from when a call to `promises::then()` is
#' executed. Given the special circumstance where only the current otel span is
#' needed to continue recording (not a full ancestry tree of otel spans), we can
#' capture _just_ the current otel span and reactivate that otel span during promise
#' domain restoration.
#'
#' @section When promise domains are captured:
#'
#' Asynchronous operation
#' * Creates `async_op` otel span
#' * Automatically ends the otel span (`async_op`) when the promise (p)
#' resolves or rejects
#'
#' The code below illustrates an example of when the promise domain are
#' created/captured/restored and when otel span objects are
#' created/activated/reactivated/ended.
#'
#' ```r
#' # t0.0
#' p2 <- with_otel_promise_domain({
#' # t0.1
#' p <- with_otel_span("async_op", {
#' # ... return a promise ...
#' init_async_work() |> # t0.2
#' then( # t0.3
#' some_async_work # t1.0
#' )
#' }) # t0.4, t1.0, t2.0
#' p |>
#' then( # t0.5
#' more_async_work # t3.0
#' )
#' }) # t0.6
#'
#' p_final <-
#' p2 |> then( # t0.7
#' final_work # t4.0
#' )
#' ```
#'
#' An in-depth explanation of the execution timeline is below.
#' * At the first initial tick, `t0.*`:
#' * `t0.0`: The code is wrapped in `with_otel_promise_domain()`
#' * `t0.1`: The `async_op` otel span is created and activated
#' * `t0.2`: Some async work is initiated
#' * `t0.3`: `then()` is called, capturing the active `async_op` otel span (as it
#' is called within `with_otel_promise_domain()`)
#' * `t0.4`: The `with_otel_span()` call exits, but the `async_op` otel span is
#' not ended as the promise is still pending. The returned promise has a
#' `finally()` step added to it that will end the otel span `async_op` when `p`
#' is resolved.
#' * `t0.5`: Another `then()` is called, but there is no active otel span to
#' capture
#' * `t0.6`: The otel span promise domain call exits
#' * `t0.7`: Another `then()` is called. No otel span will be captured as there is
#' no active otel span / promise domain
#' * At the first followup tick, `t1.0`:
#' * The active `async_op` otel span is reactivated during promise domain
#' restoration for the duration of the `then` callback
#' * The `some_async_work` function is called
#' * At tick, `t2.0`:
#' * `some_async_work` has resolved
#' * A hidden `finally()` step closes the otel span, `async_op`
#' * `p` is now resolved
#' * At tick, `t3.0`:
#' * There is no active otel span at `t0.5`, so no otel span is reactivated during
#' promise domain restoration
#' * The `more_async_work` function is executed
#' * At tick, `t4.0`:
#' * `more_async_work` has resolved, therefore `p2` is now resolved
#' * There was no otel span promise domain at `t0.7`, so no attempt is made to
#' reactivate any otel span
#' * The `final_work` function is executed
#' * At tick, `t5.0`:
#' * `p_final` has resolved
#'
#' @section Complexity:
#'
#' When reactivating the `k`th step in a promise chain, the currently active
#' otel span (during the call to `then()`) will be reactivated during promise domain
#' restoration (`O(1)`). To restore a chain of promises, the active otel span will
#' be restored at each step (`O(n)`) due to the **`n`** calls to wrapping each
#' `onFulfilled` and `onRejected` callbacks inside `then()`.
#'
#' If we did NOT have a handoff promise domain for otel span restoration, a regular
#' promise domain approach would be needed at each step to restore the active
#' otel span. Each step would call `with_active_span()` `k` times (`O(k)`, where as
#' handoff domain computes in `O(1)`). Taking a step back, to restore each otel span
#' at for every step in a promise chain would then take `O(n^2)` time, not
#' `O(n)`. The standard, naive promise domain approach does not scale for
#' multiple similar promise domain restorations.
#'
#'
#' @section Execution model for `with_otel_promise_domain()`:
#'
#' 1. `with_otel_promise_domain(expr)` is called.
#' * The following steps all occur within `expr`.
#' 2. Create an otel span object using `otel::start_span()`.
#' * We need the otel span to be active during the a followup async operation.
#' Therefore, `otel::start_local_active_span()` is not appropriate as the
#' otel span would be ended when the function exits, not when the promise chain
#' resolves.
#' 2. Be sure your otel span is activated before calling `promises::then()`.
#' * Activate it using `with_otel_span(name, expr)` (which also
#' creates/ends the otel span) or `otel::with_active_span(span, expr)`.
#' 3. Call `promises::then()`
#' * When `promises::then()` is called, the two methods (`onFulfilled` and
#' `onRejected`) capture the currently active spans. (Performed by the
#' initial `with_otel_promise_domain()`)
#' 4. During reactivation of the promise chain step, the previously captured
#' otel span is reactivated via `with_active_span()`. (Performed by the initial
#' `with_otel_promise_domain()`)
#'
#' @section OpenTelemetry span compatibility:
#'
#' For otel span objects to exist over may async ticks, the otel span must be created
#' using `otel::start_span()` and later ended using `otel::end_span()`. Ending
#' the otel span must occur **after** any promise chain work has completed.
#'
#' If we were to instead use `otel::start_local_active_span()`, the otel span would
#' be ended when the function exits, not when the promise chain completes. Even
#' though the local otel span is created, activated, and eventually ended, the otel span
#' will not exist during reactivation of the otel span promise domain.
#'
#' `with_otel_span()` is a convenience method that creates, activates, and
#' ends the otel span only after the returned promise (if any) resolves. It also
#' properly handles both synchronous (ending the otel span within `on.exit()`) and
#' asynchronous operations (ending the otel span within `promises::finally()`).
#'
#'
#'
#'
#' @describeIn otel Creates an OpenTelemetry span, executes the given expression within it, and
#' ends the span.
#'
#' This method **requires** the use of `with_otel_promise_domain()`
#' to be within the execution stack.
#'
#' This function is designed to handle both synchronous and asynchronous
#' (promise-based) operations. For promises, the span is automatically ended
#' when the promise resolves or rejects.
#'
#' Returns the result of evaluating `expr`. If `expr` returns a promise, the
#' span will be automatically ended when the promise completes.
#'
#' This function differs from synchronous otel span operations in that it
#' installs a promise domain and properly handles asynchronous operations. In
#' addition, the internal span will be ended either when the function exits (for
#' synchronous operations) or when a returned promise completes (for
#' asynchronous operations).
#'
#' If OpenTelemetry is not enabled, the expression will be evaluated without any
#' tracing context.
#'
#' @param name Character string. The name of the otel span.
#' @param expr An expression to evaluate within the otel span context.
#' @param ... Additional arguments passed to [`otel::start_span()`].
#' @param tracer (Required) An `{otel}` tracer. It is required to provide your
#' own tracer from your own package. See [`otel::get_tracer()`] for more
#' details.
#' @param attributes Attributes passed through [`otel::as_attributes()`] (when
#' not `NULL`)
#' @export
#' @seealso [`otel::start_span()`], [`otel::with_active_span()`],
#' [`otel::end_span()`]
#' @examples
#' \dontrun{
#' # Common usage:
#' with_otel_promise_domain({
#' # ... deep inside some code execution ...
#'
#' # Many calls to `with_otel_span()` within `with_otel_promise_domain()`
#' with_otel_span("my_operation", {
#' # ... do some work ...
#' })
#' })
#' }
#' \dontrun{
#' with_otel_promise_domain({
#' # ... deep inside some code execution ...
#'
#' # Synchronous operation
#' # * Creates `my_operation` span
#' result <- with_otel_span("my_operation", {
#' # ... do some work ...
#' print(otel::get_active_span()$name) # "my_operation"
#'
#' # Nest (many) more spans
#' prom_nested <- with_otel_span("my_nested_operation", {
#' # ... do some more work ...
#' promise_resolve(42) |>
#' then(\(value) {
#' print(otel::get_active_span()$name) # "my_nested_operation"
#' print(value) # 42
#' })
#' })
#'
#' # Since `then()` is called during the active `my_operation` span,
#' # the `my_operation` span will be reactivated in the `then()` callback.
#' prom_nested |> then(\(value) {
#' print(otel::get_active_span()$name) # "my_operation"
#' value
#' })
#' })
#'
#' # Since `then()` is called where there is no active span,
#' # there is no _active_ span in the `then()` callback.
#' result |> then(\(value) {
#' stopifnot(inherits(otel::get_active_span(), "otel_span_noop"))
#' print(value) # 42
#' })
#' })
#' }
with_otel_span <- function(
name,
expr,
...,
tracer,
attributes = NULL
) {
# If tracing is not enabled, just run the expression
#
# ```
# tracer <- get_tracer(); bench::mark(check = FALSE, !is_tracing_enabled(tracer), inherits(tracer, "otel_tracer_noop"), !.subset2(tracer, "is_enabled")())[,1:4]
# #> A tibble: 3 × 4
# #> expression min median `itr/sec`
# #> <bch:expr> <bch:tm> <bch:tm> <dbl>
# #> 1 "!is_tracing_enabled(tracer)" 2.79µs 3.4µs 278206.
# #> 2 "inherits(tracer, \"otel_tracer_noop\")" 205.01ns 287ns 3184760.
# #> 3 "!.subset2(tracer, \"is_enabled\")()" 82.02ns 164ns 5716255.
# ```
#
# Speed is important here as to not slow down non-tracing operations.
# Therefore, we use the fastest method and require `tracer=` to be provided.
# The method is safe to call as it just returns either `TRUE` or `FALSE` (if a
# noop tracer).
if (!.subset2(tracer, "is_enabled")()) {
return(force(expr))
}
span <- start_span(name, ..., tracer = tracer, attributes = attributes)
needs_cleanup <- TRUE
cleanup <- function() {
end_span(span)
}
on.exit(if (needs_cleanup) cleanup(), add = TRUE)
result <- with_active_span(span, expr)
if (is.promising(result)) {
needs_cleanup <- FALSE
result <- finally(result, cleanup)
}
result
}
#' @describeIn otel Adds an idempotent handoff Active OpenTelemetry span promise domain.
#'
#' Package authors are required to use this function to have otel span context
#' persist across asynchronous boundaries. This method is only needed once per
#' promise domain stack. If you are unsure, feel free to call
#' `with_otel_promise_domain()` as the underlying promise domain will only be
#' added if not found within the current promise domain stack. If your package
#' **only** works within Shiny apps, Shiny will have already added the domain so
#' no need to add it yourself. If your package works outside of Shiny and you
#' use `{promises}` (i.e. `{chromote}`), then you'll need to use this wrapper
#' method.
#'
#' This method adds a _handoff_ Active OpenTelemetry span promise domain to the
#' expression evaluation. This _handoff_ promise domain will only run once on
#' reactivation. This is critical if there are many layered
#' `with_otel_span()` calls, such as within Shiny reactivity. For example, if
#' we nested many `with_otel_span()` calls of which each call added a promise
#' domain that reactivated each otel span on restore, we'd reactivate `k` otel
#' span objects (`O(k)`) when we only need to activate the **last** span
#' (`O(1)`).
#'
#' Returns the result of evaluating `expr` within the otel span promise domain.
#'
#' @export
with_otel_promise_domain <- function(expr) {
# with_otel_promise_domain <- function(expr) {
# Do not add the otel span handoff promise domain twice
if (has_otel_promise_domain()) {
return(force(expr))
}
act_span_pd <- create_otel_promise_domain()
with_promise_domain(act_span_pd, expr)
}
#' @describeIn otel Local OpenTelemetry span promise domain
#'
#' Adds an OpenTelemetry span promise domain to the local scope. This is useful
#' for `{coro}` operations where encapsulating the coro operations inside a
#' `with_*()` methods is not allowed.
#'
#' When not using `{coro}`, please prefer to use `with_otel_span()` or
#' `with_otel_promise_domain()`.
#' @export
#' @param envir The "local" environment in which to add the promise domain. When
#' the environment is exited, the promise domain is removed.
local_otel_promise_domain <- function(envir = parent.frame()) {
# local_otel_promise_domain <- function(envir = parent.frame()) {
if (has_otel_promise_domain()) {
return(invisible())
}
local_promise_domain(
create_otel_promise_domain(),
envir = envir
)
}
# Modifies the current promise domain to include `domain` for the local scope.
local_promise_domain <- function(
domain,
envir = parent.frame(),
replace = FALSE
) {
oldval <- current_promise_domain()
if (replace) {
globals$domain <- domain
} else {
globals$domain <- compose_domains(oldval, domain)
}
# Inspired by `withr::defer()`
defer(
{
globals$domain <- oldval
},
envir = envir
)
invisible()
}
# # TODO: Set attributes on the current active span
# # 5. Set attributes on the current active span
# set_otel_span_attrs(status = 200L)
# -- Helpers --------------------------------------------------------------
has_otel_promise_domain <- function(domain = current_promise_domain()) {
# Works for:
# * `list()` `compose_domains()` result
# * `NULL` - current_promise_domain() init value
# * environment objects - `new_promise_domain()` result
isTRUE(domain[[".otel_promise_domain"]])
}
#' Create a OpenTelemetry handoff promise domain
#'
#' Creates a handoff promise domain captures the currently active span. Upon
#' promise domain restoration, the previously captured span will be reactivated.
#'
#' @param span An OpenTelemetry span object.
#' @noRd
create_otel_promise_domain <- function() {
new_promise_domain(
.otel_promise_domain = TRUE, # set flag for later discovery
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
## Motivation on why we can't use
## `promises_otel_tracer()$get_active_span()` here to save 10 microseconds:
## https://github.com/r-lib/otel/issues/31#issuecomment-3250669840
#> ❯❯ bench::mark(promises_otel_tracer()$get_active_span(), get_active_span())
#> # A tibble: 2 × 13
#> expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time result memory time
#> <bch:expr> <bch:> <bch:t> <dbl> <bch:byt> <dbl> <int> <dbl> <bch:tm> <list> <list> <list>
#> 1 promises_otel_tra… 4.8µs 6.52µs 144270. 0B 14.4 9999 1 69.3ms <otl_spn_> <Rprofmem> <bench_tm>
#> 2 get_active_span() 14.3µs 15.13µs 64876. 7.02KB 38.9 9994 6 154ms <otl_spn_> <Rprofmem> <bench_tm>
# ℹ 1 more variable: gc <list>
## tl/dr: Speed comes at the cost of safety. Just use this for now as it is only a single cost on restore.
# Instead, use `get_active_span()` directly so that the recording status
# can be changed over time for testing and within the terminal. The 10
# microseconds is not worth the inconsistent behavior or the time to debug
# the inconsistent behavior.
# The `is_recording()` method is deemed safe to call as the underlying C++
# method just checks an internal class instance property, is
# non-allocating and has been declared noexcept.
span <- get_active_span()
if (!.subset2(span, "is_recording")()) {
return(onFulfilled)
}
function(...) {
with_active_span(span, {
onFulfilled(...)
})
}
},
wrapOnRejected = function(onRejected) {
force(onRejected)
span <- get_active_span()
if (!.subset2(span, "is_recording")()) {
return(onRejected)
}
function(...) {
with_active_span(span, {
onRejected(...)
})
}
}
)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.