Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9d66707
OpenTelemetry concept
shikokuchuo Oct 27, 2025
2bbc16c
Add table creation, removal and queries
shikokuchuo Oct 27, 2025
791e073
Adhere closer to semantic conventions
shikokuchuo Oct 27, 2025
d6213f6
Move otel to suggests and cache tracer on package load
shikokuchuo Oct 28, 2025
631e53b
Refactor `otel_local_active_span()`
shikokuchuo Oct 30, 2025
d1ac9b2
Add testing infrastructure
shikokuchuo Oct 30, 2025
cca88e4
Add dbWriteTable, dbAppendTable, dbReadTable; handle names more robustly
shikokuchuo Oct 31, 2025
cfc7ec7
Rename some parameters
shikokuchuo Oct 31, 2025
fababe1
Merge branch 'main' into otel
krlmlr Oct 31, 2025
28c98df
Use updated `otel_refresh_tracer()`
shikokuchuo Nov 3, 2025
2f62ee7
Do not record query text
shikokuchuo Nov 3, 2025
4288bc1
Use `INSERT INTO` for `dbAppendTable()`
shikokuchuo Nov 3, 2025
d0e5f6c
Implement `collection_name()` helper
shikokuchuo Nov 3, 2025
d2996e3
Use safe subsetting
shikokuchuo Nov 3, 2025
f009219
Simplify `modify_binding()` helper
shikokuchuo Nov 3, 2025
989afe0
Simplify `otel_refresh_tracer()`
shikokuchuo Nov 6, 2025
c9296bf
Use local scope to cache tracer; add testing helper
shikokuchuo Nov 8, 2025
2968b5d
Simplify `otel_cache_tracer()`
shikokuchuo Nov 8, 2025
8f2db6b
Drop `otel::as_attributes()`
shikokuchuo Nov 8, 2025
debca2a
Merge branch 'main' into otel
shikokuchuo Nov 10, 2025
ea5f2be
More precise arg name
krlmlr Dec 5, 2025
dffa0f9
Update tests/testthat/test-otel.R
krlmlr Dec 5, 2025
beddef5
Add `otel_query_local_active_span()`
shikokuchuo Jan 23, 2026
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
2 changes: 2 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Suggests:
knitr,
magrittr,
nanoarrow (>= 0.3.0.1),
otel,
otelsdk,
RMariaDB,
rmarkdown,
rprojroot,
Expand Down
9 changes: 9 additions & 0 deletions R/11-dbAppendTable.R
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
setGeneric(
"dbAppendTable",
def = function(conn, name, value, ..., row.names = NULL) {
otel_local_active_span(
"INSERT INTO",
conn,
label = collection_name(name, conn),
attributes = list(
db.collection.name = collection_name(name, conn),
db.operation.name = "INSERT INTO"
)
)
standardGeneric("dbAppendTable")
}
)
9 changes: 9 additions & 0 deletions R/12-dbCreateTable.R
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
setGeneric(
"dbCreateTable",
def = function(conn, name, fields, ..., row.names = NULL, temporary = FALSE) {
otel_local_active_span(
"CREATE TABLE",
conn,
label = collection_name(name, conn),
attributes = list(
db.collection.name = collection_name(name, conn),
db.operation.name = "CREATE TABLE"
)
)
standardGeneric("dbCreateTable")
}
)
6 changes: 6 additions & 0 deletions R/13-dbWriteTable.R
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,11 @@
#' dbWriteTable(con, "mtcars", mtcars[1:10, ], overwrite = TRUE, row.names = FALSE)
#' dbReadTable(con, "mtcars")
setGeneric("dbWriteTable", def = function(conn, name, value, ...) {
otel_local_active_span(
"dbWriteTable",
conn,
label = collection_name(name, conn),
attributes = list(db.collection.name = collection_name(name, conn))
)
standardGeneric("dbWriteTable")
})
9 changes: 9 additions & 0 deletions R/21-dbAppendTableArrow.R
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,14 @@
#' dbReadTable(con, "iris")
#' dbDisconnect(con)
setGeneric("dbAppendTableArrow", def = function(conn, name, value, ...) {
otel_local_active_span(
"INSERT INTO",
conn,
label = collection_name(name, conn),
attributes = list(
db.collection.name = collection_name(name, conn),
db.operation.name = "INSERT INTO"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we distinguish between Arrow and data frame source?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think distinguishing Arrow will be useful? I'm thinking the database operations would be the same, so I'd default to not doing anything, but we could add an attribute here to all the Arrow variants if you prefer.

)
)
standardGeneric("dbAppendTableArrow")
})
9 changes: 9 additions & 0 deletions R/22-dbCreateTableArrow.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
setGeneric(
"dbCreateTableArrow",
def = function(conn, name, value, ..., temporary = FALSE) {
otel_local_active_span(
"CREATE TABLE",
conn,
label = collection_name(name, conn),
attributes = list(
db.collection.name = collection_name(name, conn),
db.operation.name = "CREATE TABLE"
)
)
standardGeneric("dbCreateTableArrow")
}
)
6 changes: 6 additions & 0 deletions R/23-dbWriteTableArrow.R
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,11 @@
#'
#' dbDisconnect(con)
setGeneric("dbWriteTableArrow", def = function(conn, name, value, ...) {
otel_local_active_span(
"dbWriteTableArrow",
conn,
label = collection_name(name, conn),
attributes = list(db.collection.name = collection_name(name, conn))
)
standardGeneric("dbWriteTableArrow")
})
4 changes: 4 additions & 0 deletions R/DBI-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ require_arrow <- function() {
}
stop("The nanoarrow package is required for this functionality.")
}

.onLoad <- function(libname, pkgname) {
otel_cache_tracer()
}
5 changes: 4 additions & 1 deletion R/dbConnect.R
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
#' dbListTables(con <- dbConnect(RSQLite::SQLite(), ":memory:"))
setGeneric(
"dbConnect",
def = function(drv, ...) standardGeneric("dbConnect"),
def = function(drv, ...) {
otel_local_active_span("dbConnect", drv)
standardGeneric("dbConnect")
},
valueClass = "DBIConnection"
)
1 change: 1 addition & 0 deletions R/dbDisconnect.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
#' con <- dbConnect(RSQLite::SQLite(), ":memory:")
#' dbDisconnect(con)
setGeneric("dbDisconnect", def = function(conn, ...) {
otel_local_active_span("dbDisconnect", conn)
standardGeneric("dbDisconnect")
})
1 change: 1 addition & 0 deletions R/dbGetQuery.R
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@
#'
#' dbDisconnect(con)
setGeneric("dbGetQuery", def = function(conn, statement, ...) {
otel_query_local_active_span(conn, statement)
standardGeneric("dbGetQuery")
})
1 change: 1 addition & 0 deletions R/dbGetQueryArrow.R
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@
#'
#' dbDisconnect(con)
setGeneric("dbGetQueryArrow", def = function(conn, statement, ...) {
otel_query_local_active_span(conn, statement)
standardGeneric("dbGetQueryArrow")
})
10 changes: 9 additions & 1 deletion R/dbReadTable.R
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
#' dbDisconnect(con)
setGeneric(
"dbReadTable",
def = function(conn, name, ...) standardGeneric("dbReadTable"),
def = function(conn, name, ...) {
otel_local_active_span(
"dbReadTable",
conn,
label = collection_name(name, conn),
attributes = list(db.collection.name = collection_name(name, conn))
)
standardGeneric("dbReadTable")
},
valueClass = "data.frame"
)
6 changes: 6 additions & 0 deletions R/dbReadTableArrow.R
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,11 @@
#' dbDisconnect(con)
setGeneric("dbReadTableArrow", def = function(conn, name, ...) {
require_arrow()
otel_local_active_span(
"dbReadTableArrow",
conn,
label = collection_name(name, conn),
attributes = list(db.collection.name = collection_name(name, conn))
)
standardGeneric("dbReadTableArrow")
})
9 changes: 9 additions & 0 deletions R/dbRemoveTable.R
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,14 @@
#'
#' dbDisconnect(con)
setGeneric("dbRemoveTable", def = function(conn, name, ...) {
otel_local_active_span(
"DROP TABLE",
conn,
label = collection_name(name, conn),
attributes = list(
db.collection.name = collection_name(name, conn),
db.operation.name = "DROP TABLE"
)
)
standardGeneric("dbRemoveTable")
})
83 changes: 83 additions & 0 deletions R/otel.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
otel_tracer_name <- "org.r-dbi.DBI"

# Generic otel helpers:

otel_cache_tracer <- NULL
otel_local_active_span <- NULL
otel_query_local_active_span <- NULL

local({
otel_tracer <- NULL
otel_is_tracing <- FALSE

otel_cache_tracer <<- function() {
requireNamespace("otel", quietly = TRUE) || return()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if otel is installed during the session? Can we somehow support this use case?

Will otel print diagnostics on the console if it's active, by default?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If otel is installed mid-session, the user would need to restart R or call DBI:::otel_cache_tracer() directly for it to take effect. I don't think it's worth adding a public function here as we don't expect this to be a common use case. A typical workflow would be to have otel installed before starting a session. We've taken this approach for pretty much all the R packages that have been instrumented to date.

Regarding console output - by default, otel doesn't print diagnostics to the console. It only exports spans to a configured collector/backend. Users would still need to explicitly configure this via the OTEL_TRACES_EXPORTER env var, but stdout or stderr are options.

otel_tracer <<- otel::get_tracer(otel_tracer_name)
otel_is_tracing <<- tracer_enabled(otel_tracer)
}

otel_local_active_span <<- function(
name,
conn,
label = NULL,
attributes = NULL,
activation_scope = parent.frame()
) {
otel_is_tracing || return()
dbname <- get_dbname(conn)
otel::start_local_active_span(
name = sprintf("%s %s", name, if (length(label)) label else dbname),
attributes = c(attributes, list(db.system.name = dbname)),
options = list(kind = "client"),
tracer = otel_tracer,
activation_scope = activation_scope
)
}

otel_query_local_active_span <<- function(
conn,
statement,
activation_scope = parent.frame()
) {
otel_is_tracing || return()
dbname <- get_dbname(conn)
tokens <- strsplit(statement, " ", fixed = TRUE)[[1L]]
op_name <- tokens[1L]
from_idx <- match("FROM", toupper(tokens))
collection <- if (!is.na(from_idx)) tokens[from_idx + 1L] else character()
otel::start_local_active_span(
name = paste(op_name, if (length(collection)) collection else dbname),
attributes = list(
db.operation.name = op_name,
db.collection.name = collection,
db.system.name = dbname
),
options = list(kind = "client"),
tracer = otel_tracer,
activation_scope = activation_scope
)
}
})

tracer_enabled <- function(tracer) {
.subset2(tracer, "is_enabled")()
}

with_otel_record <- function(expr) {
on.exit(otel_cache_tracer())
otelsdk::with_otel_record({
otel_cache_tracer()
expr
})
}

# DBI-specific helpers:

get_dbname <- function(obj) {
dbname <- attr(class(obj), "package")
if (is.null(dbname)) "unknown" else dbname
}

collection_name <- function(name, conn) {
if (is.character(name)) name else dbQuoteIdentifier(conn, x = name)
}
1 change: 1 addition & 0 deletions R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# https://github.com/r-lib/pkgload/issues/247
.onLoad <- function(libname, pkgname) {
otel_cache_tracer()
if (
"RSQLite" %in%
loadedNamespaces() &&
Expand Down
44 changes: 44 additions & 0 deletions tests/testthat/test-otel.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
test_that("OpenTelemetry tracing works", {
skip_if_not_installed("otelsdk")

record <- with_otel_record({
con <- dbConnect(RSQLite::SQLite(), ":memory:")
dbWriteTable(con, "mtcars", mtcars)
dbGetQuery(con, "SELECT * FROM mtcars")
dbGetQuery(
con,
"SELECT COUNT(*) FROM mtcars WHERE cyl = ?",
params = list(1:8)
)
dbReadTable(con, "mtcars")
dbRemoveTable(con, "mtcars")
dbDisconnect(con)
})

traces <- record$traces

expect_length(traces, 10L)
expect_equal(traces[[1L]]$name, "dbConnect RSQLite")
expect_equal(traces[[1L]]$kind, "client")
expect_equal(traces[[1L]]$attributes$db.system.name, "RSQLite")
expect_equal(traces[[2L]]$name, "CREATE TABLE mtcars")
expect_equal(traces[[2L]]$attributes$db.collection.name, "mtcars")
expect_equal(traces[[2L]]$attributes$db.operation.name, "CREATE TABLE")
expect_equal(traces[[3L]]$name, "INSERT INTO mtcars")
expect_equal(traces[[3L]]$attributes$db.collection.name, "mtcars")
expect_equal(traces[[3L]]$attributes$db.operation.name, "INSERT INTO")
expect_equal(traces[[4L]]$name, "dbWriteTable mtcars")
expect_equal(traces[[4L]]$attributes$db.collection.name, "mtcars")
expect_equal(traces[[5L]]$name, "SELECT mtcars")
expect_equal(traces[[5L]]$attributes$db.collection.name, "mtcars")
expect_equal(traces[[5L]]$attributes$db.operation.name, "SELECT")
expect_equal(traces[[6L]]$name, "SELECT mtcars")
expect_equal(traces[[7L]]$name, "SELECT `mtcars`")
expect_equal(traces[[8L]]$name, "dbReadTable mtcars")
expect_equal(traces[[8L]]$attributes$db.collection.name, "mtcars")
expect_equal(traces[[9L]]$name, "DROP TABLE mtcars")
expect_equal(traces[[9L]]$attributes$db.collection.name, "mtcars")
expect_equal(traces[[9L]]$attributes$db.operation.name, "DROP TABLE")
expect_equal(traces[[10L]]$name, "dbDisconnect RSQLite")
expect_equal(traces[[10L]]$attributes$db.system.name, "RSQLite")
})
Loading