From c5e5c3421de485aeeeb5e471c91b630cae443551 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 1 Jul 2025 15:41:22 -0400 Subject: [PATCH 1/8] auto format --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 33793148..067cbad2 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -7,7 +7,7 @@ on: branches: [main, rc-**] pull_request: schedule: - - cron: '0 6 * * 1' # every monday + - cron: "0 6 * * 1" # every monday name: Package checks From 238468100d05dc313f48ffe8044128cf816a1241 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 1 Jul 2025 15:41:39 -0400 Subject: [PATCH 2/8] Add Barret as author. Add winston orcid --- DESCRIPTION | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 703b31ec..8401e97e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,10 +1,11 @@ Type: Package Package: httpuv Title: HTTP and WebSocket Server Library -Version: 1.6.16.9000 +Version: 1.6.16.9001 Authors@R: c( person("Joe", "Cheng", , "joe@posit.co", role = "aut"), - person("Winston", "Chang", , "winston@posit.co", role = c("aut", "cre")), + person("Winston", "Chang", , "winston@posit.co", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-1576-2126")), + person("Barret", "Schloerke", role = "aut", email = "barret@posit.co", comment = c(ORCID = "0000-0001-9986-114X")), person("Posit, PBC", "fnd", role = "cph"), person("Hector", "Corrada Bravo", role = "ctb"), person("Jeroen", "Ooms", role = "ctb"), From 9bddab66c560f1eff4834f29a051f311c5f70967 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 1 Jul 2025 16:04:20 -0400 Subject: [PATCH 3/8] Bump dev version to 1.6.16.9002 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 8401e97e..560c3678 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: httpuv Title: HTTP and WebSocket Server Library -Version: 1.6.16.9001 +Version: 1.6.16.9002 Authors@R: c( person("Joe", "Cheng", , "joe@posit.co", role = "aut"), person("Winston", "Chang", , "winston@posit.co", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-1576-2126")), From 090bf7796cb666617fbf86d4863093fe0cd44b25 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 1 Jul 2025 16:04:32 -0400 Subject: [PATCH 4/8] Add winston/barret orcid info --- DESCRIPTION | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 560c3678..0973cef8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -4,8 +4,10 @@ Title: HTTP and WebSocket Server Library Version: 1.6.16.9002 Authors@R: c( person("Joe", "Cheng", , "joe@posit.co", role = "aut"), - person("Winston", "Chang", , "winston@posit.co", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-1576-2126")), - person("Barret", "Schloerke", role = "aut", email = "barret@posit.co", comment = c(ORCID = "0000-0001-9986-114X")), + person("Winston", "Chang", , "winston@posit.co", role = c("aut", "cre"), + comment = c(ORCID = "0000-0002-1576-2126")), + person("Barret", "Schloerke", , "barret@posit.co", role = "aut", + comment = c(ORCID = "0000-0001-9986-114X")), person("Posit, PBC", "fnd", role = "cph"), person("Hector", "Corrada Bravo", role = "ctb"), person("Jeroen", "Ooms", role = "ctb"), From 390c997b4a4b147bf39f0a43b6364506fdb7710a Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 1 Jul 2025 16:04:47 -0400 Subject: [PATCH 5/8] init otel file --- DESCRIPTION | 4 ++++ R/otel.R | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 R/otel.R diff --git a/DESCRIPTION b/DESCRIPTION index 0973cef8..85ea45b9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -60,6 +60,7 @@ Depends: R (>= 2.15.1) Imports: later (>= 0.8.0), + otel, promises, R6, Rcpp (>= 1.0.7), @@ -73,6 +74,8 @@ Suggests: LinkingTo: later, Rcpp +Remotes: + r-lib/otel Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 @@ -80,6 +83,7 @@ SystemRequirements: GNU make, zlib Collate: 'RcppExports.R' 'httpuv.R' + 'otel.R' 'random_port.R' 'server.R' 'staticServer.R' diff --git a/R/otel.R b/R/otel.R new file mode 100644 index 00000000..2c24b498 --- /dev/null +++ b/R/otel.R @@ -0,0 +1,38 @@ +otel_create_span_promise_domain <- function( + active_span + # , tracer = otel::get_tracer() + # span_ctx = tracer$get_current_span_context() +) { + promises::new_promise_domain( + wrapOnFulfilled = function(onFulfilled) { + # During binding ("then()") + force(onFulfilled) + + function(value) { + # During runtime ("resolve()") + otel::with_active_span(active_span, { + onFulfilled(value) + }) + } + }, + wrapOnRejected = function(onRejected) { + force(onRejected) + + function(reason) { + otel::with_active_span(active_span, { + onRejected(reason) + }) + } + } + ) +} + + +if (FALSE) { + promises::with_promise_domain( + createGraphicsDevicePromiseDomain(device_id), + { + .next(...) + } + ) +} From 80cf7aca124c911353809503c111e714647fb135 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 3 Jul 2025 15:39:29 -0400 Subject: [PATCH 6/8] Commit init exploration --- DESCRIPTION | 7 +- R/httpuv-package.R | 3 + R/httpuv.R | 8 +- R/otel.R | 281 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 290 insertions(+), 9 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index a0c471bb..db59ff2d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -65,18 +65,21 @@ Imports: promises, R6, Rcpp (>= 1.0.7), - utils + utils, + withr Suggests: callr, curl, jsonlite, + otelsdk, testthat (>= 3.0.0), websocket LinkingTo: later, Rcpp Remotes: - r-lib/otel + r-lib/otel, + r-lib/otelsdk Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 Config/usethis/last-upkeep: 2025-07-01 diff --git a/R/httpuv-package.R b/R/httpuv-package.R index 73d2e2c8..6c5d84a8 100644 --- a/R/httpuv-package.R +++ b/R/httpuv-package.R @@ -32,3 +32,6 @@ #' @importFrom R6 R6Class ## usethis namespace: end NULL + + +otel_tracer_name <- "io.github.rstudio/httpuv" diff --git a/R/httpuv.R b/R/httpuv.R index f753e7e5..fcc2634d 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -213,7 +213,6 @@ AppWrapper <- R6Class( if (!private$supportsOnHeaders) { return(NULL) } - rookCall(private$app$onHeaders, req) }, onBodyData = function(req, bytes) { @@ -226,6 +225,13 @@ AppWrapper <- R6Class( # The cpp_callback is an external pointer to a C++ function that writes # the response. + str(req) + + if (otel::is_tracing()) { + call_span <- otel_start_active_call_span(req) + local_active_span_promise_domain(call_span) + } + resp <- if (is.null(private$app$call)) { list( status = 404L, diff --git a/R/otel.R b/R/otel.R index 2c24b498..aab5b494 100644 --- a/R/otel.R +++ b/R/otel.R @@ -1,8 +1,11 @@ -otel_create_span_promise_domain <- function( +# Make promise domain for active span +otel_create_active_span_promise_domain <- function( active_span # , tracer = otel::get_tracer() # span_ctx = tracer$get_current_span_context() ) { + force(active_span) + promises::new_promise_domain( wrapOnFulfilled = function(onFulfilled) { # During binding ("then()") @@ -27,12 +30,278 @@ otel_create_span_promise_domain <- function( ) } +# with_promise_domain <- function(domain, expr, replace = FALSE) { +# oldval <- current_promise_domain() +# if (replace) { +# globals$domain <- domain +# } else { +# globals$domain <- compose_domains(oldval, domain) +# } +# on.exit(globals$domain <- oldval) + +# if (!is.null(domain)) domain$wrapSync(expr) else force(expr) +# } + +# Make a promise domain for a local scope +local_promise_domain <- function( + domain, + ..., + replace = FALSE, + local_envir = parent.frame() +) { + stopifnot(length(list(...)) == 0L) -if (FALSE) { - promises::with_promise_domain( - createGraphicsDevicePromiseDomain(device_id), + oldval <- promises:::current_promise_domain() + if (replace) { + promises:::globals$domain <- domain + } else { + promises:::globals$domain <- promises:::compose_domains( + promises:::globals$domain, + domain + ) + } + withr::defer( { - .next(...) - } + promises:::globals$domain <- oldval + }, + envir = local_envir + ) + + if (!is.null(domain)) { + domain$wrapSync(expr) + promises::with_promise_domain( + createGraphicsDevicePromiseDomain(device_id), + { + .next(...) + } + ) + } +} + +# Set promise domain to local scope with active span +local_active_span_promise_domain <- function( + active_span, + ..., + replace = FALSE, + .envir = parent.frame() +) { + stopifnot(length(list(...)) == 0L) + + act_span_pd <- otel_create_active_span_promise_domain(active_span) + + local_promise_domain( + act_span_pd, + replace = replace, + local_envir = .envir + ) + + invisible() +} + + +otel_start_active_span <- function( + name, + ..., + attributes = list(), + active_frame = parent.frame() +) { + otel::start_span( + name = name, + scope = NULL, + active_frame = active_frame, + attributes = otel::as_attributes(attributes) + ) +} + + +otel_start_active_call_span <- function( + req, + ..., + active_frame = parent.frame() +) { + str(as.list(req)) + + otel_start_active_span( + paste0( + # "httpuv ", + req$METHOD, + " ", + req$PATH + ), + + active_frame = active_frame, + + # https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server + attributes = list( + # HTTP request method. + # Ex: `"GET"`, `"POST"`, `"PUT"`, etc. + 'http.request.method' = req$METHOD, + + # The URI path component + # Ex: `"/users/123"` + 'url.path' = req$PATH, + + # The URI scheme component identifying the used protocol. + # Ex: `"http"`, `"https"` + 'url.scheme' = req$HTTP_VERSION, + + # Describes a class of error the operation ended with. + # https://opentelemetry.io/docs/specs/semconv/registry/attributes/error/ + # 'error.type' = NULL, + + # Original HTTP method sent by the client in the request line + # Ex: `"GET"`, `"POST"`, `"PUT"`, etc. + 'http.request.method_original' = req$METHOD, + + # HTTP response status code + # Ex: `200` + 'http.response.status_code' = NULL, + + # # The matched route, that is, the path template in the format used by the respective server framework. + # # Ex: `"/users/:userID?"` + # ## Maybe something for plumber2? + # 'http.route' = NULL, + + # OSI application layer or non-OSI equivalent. + # Ex: `"http"`, `"spdy"` + 'network.protocol.name' = "http", + + # Port of the local HTTP server that received the request. + # Ex: `80`, `8080`, `443` + 'server.port' = if (is.null(req$SERVER_PORT)) { + NULL + } else { + as.integer(req$SERVER_PORT) + }, + + 'url.scheme' = req$HTTP_SCHEME, + + # The URI query component. + # Ex: `"q=OpenTelemetry"` + 'url.query' = req$QUERY_STRING, + + # Client address + # Ex: `"83.164.160.102"` + # TODO: Implement `client.address` + 'client.address' = req$REMOTE_ADDR, + + # # Peer address of the network connection - IP address or Unix domain socket name. + # # Ex: `"10.1.2.80"`, `"/tmp/my.sock"` + # # TODO: Implement `network.peer.address` + # 'network.peer.address' = req$REMOTE_ADDR, + + # # Peer port number of the network connection. + # # Ex: `65123` + # # TODO: Implement `network.peer.port` + # 'network.peer.port' = if (is.null(req$REMOTE_PORT)) NULL else as.integer(req$REMOTE_PORT), + + # # The actual version of the protocol used for network communication. + # # Ex: `"1.0"`, `"1.1"`, `"2"`, `"3"` + # # TODO: Implement `network.protocol.version` + # 'network.protocol.version' = req$HTTP_VERSION, + + # Name of the local HTTP server that received the request. + # Ex: `"example.com"`, `"10.1.2.80"`, `"/tmp/my.sock"` + 'server.address' = req$SERVER_NAME, + + # Value of the HTTP User-Agent header sent by the client. + # Ex: `"CERN-LineMode/2.15 libwww/2.17b3"`, `"Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"` + 'user_agent.original' = req$HTTP_USER_AGENT, + + # The port of whichever client was captured in client.address. + # Ex: `65123` + # TODO: Implement `client.port` + 'client.port' = if (is.null(req$REMOTE_PORT)) { + NULL + } else { + as.integer(req$REMOTE_PORT) + }, + + # The size of the request payload body in bytes. + # This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. + # For requests using transport encoding, this should be the compressed size. + # TODO: Implement `http.request.body.size` + 'http.request.body.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) { + NULL + } else { + as.integer(req$HTTP_CONTENT_LENGTH) + }, + + # HTTP request headers, being the normalized HTTP Header name (lowercase), the value being the header values. + # Ex: `["application/json"]`, `["1.2.3.4", "1.2.3.5"]` + # TODO: Implement `http.request.header.` + # TODO: Expand the list! rlang::list2()? !!! the result + 'http.request.header' = lapply(req$HEADERS, function(x) { + if (is.null(x)) { + return(NULL) + } + # Are these formats needed? + + if (is.character(x)) { + return(as.character(x)) + } else if (is.raw(x)) { + return(rawToChar(x)) + } else { + return(as.character(x)) + } + }), + + # # The total size of the request in bytes. + # # This should be the total number of bytes sent over the wire, including the request line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and request body if any. + # # Ex: `1437` + # TODO: Implement `http.request.size` + # 'http.request.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) NULL else as.integer(req$HTTP_CONTENT_LENGTH) + nchar(req$PATH) + nchar(req$HTTP_VERSION) + sum(nchar(names(req$HEADERS))) + sum(nchar(unlist(req$HEADERS))), + + # # The size of the response payload body in bytes. + # # This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. + # # For requests using transport encoding, this should be the compressed size. + # # Ex: `3495` + # TODO: Implement `http.response.body.size` + # 'http.response.body.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) NULL else as.integer(req$HTTP_CONTENT_LENGTH), + + # HTTP response headers, being the normalized HTTP Header name (lowercase), the value being the header values. + # Ex: `["application/json"]`, `["abc", "def"]` + # TODO: Implement headers + 'http.response.header' = lapply(res$HEADERS, function(x) { + if (is.null(x)) { + return(NULL) + } + # Are these formats needed? + + if (is.character(x)) { + return(as.character(x)) + } else if (is.raw(x)) { + return(rawToChar(x)) + } else { + return(as.character(x)) + } + }), + + # # The total size of the response in bytes. + # # This should be the total number of bytes sent over the wire, including the status line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and response body and trailers if any. + # # Ex: `1437` + # # TODO: Implement `http.response.size` + # 'http.response.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) NULL else as.integer(req$HTTP_CONTENT_LENGTH) + nchar(req$PATH) + nchar(req$HTTP_VERSION) + sum(nchar(names(req$HEADERS))) + sum(nchar(unlist(req$HEADERS))), + + # # Local socket address. Useful in case of a multi-IP host. + # # Ex: `"10.1.2.80"`, `"/tmp/my.sock"` + # # TODO: Implement `network.local.address` + # 'network.local.address' = req$SERVER_NAME, + + # # Local socket port. Useful in case of a multi-port host. + # # Ex: `65123` + # # TODO: Implement `network.local.port`; get from host? + # 'network.local.port' = if (is.null(req$SERVER_PORT)) NULL else as.integer(req$SERVER_PORT), + + # # OSI transport layer or inter-process communication method. + # # Ex: `"tcp"`, `"udp"` + # # TODO: Implement `network.transport` + # 'network.transport' = if (is.null(req$HTTP_TRANSPORT)) "tcp" else req$HTTP_TRANSPORT, + + # # Specifies the category of synthetic traffic, such as tests or bots. + # # Ex: `bot`, `test` + # # TODO: Implement `user_agent.synthetic.type` + # 'user_agent.synthetic.type' = if (is.null(req$HTTP_USER_AGENT_SYNTHETIC_TYPE)) NULL else req$HTTP_USER_AGENT_SYNTHETIC_TYPE, + ) ) } From f0e217818784a8754a8d4ba48c072d6cf5c03710 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 24 Jul 2025 18:04:07 -0400 Subject: [PATCH 7/8] Use standalone withr::defer --- DESCRIPTION | 4 ++-- R/import-standalone-defer.R | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 R/import-standalone-defer.R diff --git a/DESCRIPTION b/DESCRIPTION index db59ff2d..49c88c51 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -65,8 +65,7 @@ Imports: promises, R6, Rcpp (>= 1.0.7), - utils, - withr + utils Suggests: callr, curl, @@ -91,6 +90,7 @@ Collate: 'RcppExports.R' 'httpuv-package.R' 'httpuv.R' + 'import-standalone-defer.R' 'otel.R' 'random_port.R' 'server.R' diff --git a/R/import-standalone-defer.R b/R/import-standalone-defer.R new file mode 100644 index 00000000..e689e509 --- /dev/null +++ b/R/import-standalone-defer.R @@ -0,0 +1,35 @@ +# Standalone file: do not edit by hand +# Source: https://github.com/r-lib/withr/blob/HEAD/R/standalone-defer.R +# Generated by: usethis::use_standalone("r-lib/withr", "defer") +# ---------------------------------------------------------------------- +# +# --- +# repo: r-lib/withr +# file: standalone-defer.R +# last-updated: 2024-01-15 +# license: https://unlicense.org +# --- +# +# `defer()` is similar to `on.exit()` but with a better default for +# `add` (hardcoded to `TRUE`) and `after` (`FALSE` by default). +# It also supports adding handlers to other frames which is useful +# to implement `local_` functions. +# +# +# ## Changelog +# +# 2024-01-15: +# * Rewritten to be pure base R. +# +# nocov start + +defer <- function(expr, envir = parent.frame(), after = FALSE) { + thunk <- as.call(list(function() expr)) + do.call( + on.exit, + list(thunk, add = TRUE, after = after), + envir = envir + ) +} + +# nocov end From 53f7d8c7cc034dcf9d5afd1f88384d3d0f9dd6d1 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 24 Jul 2025 18:08:47 -0400 Subject: [PATCH 8/8] Update to lastest otel api and propose new promises methods --- R/httpuv.R | 13 +- R/otel.R | 405 +++++++++++++++++++++++++++++------------------------ 2 files changed, 232 insertions(+), 186 deletions(-) diff --git a/R/httpuv.R b/R/httpuv.R index fcc2634d..05f04751 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -225,11 +225,13 @@ AppWrapper <- R6Class( # The cpp_callback is an external pointer to a C++ function that writes # the response. - str(req) + otel_is_tracing <- otel::is_tracing() - if (otel::is_tracing()) { - call_span <- otel_start_active_call_span(req) - local_active_span_promise_domain(call_span) + if (otel_is_tracing) { + otel_active_span_for_call <- otel_start_active_call_span(req) + local_otel_active_span_promise_domain(otel_active_span_for_call) + + # promises:::local_otel_active_span_promise_domain(otel_active_span_for_call) } resp <- if (is.null(private$app$call)) { @@ -250,6 +252,9 @@ AppWrapper <- R6Class( if (!is.null(req$.bodyData)) { close(req$.bodyData) } + if (otel_is_tracing) { + otel_span_for_call$end() + } req$.bodyData <- NULL } diff --git a/R/otel.R b/R/otel.R index aab5b494..4713bfc4 100644 --- a/R/otel.R +++ b/R/otel.R @@ -6,7 +6,9 @@ otel_create_active_span_promise_domain <- function( ) { force(active_span) - promises::new_promise_domain( + promises_env <- rlang::ns_env("promises") + + promises_env$new_promise_domain( wrapOnFulfilled = function(onFulfilled) { # During binding ("then()") force(onFulfilled) @@ -26,10 +28,16 @@ otel_create_active_span_promise_domain <- function( onRejected(reason) }) } - } + }, + wrapSync = base::force, + "_local_promise_domain_compat" = TRUE + # wrapEnter = force, + # wrapExit = force, + # wrapSync = FALSE ) } + # with_promise_domain <- function(domain, expr, replace = FALSE) { # oldval <- current_promise_domain() # if (replace) { @@ -51,35 +59,45 @@ local_promise_domain <- function( ) { stopifnot(length(list(...)) == 0L) - oldval <- promises:::current_promise_domain() + if (!identical(domain[["_local_promise_domain_compat"]], TRUE)) { + if (!identical(domain$wrapSync, base::force)) { + # If the domain's `wrapSync` is not `base::force`, then we assume it is a custom domain + # a custom domain that has been created by the otel package. + # This is to ensure that the domain's `wrapSync` is used correctly. + stop( + "The domain's `wrapSync` must be `base::force` to be used within `local_promise_domain()`." + ) + } + stop( + "local_promise_domain() is only compatible with `otel_create_active_span_promise_domain(active_span)`.", + call. = FALSE + ) + } + + promises_env <- rlang::ns_env("promises") + promises_globals <- promises_env$globals + oldval <- promises_env$current_promise_domain() if (replace) { - promises:::globals$domain <- domain + promises_globals$domain <- domain } else { - promises:::globals$domain <- promises:::compose_domains( - promises:::globals$domain, + new_domain <- promises_env$compose_domains( + promises_globals$domain, domain ) + + promises_globals$domain <- new_domain } - withr::defer( + + defer( { - promises:::globals$domain <- oldval + promises_globals$domain <- oldval }, envir = local_envir ) - - if (!is.null(domain)) { - domain$wrapSync(expr) - promises::with_promise_domain( - createGraphicsDevicePromiseDomain(device_id), - { - .next(...) - } - ) - } } # Set promise domain to local scope with active span -local_active_span_promise_domain <- function( +local_otel_active_span_promise_domain <- function( active_span, ..., replace = FALSE, @@ -98,18 +116,39 @@ local_active_span_promise_domain <- function( invisible() } +with_otel_active_span_promise_domain <- function( + active_span, + expr, + ..., + replace = FALSE, + .envir = parent.frame() +) { + stopifnot(length(list(...)) == 0L) + + act_span_pd <- otel_create_active_span_promise_domain(active_span) + + with_promise_domain( + act_span_pd, + expr, + replace = replace, + local_envir = .envir + ) + + invisible() +} + otel_start_active_span <- function( name, ..., attributes = list(), - active_frame = parent.frame() + activation_scope = parent.frame() ) { - otel::start_span( + otel::start_local_active_span( name = name, - scope = NULL, - active_frame = active_frame, - attributes = otel::as_attributes(attributes) + activation_scope = activation_scope, + attributes = otel::as_attributes(attributes), + end_on_exit = FALSE ) } @@ -117,7 +156,7 @@ otel_start_active_span <- function( otel_start_active_call_span <- function( req, ..., - active_frame = parent.frame() + activation_scope = parent.frame() ) { str(as.list(req)) @@ -129,7 +168,7 @@ otel_start_active_call_span <- function( req$PATH ), - active_frame = active_frame, + activation_scope = activation_scope, # https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server attributes = list( @@ -143,165 +182,167 @@ otel_start_active_call_span <- function( # The URI scheme component identifying the used protocol. # Ex: `"http"`, `"https"` - 'url.scheme' = req$HTTP_VERSION, - - # Describes a class of error the operation ended with. - # https://opentelemetry.io/docs/specs/semconv/registry/attributes/error/ - # 'error.type' = NULL, - - # Original HTTP method sent by the client in the request line - # Ex: `"GET"`, `"POST"`, `"PUT"`, etc. - 'http.request.method_original' = req$METHOD, - - # HTTP response status code - # Ex: `200` - 'http.response.status_code' = NULL, - - # # The matched route, that is, the path template in the format used by the respective server framework. - # # Ex: `"/users/:userID?"` - # ## Maybe something for plumber2? - # 'http.route' = NULL, - - # OSI application layer or non-OSI equivalent. - # Ex: `"http"`, `"spdy"` - 'network.protocol.name' = "http", - - # Port of the local HTTP server that received the request. - # Ex: `80`, `8080`, `443` - 'server.port' = if (is.null(req$SERVER_PORT)) { - NULL - } else { - as.integer(req$SERVER_PORT) - }, - - 'url.scheme' = req$HTTP_SCHEME, - - # The URI query component. - # Ex: `"q=OpenTelemetry"` - 'url.query' = req$QUERY_STRING, - - # Client address - # Ex: `"83.164.160.102"` - # TODO: Implement `client.address` - 'client.address' = req$REMOTE_ADDR, - - # # Peer address of the network connection - IP address or Unix domain socket name. - # # Ex: `"10.1.2.80"`, `"/tmp/my.sock"` - # # TODO: Implement `network.peer.address` - # 'network.peer.address' = req$REMOTE_ADDR, - - # # Peer port number of the network connection. + # 'url.scheme' = req$HTTP_VERSION + 'url.scheme' = "http" + #, + + # # Describes a class of error the operation ended with. + # # https://opentelemetry.io/docs/specs/semconv/registry/attributes/error/ + # # 'error.type' = NULL, + + # # Original HTTP method sent by the client in the request line + # # Ex: `"GET"`, `"POST"`, `"PUT"`, etc. + # 'http.request.method_original' = req$METHOD, + + # # HTTP response status code + # # Ex: `200` + # 'http.response.status_code' = NULL, + + # # # The matched route, that is, the path template in the format used by the respective server framework. + # # # Ex: `"/users/:userID?"` + # # ## Maybe something for plumber2? + # # 'http.route' = NULL, + + # # OSI application layer or non-OSI equivalent. + # # Ex: `"http"`, `"spdy"` + # 'network.protocol.name' = "http", + + # # Port of the local HTTP server that received the request. + # # Ex: `80`, `8080`, `443` + # 'server.port' = if (is.null(req$SERVER_PORT)) { + # NULL + # } else { + # as.integer(req$SERVER_PORT) + # }, + + # 'url.scheme' = req$HTTP_SCHEME, + + # # The URI query component. + # # Ex: `"q=OpenTelemetry"` + # 'url.query' = req$QUERY_STRING, + + # # Client address + # # Ex: `"83.164.160.102"` + # # TODO: Implement `client.address` + # 'client.address' = req$REMOTE_ADDR, + + # # # Peer address of the network connection - IP address or Unix domain socket name. + # # # Ex: `"10.1.2.80"`, `"/tmp/my.sock"` + # # # TODO: Implement `network.peer.address` + # # 'network.peer.address' = req$REMOTE_ADDR, + + # # # Peer port number of the network connection. + # # # Ex: `65123` + # # # TODO: Implement `network.peer.port` + # # 'network.peer.port' = if (is.null(req$REMOTE_PORT)) NULL else as.integer(req$REMOTE_PORT), + + # # # The actual version of the protocol used for network communication. + # # # Ex: `"1.0"`, `"1.1"`, `"2"`, `"3"` + # # # TODO: Implement `network.protocol.version` + # # 'network.protocol.version' = req$HTTP_VERSION, + + # # Name of the local HTTP server that received the request. + # # Ex: `"example.com"`, `"10.1.2.80"`, `"/tmp/my.sock"` + # 'server.address' = req$SERVER_NAME, + + # # Value of the HTTP User-Agent header sent by the client. + # # Ex: `"CERN-LineMode/2.15 libwww/2.17b3"`, `"Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"` + # 'user_agent.original' = req$HTTP_USER_AGENT, + + # # The port of whichever client was captured in client.address. # # Ex: `65123` - # # TODO: Implement `network.peer.port` - # 'network.peer.port' = if (is.null(req$REMOTE_PORT)) NULL else as.integer(req$REMOTE_PORT), - - # # The actual version of the protocol used for network communication. - # # Ex: `"1.0"`, `"1.1"`, `"2"`, `"3"` - # # TODO: Implement `network.protocol.version` - # 'network.protocol.version' = req$HTTP_VERSION, - - # Name of the local HTTP server that received the request. - # Ex: `"example.com"`, `"10.1.2.80"`, `"/tmp/my.sock"` - 'server.address' = req$SERVER_NAME, - - # Value of the HTTP User-Agent header sent by the client. - # Ex: `"CERN-LineMode/2.15 libwww/2.17b3"`, `"Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"` - 'user_agent.original' = req$HTTP_USER_AGENT, - - # The port of whichever client was captured in client.address. - # Ex: `65123` - # TODO: Implement `client.port` - 'client.port' = if (is.null(req$REMOTE_PORT)) { - NULL - } else { - as.integer(req$REMOTE_PORT) - }, - - # The size of the request payload body in bytes. - # This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. - # For requests using transport encoding, this should be the compressed size. - # TODO: Implement `http.request.body.size` - 'http.request.body.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) { - NULL - } else { - as.integer(req$HTTP_CONTENT_LENGTH) - }, - - # HTTP request headers, being the normalized HTTP Header name (lowercase), the value being the header values. - # Ex: `["application/json"]`, `["1.2.3.4", "1.2.3.5"]` - # TODO: Implement `http.request.header.` - # TODO: Expand the list! rlang::list2()? !!! the result - 'http.request.header' = lapply(req$HEADERS, function(x) { - if (is.null(x)) { - return(NULL) - } - # Are these formats needed? - - if (is.character(x)) { - return(as.character(x)) - } else if (is.raw(x)) { - return(rawToChar(x)) - } else { - return(as.character(x)) - } - }), - - # # The total size of the request in bytes. - # # This should be the total number of bytes sent over the wire, including the request line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and request body if any. - # # Ex: `1437` - # TODO: Implement `http.request.size` - # 'http.request.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) NULL else as.integer(req$HTTP_CONTENT_LENGTH) + nchar(req$PATH) + nchar(req$HTTP_VERSION) + sum(nchar(names(req$HEADERS))) + sum(nchar(unlist(req$HEADERS))), - - # # The size of the response payload body in bytes. + # # TODO: Implement `client.port` + # 'client.port' = if (is.null(req$REMOTE_PORT)) { + # NULL + # } else { + # as.integer(req$REMOTE_PORT) + # }, + + # # The size of the request payload body in bytes. # # This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. # # For requests using transport encoding, this should be the compressed size. - # # Ex: `3495` - # TODO: Implement `http.response.body.size` - # 'http.response.body.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) NULL else as.integer(req$HTTP_CONTENT_LENGTH), - - # HTTP response headers, being the normalized HTTP Header name (lowercase), the value being the header values. - # Ex: `["application/json"]`, `["abc", "def"]` - # TODO: Implement headers - 'http.response.header' = lapply(res$HEADERS, function(x) { - if (is.null(x)) { - return(NULL) - } - # Are these formats needed? - - if (is.character(x)) { - return(as.character(x)) - } else if (is.raw(x)) { - return(rawToChar(x)) - } else { - return(as.character(x)) - } - }), - - # # The total size of the response in bytes. - # # This should be the total number of bytes sent over the wire, including the status line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and response body and trailers if any. - # # Ex: `1437` - # # TODO: Implement `http.response.size` - # 'http.response.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) NULL else as.integer(req$HTTP_CONTENT_LENGTH) + nchar(req$PATH) + nchar(req$HTTP_VERSION) + sum(nchar(names(req$HEADERS))) + sum(nchar(unlist(req$HEADERS))), - - # # Local socket address. Useful in case of a multi-IP host. - # # Ex: `"10.1.2.80"`, `"/tmp/my.sock"` - # # TODO: Implement `network.local.address` - # 'network.local.address' = req$SERVER_NAME, - - # # Local socket port. Useful in case of a multi-port host. - # # Ex: `65123` - # # TODO: Implement `network.local.port`; get from host? - # 'network.local.port' = if (is.null(req$SERVER_PORT)) NULL else as.integer(req$SERVER_PORT), - - # # OSI transport layer or inter-process communication method. - # # Ex: `"tcp"`, `"udp"` - # # TODO: Implement `network.transport` - # 'network.transport' = if (is.null(req$HTTP_TRANSPORT)) "tcp" else req$HTTP_TRANSPORT, - - # # Specifies the category of synthetic traffic, such as tests or bots. - # # Ex: `bot`, `test` - # # TODO: Implement `user_agent.synthetic.type` - # 'user_agent.synthetic.type' = if (is.null(req$HTTP_USER_AGENT_SYNTHETIC_TYPE)) NULL else req$HTTP_USER_AGENT_SYNTHETIC_TYPE, + # # TODO: Implement `http.request.body.size` + # 'http.request.body.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) { + # NULL + # } else { + # as.integer(req$HTTP_CONTENT_LENGTH) + # }, + + # # HTTP request headers, being the normalized HTTP Header name (lowercase), the value being the header values. + # # Ex: `["application/json"]`, `["1.2.3.4", "1.2.3.5"]` + # # TODO: Implement `http.request.header.` + # # TODO: Expand the list! rlang::list2()? !!! the result + # 'http.request.header' = lapply(req$HEADERS, function(x) { + # if (is.null(x)) { + # return(NULL) + # } + # # Are these formats needed? + + # if (is.character(x)) { + # return(as.character(x)) + # } else if (is.raw(x)) { + # return(rawToChar(x)) + # } else { + # return(as.character(x)) + # } + # }), + + # # # The total size of the request in bytes. + # # # This should be the total number of bytes sent over the wire, including the request line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and request body if any. + # # # Ex: `1437` + # # TODO: Implement `http.request.size` + # # 'http.request.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) NULL else as.integer(req$HTTP_CONTENT_LENGTH) + nchar(req$PATH) + nchar(req$HTTP_VERSION) + sum(nchar(names(req$HEADERS))) + sum(nchar(unlist(req$HEADERS))), + + # # # The size of the response payload body in bytes. + # # # This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. + # # # For requests using transport encoding, this should be the compressed size. + # # # Ex: `3495` + # # TODO: Implement `http.response.body.size` + # # 'http.response.body.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) NULL else as.integer(req$HTTP_CONTENT_LENGTH), + + # # HTTP response headers, being the normalized HTTP Header name (lowercase), the value being the header values. + # # Ex: `["application/json"]`, `["abc", "def"]` + # # TODO: Implement headers + # 'http.response.header' = lapply(res$HEADERS, function(x) { + # if (is.null(x)) { + # return(NULL) + # } + # # Are these formats needed? + + # if (is.character(x)) { + # return(as.character(x)) + # } else if (is.raw(x)) { + # return(rawToChar(x)) + # } else { + # return(as.character(x)) + # } + # }), + + # # # The total size of the response in bytes. + # # # This should be the total number of bytes sent over the wire, including the status line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and response body and trailers if any. + # # # Ex: `1437` + # # # TODO: Implement `http.response.size` + # # 'http.response.size' = if (is.null(req$HTTP_CONTENT_LENGTH)) NULL else as.integer(req$HTTP_CONTENT_LENGTH) + nchar(req$PATH) + nchar(req$HTTP_VERSION) + sum(nchar(names(req$HEADERS))) + sum(nchar(unlist(req$HEADERS))), + + # # # Local socket address. Useful in case of a multi-IP host. + # # # Ex: `"10.1.2.80"`, `"/tmp/my.sock"` + # # # TODO: Implement `network.local.address` + # # 'network.local.address' = req$SERVER_NAME, + + # # # Local socket port. Useful in case of a multi-port host. + # # # Ex: `65123` + # # # TODO: Implement `network.local.port`; get from host? + # # 'network.local.port' = if (is.null(req$SERVER_PORT)) NULL else as.integer(req$SERVER_PORT), + + # # # OSI transport layer or inter-process communication method. + # # # Ex: `"tcp"`, `"udp"` + # # # TODO: Implement `network.transport` + # # 'network.transport' = if (is.null(req$HTTP_TRANSPORT)) "tcp" else req$HTTP_TRANSPORT, + + # # # Specifies the category of synthetic traffic, such as tests or bots. + # # # Ex: `bot`, `test` + # # # TODO: Implement `user_agent.synthetic.type` + # # 'user_agent.synthetic.type' = if (is.null(req$HTTP_USER_AGENT_SYNTHETIC_TYPE)) NULL else req$HTTP_USER_AGENT_SYNTHETIC_TYPE, ) ) }