Skip to content

Commit e3cf4fb

Browse files
refactor(otel): Cache tracer and logger on init and on demand (#4315)
Co-authored-by: Charlie Gao <53399081+shikokuchuo@users.noreply.github.com>
1 parent 472a1cd commit e3cf4fb

File tree

6 files changed

+68
-253
lines changed

6 files changed

+68
-253
lines changed

R/otel-shiny.R

Lines changed: 63 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,71 @@
1-
21
# Used by otel to identify the tracer and logger for this package
32
# https://github.com/r-lib/otel/blob/afc31bc1f4bd177870d44b051ada1d9e4e685346/R/tracer-name.R#L33-L49
43
# DO NOT CHANGE THIS VALUE without understanding the implications for existing telemetry data!
54
otel_tracer_name <- "co.posit.r-package.shiny"
65

6+
init_otel <- function() {
7+
.globals$otel_tracer <- otel::get_tracer()
8+
.globals$otel_is_tracing_enabled <- otel::is_tracing_enabled(.globals$otel_tracer)
9+
10+
.globals$otel_logger <- otel::get_logger()
11+
# .globals$otel_is_logging_enabled <- otel::is_logging_enabled()
12+
}
13+
on_load({init_otel()})
14+
15+
#' Run expr within a Shiny OpenTelemetry recording context
16+
#'
17+
#' Reset the OpenTelemetry tracer and logger for Shiny.
18+
#' Used for testing purposes only.
19+
#' @param expr Expression to evaluate within the recording context
20+
#' @return The result of evaluating `otelsdk::with_otel_record(expr)` with freshly enabled Shiny otel tracer and logger
21+
#' @noRd
22+
with_shiny_otel_record <- function(expr) {
23+
# Only use within internal testthat tests
24+
stopifnot(testthat__is_testing())
25+
withr::defer({ init_otel() })
26+
27+
otelsdk::with_otel_record({
28+
init_otel()
29+
30+
force(expr)
31+
})
32+
}
33+
34+
#' Check if OpenTelemetry tracing is enabled
35+
#'
36+
#' @param tracer The OpenTelemetry tracer to check (default: Shiny otel tracer)
37+
#' @return `TRUE` if tracing is enabled, `FALSE` otherwise
38+
#' @noRd
39+
otel_is_tracing_enabled <- function() {
40+
.globals[["otel_is_tracing_enabled"]]
41+
}
42+
43+
#' Shiny OpenTelemetry logger
44+
#'
45+
#' Used for logging OpenTelemetry events via `otel_log()`
46+
#' @return An OpenTelemetry logger
47+
#' @noRd
48+
shiny_otel_logger <- function() {
49+
.globals[["otel_logger"]]
50+
}
51+
52+
53+
54+
#' Shiny OpenTelemetry tracer
55+
#'
56+
#' Used for creating OpenTelemetry spans via `with_otel_span()` and
57+
#' `start_otel_span()`
58+
#'
59+
#' Inspired by httr2:::get_tracer().
60+
#' @return An OpenTelemetry tracer
61+
#' @noRd
62+
shiny_otel_tracer <- function() {
63+
.globals[["otel_tracer"]]
64+
}
65+
66+
67+
68+
769
#' Create and use a Shiny OpenTelemetry span
870
#'
971
#' If otel is disabled, the span will not be created,
@@ -63,80 +125,3 @@ otel_log <- function(
63125
) {
64126
otel::log(msg, ..., severity = severity, logger = logger)
65127
}
66-
67-
#' Check if OpenTelemetry tracing is enabled
68-
#'
69-
#' @param tracer The OpenTelemetry tracer to check (default: Shiny otel tracer)
70-
#' @return `TRUE` if tracing is enabled, `FALSE` otherwise
71-
#' @noRd
72-
otel_is_tracing_enabled <- function(tracer = shiny_otel_tracer()) {
73-
otel::is_tracing_enabled(tracer)
74-
}
75-
76-
#' Shiny OpenTelemetry logger
77-
#'
78-
#' Used for logging OpenTelemetry events via `otel_log()`
79-
#' @return An OpenTelemetry logger
80-
#' @noRd
81-
shiny_otel_logger <- local({
82-
logger <- NULL
83-
84-
# For internal testing purposes only
85-
reset_logger <- function() {
86-
logger <<- NULL
87-
}
88-
89-
function() {
90-
if (!is.null(logger)) {
91-
return(logger)
92-
}
93-
94-
this_logger <- otel::get_logger()
95-
96-
if (testthat__is_testing()) {
97-
# Don't cache the logger in unit tests. It interferes with logger provider
98-
# injection in otelsdk::with_otel_record().
99-
return(this_logger)
100-
}
101-
logger <<- this_logger
102-
logger
103-
}
104-
})
105-
106-
107-
108-
#' Shiny OpenTelemetry tracer
109-
#'
110-
#' Used for creating OpenTelemetry spans via `with_otel_span()` and
111-
#' `start_otel_span()`
112-
#'
113-
#' Inspired by httr2:::get_tracer().
114-
#' @return An OpenTelemetry tracer
115-
#' @noRd
116-
shiny_otel_tracer <- local({
117-
# Using local scope avoids an environment object lookup on each call.
118-
119-
tracer <- NULL
120-
121-
# For internal testing purposes only
122-
reset_tracer <- function() {
123-
tracer <<- NULL
124-
}
125-
126-
function() {
127-
if (!is.null(tracer)) {
128-
return(tracer)
129-
}
130-
131-
this_tracer <- otel::get_tracer()
132-
133-
if (testthat__is_testing()) {
134-
# Don't cache the tracer in unit tests. It interferes with tracer provider
135-
# injection in otelsdk::with_otel_record().
136-
return(this_tracer)
137-
}
138-
139-
tracer <<- this_tracer
140-
tracer
141-
}
142-
})

R/shiny-package.R

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,3 @@ release_bullets <- function() {
4242
"Update static imports: `staticimports::import()`"
4343
)
4444
}
45-
46-
47-
# To get around R CMD check lint
48-
`_ignore` <- function() {
49-
otelsdk::with_otel_record
50-
}

tests/testthat/test-otel-error.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ test_server_with_otel_error <- function(session, server, expr, sanitize = FALSE,
3434
stopifnot(inherits(session, "MockShinySession"))
3535
stopifnot(is.function(server))
3636

37-
traces <- otelsdk::with_otel_record({ 42 })$traces
37+
traces <- with_shiny_otel_record({ 42 })$traces
3838
expect_length(traces, 0)
3939

4040
withr::with_options(
@@ -43,7 +43,7 @@ test_server_with_otel_error <- function(session, server, expr, sanitize = FALSE,
4343
shiny.otel.sanitize.errors = sanitize
4444
),
4545
{
46-
info <- otelsdk::with_otel_record({
46+
info <- with_shiny_otel_record({
4747
# rlang quosure magic to capture and pass through `expr`
4848
testServer(server, {{ expr }}, args = args, session = session)
4949
})

tests/testthat/test-otel-label.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ test_that("reactive bindCache labels are created", {
7070
test_that("ExtendedTask otel labels are created", {
7171
ex_task <- ExtendedTask$new(function() { promises::then(promises::promise_resolve(42), force) })
7272

73-
info <- otelsdk::with_otel_record({
73+
info <- with_shiny_otel_record({
7474
ex_task$invoke()
7575
while(!later::loop_empty()) {
7676
later::run_now()
@@ -88,7 +88,7 @@ test_that("ExtendedTask otel labels are created", {
8888
withReactiveDomain(MockShinySession$new(), {
8989
ex2_task <- ExtendedTask$new(function() { promises::then(promises::promise_resolve(42), force) })
9090

91-
info <- otelsdk::with_otel_record({
91+
info <- with_shiny_otel_record({
9292
ex2_task$invoke()
9393
while(!later::loop_empty()) {
9494
later::run_now()

tests/testthat/test-otel-mock.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ test_server_with_otel <- function(session, server, expr, bind = "all", args = li
4747
stopifnot(is.function(server))
4848

4949
withr::with_options(list(shiny.otel.bind = bind), {
50-
info <- otelsdk::with_otel_record({
50+
info <- with_shiny_otel_record({
5151
# rlang quosure magic to capture and pass through `expr`
5252
testServer(server, {{ expr }}, args = args, session = session)
5353
})

tests/testthat/test-otel-shiny.R

Lines changed: 0 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -140,170 +140,6 @@ test_that("otel_log uses default severity and logger", {
140140
expect_true(log_called)
141141
})
142142

143-
test_that("otel_is_tracing_enabled calls otel::is_tracing_enabled", {
144-
mock_tracer <- create_mock_tracer()
145-
is_tracing_called <- FALSE
146-
147-
local_mocked_bindings(
148-
is_tracing_enabled = function(tracer) {
149-
is_tracing_called <<- TRUE
150-
expect_equal(tracer, mock_tracer)
151-
TRUE
152-
},
153-
.package = "otel"
154-
)
155-
local_mocked_bindings(
156-
shiny_otel_tracer = function() mock_tracer,
157-
)
158-
159-
result <- otel_is_tracing_enabled()
160-
expect_true(is_tracing_called)
161-
expect_true(result)
162-
})
163-
164-
test_that("otel_is_tracing_enabled accepts custom tracer", {
165-
custom_tracer <- create_mock_tracer()
166-
is_tracing_called <- FALSE
167-
168-
local_mocked_bindings(
169-
is_tracing_enabled = function(tracer) {
170-
is_tracing_called <<- TRUE
171-
expect_equal(tracer, custom_tracer)
172-
FALSE
173-
},
174-
.package = "otel"
175-
)
176-
177-
result <- otel_is_tracing_enabled(custom_tracer)
178-
expect_true(is_tracing_called)
179-
expect_false(result)
180-
})
181-
182-
test_that("shiny_otel_logger caches logger in non-test environment", {
183-
mock_logger <- create_mock_logger()
184-
get_logger_call_count <- 0
185-
186-
fn_env <- environment(shiny_otel_logger)
187-
# Reset cached logger now and when test ends
188-
fn_env$reset_logger()
189-
withr::defer({ fn_env$reset_logger() })
190-
191-
local_mocked_bindings(
192-
get_logger = function(...) {
193-
get_logger_call_count <<- get_logger_call_count + 1
194-
mock_logger
195-
},
196-
.package = "otel"
197-
)
198-
199-
with_mocked_bindings(
200-
testthat__is_testing = function() TRUE,
201-
{
202-
# First call
203-
logger1 <- shiny_otel_logger()
204-
expect_equal(logger1, mock_logger)
205-
expect_equal(get_logger_call_count, 1)
206-
207-
# Second call should call otel::get_logger again (no caching in tests)
208-
logger2 <- shiny_otel_logger()
209-
expect_equal(logger2, mock_logger)
210-
expect_equal(get_logger_call_count, 2) # Incremented
211-
}
212-
)
213-
214-
with_mocked_bindings(
215-
testthat__is_testing = function() FALSE,
216-
{
217-
# First call should call otel::get_logger
218-
logger1 <- shiny_otel_logger()
219-
expect_equal(logger1, mock_logger)
220-
expect_equal(get_logger_call_count, 3)
221-
222-
# Second call should use cached logger
223-
logger2 <- shiny_otel_logger()
224-
expect_equal(logger2, mock_logger)
225-
expect_equal(get_logger_call_count, 3) # Still 3, not incremented
226-
}
227-
)
228-
})
229-
230-
231-
test_that("shiny_otel_tracer caches tracer in non-test environment", {
232-
mock_tracer <- create_mock_tracer()
233-
get_tracer_call_count <- 0
234-
235-
fn_env <- environment(shiny_otel_tracer)
236-
# Reset cached tracer now and when test ends
237-
fn_env$reset_tracer()
238-
withr::defer({ fn_env$reset_tracer() })
239-
240-
local_mocked_bindings(
241-
get_tracer = function(...) {
242-
get_tracer_call_count <<- get_tracer_call_count + 1
243-
mock_tracer
244-
},
245-
.package = "otel"
246-
)
247-
248-
with_mocked_bindings(
249-
testthat__is_testing = function() TRUE,
250-
{
251-
# First call
252-
tracer1 <- shiny_otel_tracer()
253-
expect_equal(tracer1, mock_tracer)
254-
expect_equal(get_tracer_call_count, 1)
255-
256-
# Second call should call otel::get_tracer again (no caching in tests)
257-
tracer2 <- shiny_otel_tracer()
258-
expect_equal(tracer2, mock_tracer)
259-
expect_equal(get_tracer_call_count, 2) # Incremented
260-
}
261-
)
262-
263-
with_mocked_bindings(
264-
testthat__is_testing = function() FALSE,
265-
{
266-
# First call should call otel::get_tracer
267-
tracer1 <- shiny_otel_tracer()
268-
expect_equal(tracer1, mock_tracer)
269-
expect_equal(get_tracer_call_count, 3)
270-
271-
# Second call should use cached tracer
272-
tracer2 <- shiny_otel_tracer()
273-
expect_equal(tracer2, mock_tracer)
274-
expect_equal(get_tracer_call_count, 3) # Still 3, not incremented
275-
}
276-
)
277-
})
278-
279-
test_that("integration test - with_otel_span uses cached tracer", {
280-
mock_tracer <- create_mock_tracer()
281-
get_tracer_call_count <- 0
282-
283-
fn_env <- environment(shiny_otel_tracer)
284-
# Reset cached tracer now and when test ends
285-
fn_env$reset_tracer()
286-
withr::defer({ fn_env$reset_tracer() })
287-
288-
local_mocked_bindings(
289-
get_tracer = function() {
290-
get_tracer_call_count <<- get_tracer_call_count + 1
291-
mock_tracer
292-
},
293-
.package = "otel"
294-
)
295-
local_mocked_bindings(
296-
testthat__is_testing = function() FALSE,
297-
)
298-
299-
# First call to with_otel_span
300-
with_otel_span("span1", { "result1" })
301-
expect_equal(get_tracer_call_count, 1)
302-
303-
# Second call should use cached tracer
304-
with_otel_span("span2", { "result2" })
305-
expect_equal(get_tracer_call_count, 1) # Still 1, tracer was cached
306-
})
307143

308144
test_that("integration test - start_otel_span with custom parameters", {
309145
mock_tracer <- create_mock_tracer()

0 commit comments

Comments
 (0)