% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/otel.R
\name{with_otel_span}
\alias{with_otel_span}
\alias{with_otel_promise_domain}
\alias{local_otel_promise_domain}
\title{OpenTelemetry integration}
\usage{
with_otel_span(name, expr, ..., tracer, attributes = NULL)

with_otel_promise_domain(expr)

local_otel_promise_domain(envir = parent.frame())
}
\arguments{
\item{name}{Character string. The name of the otel span.}

\item{expr}{An expression to evaluate within the otel span context.}

\item{...}{Additional arguments passed to \code{\link[otel:start_span]{otel::start_span()}}.}

\item{tracer}{(Required) An \code{{otel}} tracer. It is required to provide your
own tracer from your own package. See \code{\link[otel:get_tracer]{otel::get_tracer()}} for more
details.}

\item{attributes}{Attributes passed through \code{\link[otel:as_attributes]{otel::as_attributes()}} (when
not \code{NULL})}

\item{envir}{The "local" environment in which to add the promise domain. When
the environment is exited, the promise domain is removed.}
}
\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{Functions}{
\itemize{
\item \code{with_otel_span()}: Creates an OpenTelemetry span, executes the given expression within it, and
ends the span.

This method \strong{requires} the use of \code{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 \code{expr}. If \code{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.

\item \code{with_otel_promise_domain()}: 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
\code{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
\strong{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 \code{{promises}} (i.e. \code{{chromote}}), then you'll need to use this wrapper
method.

This method adds a \emph{handoff} Active OpenTelemetry span promise domain to the
expression evaluation. This \emph{handoff} promise domain will only run once on
reactivation. This is critical if there are many layered
\code{with_otel_span()} calls, such as within Shiny reactivity. For example, if
we nested many \code{with_otel_span()} calls of which each call added a promise
domain that reactivated each otel span on restore, we'd reactivate \code{k} otel
span objects (\code{O(k)}) when we only need to activate the \strong{last} span
(\code{O(1)}).

Returns the result of evaluating \code{expr} within the otel span promise domain.

\item \code{local_otel_promise_domain()}: Local OpenTelemetry span promise domain

Adds an OpenTelemetry span promise domain to the local scope. This is useful
for \code{{coro}} operations where encapsulating the coro operations inside a
\verb{with_*()} methods is not allowed.

When not using \code{{coro}}, please prefer to use \code{with_otel_span()} or
\code{with_otel_promise_domain()}.

}}
\section{Definitions}{

\itemize{
\item 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 \code{with_otel_promise_domain()} does this for
OpenTelemetry spans.
\item 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.
\item Promise chain: A set of promise objects to execute over multiple async
ticks.
\item Async tick: the number of times an event loop must run to move computation
forward. (Similar to a JavaScript event loop tick.)
\item \code{then()} promise domain capture: When \code{then()} is called, it will capture
the current promise domain. This promise domain is restored (only if
needed) when evaluating the given \code{onFulfilled} and \code{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
\code{then()} method is called.
}

\code{with_otel_promise_domain()} creates a promise domain that restores the
currently active OpenTelemetry span from when a call to \code{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 \emph{just} the current otel span and reactivate that otel span during promise
domain restoration.
}

\section{When promise domains are captured}{


Asynchronous operation
\itemize{
\item Creates \code{async_op} otel span
\item Automatically ends the otel span (\code{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.

\if{html}{\out{<div class="sourceCode r">}}\preformatted{# 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
  )
}\if{html}{\out{</div>}}

An in-depth explanation of the execution timeline is below.
\itemize{
\item At the first initial tick, \verb{t0.*}:
\itemize{
\item \code{t0.0}: The code is wrapped in \code{with_otel_promise_domain()}
\item \code{t0.1}: The \code{async_op} otel span is created and activated
\item \code{t0.2}: Some async work is initiated
\item \code{t0.3}: \code{then()} is called, capturing the active \code{async_op} otel span (as it
is called within \code{with_otel_promise_domain()})
\item \code{t0.4}: The \code{with_otel_span()} call exits, but the \code{async_op} otel span is
not ended as the promise is still pending. The returned promise has a
\code{finally()} step added to it that will end the otel span \code{async_op} when \code{p}
is resolved.
\item \code{t0.5}: Another \code{then()} is called, but there is no active otel span to
capture
\item \code{t0.6}: The otel span promise domain call exits
\item \code{t0.7}: Another \code{then()} is called. No otel span will be captured as there is
no active otel span / promise domain
}
\item At the first followup tick, \code{t1.0}:
\itemize{
\item The active \code{async_op} otel span is reactivated during promise domain
restoration for the duration of the \code{then} callback
\item The \code{some_async_work} function is called
}
\item At tick, \code{t2.0}:
\itemize{
\item \code{some_async_work} has resolved
\item A hidden \code{finally()} step closes the otel span, \code{async_op}
\item \code{p} is now resolved
}
\item At tick, \code{t3.0}:
\itemize{
\item There is no active otel span at \code{t0.5}, so no otel span is reactivated during
promise domain restoration
\item The \code{more_async_work} function is executed
}
\item At tick, \code{t4.0}:
\itemize{
\item \code{more_async_work} has resolved, therefore \code{p2} is now resolved
\item There was no otel span promise domain at \code{t0.7}, so no attempt is made to
reactivate any otel span
\item The \code{final_work} function is executed
}
\item At tick, \code{t5.0}:
\itemize{
\item \code{p_final} has resolved
}
}
}

\section{Complexity}{


When reactivating the \code{k}th step in a promise chain, the currently active
otel span (during the call to \code{then()}) will be reactivated during promise domain
restoration (\code{O(1)}). To restore a chain of promises, the active otel span will
be restored at each step (\code{O(n)}) due to the \strong{\code{n}} calls to wrapping each
\code{onFulfilled} and \code{onRejected} callbacks inside \code{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 \code{with_active_span()} \code{k} times (\code{O(k)}, where as
handoff domain computes in \code{O(1)}). Taking a step back, to restore each otel span
at for every step in a promise chain would then take \code{O(n^2)} time, not
\code{O(n)}. The standard, naive promise domain approach does not scale for
multiple similar promise domain restorations.
}

\section{Execution model for \code{with_otel_promise_domain()}}{

\enumerate{
\item \code{with_otel_promise_domain(expr)} is called.
\itemize{
\item The following steps all occur within \code{expr}.
}
\item Create an otel span object using \code{otel::start_span()}.
\itemize{
\item We need the otel span to be active during the a followup async operation.
Therefore, \code{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.
}
\item Be sure your otel span is activated before calling \code{promises::then()}.
\itemize{
\item Activate it using \code{with_otel_span(name, expr)} (which also
creates/ends the otel span) or \code{otel::with_active_span(span, expr)}.
}
\item Call \code{promises::then()}
}
\itemize{
\item When \code{promises::then()} is called, the two methods (\code{onFulfilled} and
\code{onRejected}) capture the currently active spans. (Performed by the
initial \code{with_otel_promise_domain()})
}
\enumerate{
\item During reactivation of the promise chain step, the previously captured
otel span is reactivated via \code{with_active_span()}. (Performed by the initial
\code{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 \code{otel::start_span()} and later ended using \code{otel::end_span()}. Ending
the otel span must occur \strong{after} any promise chain work has completed.

If we were to instead use \code{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.

\code{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 \code{on.exit()}) and
asynchronous operations (ending the otel span within \code{promises::finally()}).
}

\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
  })
})
}
}
\seealso{
\code{\link[otel:start_span]{otel::start_span()}}, \code{\link[otel:with_active_span]{otel::with_active_span()}},
\code{\link[otel:end_span]{otel::end_span()}}
}
