‘shinyOAuth’ can emit OpenTelemetry (OTel) logs and traces for key login steps. If you already collect OTel data in your apps, this lets ‘shinyOAuth’ fit into the same observability setup.
The otel package is installed automatically with
‘shinyOAuth’. Install otelsdk as well if you want to use
the SDK helpers and exporters shown in the examples below.
OpenTelemetry is an open standard for telemetry data (logs, traces,
metrics) that many backends can collect. If you do not use it, you can
skip this vignette and rely on the package’s native R hooks for auditing
and tracing instead (see
vignette("audit-logging", package = "shinyOAuth")).
Please refer to the ‘otelsdk’ package to learn more about configuring exporters in R. Once that is set up, ‘shinyOAuth’ will emit OTel signals from that R process.
When oauth_module_server(async = TRUE) runs work in
background workers, ‘shinyOAuth’ automatically replays relevant OTel
environment variables there, including OTEL_* and
OTEL_R_* exporter settings. Exporter or SDK setup performed
from R code is not replayed automatically; rerun that setup in each
worker or recreate workers after changing it.
All signals are emitted under the instrumentation scope
io.github.lukakoning.shinyOAuth. Use this identifier when
configuring collector routing rules or filtering ‘shinyOAuth’ telemetry
in your backend.
This vignette describes the OTel signals emitted by ‘shinyOAuth’, their content, and how to enable/disable them.
OTel log records are generated from the same structured events that
‘shinyOAuth’ emits to its native R hook
(shinyOAuth.audit_hook). The log content and event types
mirror what is described in
vignette("audit-logging", package = "shinyOAuth"), so refer
there for full details about the various events and their content.
The package’s own audit correlation id is exported as the scalar
attribute shinyoauth.trace_id. This is different from
OpenTelemetry’s trace/span ids. When a package operation-level
correlation id is available, spans also carry the same
shinyoauth.trace_id attribute so you can connect the pieces
of one login flow more easily.
When options(shinyOAuth.otel_logging_enabled = FALSE) is
set, ‘shinyOAuth’ stops emitting all OTel logs.
‘shinyOAuth’ also emits OpenTelemetry spans from key operations in the OAuth flows. All spans share these behaviors:
ok; errors
are marked error and include an exception
event with the error class and messagereactive_update spansWhen options(shinyOAuth.otel_tracing_enabled = FALSE) is
set, ‘shinyOAuth’ stops emitting all OTel spans.
shinyOAuth.module.initoauth_module_server() initializes for a
Shiny sessionsession_started audit emissionoauth.provider.name,
oauth.provider.issueroauth.client_id_digestshiny.module_idoauth.phase = "module.init"oauth.auto_redirect,
oauth.refresh_proactively,
oauth.revoke_on_session_end,
oauth.indefinite_sessionoauth.reauth_after_seconds,
oauth.refresh_lead_secondsoauth.browser_cookie_samesite,
oauth.browser_cookie_path_rootshinyOAuth.login.requestprepare_call()oauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.phase = "login.request"oauth.used_pkceoauth.nonce_enabledoauth.scopes.requested,
oauth.scopes.requested_countoauth.claims.requestedoauth.claims.targetsoauth.required_acr_values,
oauth.required_acr_values_countoauth.max_age.requestedoauth.request_object_usedoauth.extra_auth_params_countshinyOAuth.login.parpar_urlrequest_urioauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.phase = "login.par"oauth.client_auth_styleoauth.extra_auth_params_countoauth.extra_token_headers_countshinyOAuth.login.par.httphttp.request.method = "POST"server.addressoauth.phase = "login.par"http.response.status_code,
http.response.content_type after a response is
availablekind = "client")shinyOAuth.callbackhandle_callback()oauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.asyncoauth.phase = "callback"oauth.introspect,
oauth.introspect_elements_countoauth.userinfo.requiredoauth.userinfo.id_token_match_requiredoauth.id_token.validation_enabledhandle_callback() spans also include the
joined oauth.introspect_elements attributeoauth.introspect_elements_countshinyOAuth.login.requestreactive_updateshinyOAuth.form_postresponse_mode = "form_post"
POST callback is validated and bridged into Shinyoauth.provider.name,
oauth.provider.issueroauth.client_id_digestshiny.module_idoauth.phase = "form_post.post"oauth.response_mode = "form_post"shinyoauth.trace_idshinyOAuth.form_post.bridgeoauth.provider.name,
oauth.provider.issueroauth.client_id_digestshiny.module_idoauth.phase = "form_post.callback_lookup"oauth.form_post.handle_digestshinyOAuth.callback.validateoauth.phasecallback.state_payload and
callback.state_store_consume
callback.browser_token_validationcallback.pkce_verifier_validationcallback.nonce_validation
oauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.phase set to the specific validation stageshinyOAuth.callback.workeroauth.provider.name,
oauth.provider.issueroauth.client_id_digestshiny.module_idoauth.async = TRUEoauth.phase = "callback.worker"shinyOAuth.token.exchangeoauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.phase = "token.exchange"oauth.used_pkceoauth.client_auth_styleoauth.dpop.configured, oauth.dpop.bound,
oauth.dpop.token_type_inferredoauth.mtls.client_auth,
oauth.mtls.certificate_bound_tokens,
oauth.mtls.boundoauth.extra_token_params_countoauth.extra_token_headers_countoauth.token_typeoauth.received_id_token,
oauth.received_refresh_tokenoauth.expires_in_present,
oauth.expires_in_synthesizedoauth.scope.present,
oauth.scopes.grantedshinyOAuth.token.exchange.httpoauth.phasetoken.exchangerefreshhttp.request.method = "POST"server.addressoauth.phaseoauth.mtls.endpoint_alias when an RFC 8705 alias URL is
selectedoauth.dpop.nonce_challenge,
oauth.dpop.nonce_retry when a DPoP nonce challenge
occurshttp.response.status_code,
http.response.content_type after a response is
availablekind = "client")shinyOAuth.token.verifyoauth.phasecallback.verifyrefresh.verifyoauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.phaseoauth.dpop.bound,
oauth.dpop.token_type_inferredoauth.mtls.boundoauth.received_id_tokenoauth.received_refresh_tokenoauth.id_token.required,
oauth.id_token.present,
oauth.id_token.validatedoauth.nonce.requiredoauth.scope.validation_modeoauth.scopes.requested,
oauth.scopes.requested_countoauth.scopes.granted,
oauth.scopes.granted_countoauth.required_acr_values,
oauth.required_acr_values_countoauth.refresh_flowshinyOAuth.userinfoget_userinfo() is calledoauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.phase = "userinfo"oauth.dpop.bound,
oauth.dpop.token_type_inferredoauth.mtls.client_certificate,
oauth.mtls.certificate_bound_tokens,
oauth.mtls.boundoauth.userinfo.jwt_requiredoauth.userinfo.jwt_responseoauth.userinfo.subject_presentshinyOAuth.userinfo.httphttp.request.method = "GET"server.addressoauth.phase = "userinfo"oauth.mtls.endpoint_alias when an RFC 8705 alias URL is
selectedoauth.dpop.nonce_challenge,
oauth.dpop.nonce_retry when a DPoP nonce challenge
occurshttp.response.status_code,
http.response.content_type after a response is
availablekind = "client")shinyOAuth.refreshrefresh_token()oauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.asyncoauth.phase = "refresh"oauth.client_auth_styleoauth.dpop.configured, oauth.dpop.bound,
oauth.dpop.token_type_inferredoauth.mtls.client_auth,
oauth.mtls.certificate_bound_tokens,
oauth.mtls.boundoauth.extra_token_params_countoauth.extra_token_headers_countoauth.token_typeoauth.received_id_token,
oauth.received_refresh_tokenoauth.expires_in_present,
oauth.expires_in_synthesizedoauth.scope.present,
oauth.scopes.grantedshinyOAuth.refresh.workeroauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.async = TRUEoauth.phase = "refresh.worker"shinyOAuth.refresh span beneath this bridge spanshinyOAuth.logoutauth$logout() is called from the moduleoauth.provider.name,
oauth.provider.issueroauth.client_id_digestshiny.module_idoauth.phase = "logout"shinyOAuth.session.end.revokerevoke_on_session_end = TRUE and ‘shinyOAuth’ starts
best-effort token revocationrevoke_token() callsoauth.provider.name,
oauth.provider.issueroauth.client_id_digestshiny.module_idoauth.phase = "session.end.revoke"shinyOAuth.token.revokerevoke_token()oauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.asyncoauth.phase = "token.revoke"oauth.token.which ("access" or
"refresh")oauth.client_auth_styleoauth.extra_token_params_countoauth.extra_token_headers_countoauth.supported, oauth.revoked,
oauth.status after completionshinyOAuth.token.revoke.httphttp.request.method = "POST"server.addressoauth.phase = "token.revoke"http.response.status_code,
http.response.content_type after a response is
availablekind = "client")shinyOAuth.token.revoke.workeroauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.async = TRUEoauth.phase = "token.revoke.worker"oauth.token.which ("access" or
"refresh")shinyOAuth.token.revoke span beneath this bridge spanshinyOAuth.token.introspectintrospect_token()oauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.asyncoauth.phase = "token.introspect"oauth.token.which ("access" or
"refresh")oauth.client_auth_styleoauth.extra_token_params_countoauth.extra_token_headers_countoauth.supported, oauth.active,
oauth.status after completionshinyOAuth.token.introspect.httphttp.request.method = "POST"server.addressoauth.phase = "token.introspect"http.response.status_code,
http.response.content_type after a response is
availablekind = "client")shinyOAuth.token.introspect.workeroauth.provider.name,
oauth.provider.issueroauth.client_id_digestoauth.async = TRUEoauth.phase = "token.introspect.worker"oauth.token.which ("access" or
"refresh")shinyOAuth.token.introspect span beneath this bridge
span