-
Notifications
You must be signed in to change notification settings - Fork 78
feat: Add support for OpenTelemetry #551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9d66707
2bbc16c
791e073
d6213f6
631e53b
d1ac9b2
cca88e4
cfc7ec7
fababe1
28c98df
2f62ee7
4288bc1
d0e5f6c
d2996e3
f009219
989afe0
c9296bf
2968b5d
8f2db6b
debca2a
ea5f2be
dffa0f9
beddef5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,8 @@ Suggests: | |
| knitr, | ||
| magrittr, | ||
| nanoarrow (>= 0.3.0.1), | ||
| otel, | ||
| otelsdk, | ||
| RMariaDB, | ||
| rmarkdown, | ||
| rprojroot, | ||
|
|
||
| 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() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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_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 | ||
krlmlr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
| } | ||
|
|
||
| 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 | ||
| }) | ||
| } | ||
krlmlr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # 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) | ||
| } | ||
| 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") | ||
shikokuchuo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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") | ||
| }) | ||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.