Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ Suggests:
magick,
litedown,
markdown (>= 1.3),
otel,
otelsdk,
png,
ragg,
reticulate (>= 1.4),
Expand Down Expand Up @@ -180,6 +182,7 @@ Collate:
'hooks-rst.R'
'hooks-textile.R'
'hooks.R'
'otel.R'
'output.R'
'package.R'
'pandoc.R'
Expand Down
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# CHANGES IN knitr VERSION 1.51

## NEW FEATURES

- Added support for [OpenTelemetry](https://opentelemetry.io/) observability. When the **otel** and **otelsdk** packages are installed and tracing is enabled, spans are automatically created for all knit operations. See [`otelsdk`'s Collecting Telemetry Data](https://otelsdk.r-lib.org/reference/collecting.html) for more details on configuring OpenTelemetry (thanks, @shikokuchuo, #2422).
- `knit()` produces 'knitr processing' and 'knitr output' spans when starting and finishing an operation
- `knit()` produces 'knit' spans for each chunk, recording attributes such as the label and knit engine

## BUG FIXES

- Fix issue with error traceback not correctly showing when **rlang** is available.

- Improve error traceback when **rlang** is available and **evaluate** > 1.0.3 is used (thanks, @cderv, #2388).
Expand Down
7 changes: 7 additions & 0 deletions R/block.R
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ cache2.opts = c('fig.keep', 'fig.path', 'fig.ext', 'dev', 'dpi', 'dev.args', 'fi
cache0.opts = c('include', 'out.width.px', 'out.height.px', 'cache.rebuild')

block_exec = function(options) {
otel_active_span(
name = 'knit',
label = options$label,
attributes = make_chunk_attributes(options),
scope = environment()
)

if (options$engine == 'R') return(eng_r(options))

# when code is not R language
Expand Down
73 changes: 73 additions & 0 deletions R/otel.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
otel_tracer_name = 'org.yihui.knitr'
otel_tracer = NULL
otel_is_tracing = FALSE

# generic otel helpers:

# - without specifying `scope`, the span ends when this function returns;
# to make this a local span (last as long as the function it is called from),
# specify `scope = environment()`
# - arguments remain unevaluated on early return
otel_active_span = function(
name,
label,
attributes = list(),
scope = environment()
) {
otel_is_tracing || return()
otel::start_local_active_span(
name = sprintf('%s %s', name, label),
attributes = otel::as_attributes(attributes),
tracer = otel_tracer,
activation_scope = scope
)
}

otel_cache_tracer = function() {
requireNamespace('otel', quietly = TRUE) || return()
otel_tracer <<- otel::get_tracer(otel_tracer_name)
otel_is_tracing <<- tracer_enabled(otel_tracer)
}

tracer_enabled = function(tracer) {
.subset2(tracer, 'is_enabled')()
}

otel_refresh_tracer = function(pkgname) {
requireNamespace('otel', quietly = TRUE) || return()
ns = getNamespace(pkgname)
do.call(unlockBinding, list('otel_is_tracing', ns)) # do.call for R CMD Check
do.call(unlockBinding, list('otel_tracer', ns))
otel_tracer = otel::get_tracer()
ns[['otel_is_tracing']] = tracer_enabled(otel_tracer)
ns[['otel_tracer']] = otel_tracer
lockBinding('otel_is_tracing', ns)
lockBinding('otel_tracer', ns)
}

# knitr-specific helpers:

make_chunk_attributes = function(options) {
list(
knitr.chunk.device = options$dev,
knitr.chunk.echo = options$echo,
knitr.chunk.engine = options$engine,
knitr.chunk.eval = options$eval,
knitr.chunk.label = options$label
)
}

make_knitr_attributes = function() {
list(
knitr.format = out_format(),
knitr.input = get_knitr_concord('infile'),
knitr.output = get_knitr_concord('outfile')
)
}

# safe version that always returns a string
get_knitr_concord = function(name) {
item = knit_concord$get(name)
is.null(item) && return('')
item
}
12 changes: 12 additions & 0 deletions R/output.R
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ knit = function(

if (is.null(out_format())) auto_format(ext)

otel_active_span(
name = 'knitr processing',
label = get_knitr_concord('infile'),
attributes = make_knitr_attributes()
)

params = NULL # the params field from YAML
if (out_format('markdown')) {
if (child_mode()) {
Expand Down Expand Up @@ -264,6 +270,12 @@ knit = function(
if (!quiet) message('output file: ', output, ifelse(progress, '\n', ''))
}

otel_active_span(
name = 'knitr output',
label = get_knitr_concord('outfile'),
attributes = make_knitr_attributes()
)

output %n% res
}
#' @rdname knit
Expand Down
4 changes: 3 additions & 1 deletion R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ has_rlang = FALSE

.onLoad = function(lib, pkg) {
register_vignette_engines(pkg)

default_handlers <<- evaluate::new_output_handler()

otel_cache_tracer()

has_rlang <<- requireNamespace("rlang", quietly = TRUE)

if (has_rlang)
Expand Down
70 changes: 70 additions & 0 deletions tests/testit/test-otel.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
library(testit)

if (requireNamespace('otelsdk', quietly = TRUE)) {

record = otelsdk::with_otel_record({
# refresh tracer within the `with_otel_record()` scope
otel_refresh_tracer('knitr')

knit(
text = c('<<tidy=FALSE, eval=1:2, echo=FALSE, results="asis">>=', '1', '1+', '1', '1', '@'),
quiet = TRUE
)
})

traces = record$traces

assert('otel tracing works on text input', {
(length(traces) %==% 3L)
(startsWith(traces[[1L]]$name, 'knitr processing'))
(traces[[1L]]$attributes$knitr.format %==% 'latex')
(traces[[1L]]$attributes$knitr.input %==% '')
(traces[[1L]]$attributes$knitr.output %==% '')
(traces[[2L]]$name %==% 'knit unnamed-chunk-1')
(traces[[2L]]$attributes$knitr.chunk.device %==% 'pdf')
(traces[[2L]]$attributes$knitr.chunk.echo %==% FALSE)
(traces[[2L]]$attributes$knitr.chunk.engine %==% 'R')
(traces[[2L]]$attributes$knitr.chunk.eval %==% c(1, 2))
(traces[[2L]]$attributes$knitr.chunk.label %==% 'unnamed-chunk-1')
(startsWith(traces[[3L]]$name, 'knitr output'))
(traces[[3L]]$attributes$knitr.format %==% 'latex')
(traces[[3L]]$attributes$knitr.input %==% '')
(traces[[3L]]$attributes$knitr.output %==% '')
})

record = otelsdk::with_otel_record({
otel_refresh_tracer('knitr')

local({
env = new.env()
env$y = 1:3
z = 5
on.exit(file.remove('knit-envir.md'))
knit('knit-envir.Rmd', envir = env, quiet = TRUE)
})
})

traces = record$traces

assert('otel tracing works when knitting files', {
(length(traces) %==% 3L)
(traces[[1L]]$name %==% 'knitr processing knit-envir.Rmd')
(traces[[1L]]$attributes$knitr.format %==% 'markdown')
(traces[[1L]]$attributes$knitr.input %==% 'knit-envir.Rmd')
(traces[[1L]]$attributes$knitr.output %==% 'knit-envir.md')
(traces[[2L]]$name %==% 'knit test')
(traces[[2L]]$attributes$knitr.chunk.device %==% 'png')
(traces[[2L]]$attributes$knitr.chunk.echo %==% TRUE)
(traces[[2L]]$attributes$knitr.chunk.engine %==% 'R')
(traces[[2L]]$attributes$knitr.chunk.eval %==% TRUE)
(traces[[2L]]$attributes$knitr.chunk.label %==% 'test')
(traces[[3L]]$name %==% 'knitr output knit-envir.md')
(traces[[3L]]$attributes$knitr.format %==% 'markdown')
(traces[[3L]]$attributes$knitr.input %==% 'knit-envir.Rmd')
(traces[[3L]]$attributes$knitr.output %==% 'knit-envir.md')
})

# reset tracer after tests
otel_refresh_tracer('knitr')

}
Loading