shinyOAuth exports diagnostic logs and traces through OpenTelemetry
(OTel), using the otel R package. Logs record events such
as a failed token exchange. Traces group related operations into spans,
which record their duration and outcome. This allows authentication
diagnostics to be collected alongside other application telemetry.
An exporter sends the records to a console, file, or monitoring
service. The otel package is a dependency of shinyOAuth;
install otelsdk for its exporters. For R callbacks that
receive events directly, see Audit logging
and hooks.
Install otelsdk, then set these variables in a fresh R
session before loading shinyOAuth or starting the app:
# install.packages("otelsdk")
Sys.setenv(
OTEL_TRACES_EXPORTER = "console",
OTEL_LOGS_EXPORTER = "console",
OTEL_LOG_LEVEL = "debug"
)
library(shinyOAuth)Authenticate with the example Shiny app. The console exporter prints the emitted records. To send them to a monitoring service instead, follow the otelsdk exporter setup and your service’s endpoint and credential instructions.
If nothing appears, check that an exporter is configured before the package first creates a logger or tracer, and that the logging and tracing options below are enabled. Restart R after changing exporter configuration.
Filter by instrumentation scope
io.github.lukakoning.shinyOAuth in your monitoring system.
Start with shinyOAuth.login.request and
shinyOAuth.callback, then inspect token exchange or
userinfo spans to see where time was spent. The package’s
shinyoauth.trace_id attribute also connects related logs
and spans; it is separate from OTel’s own trace/span IDs.
The logs come from the same events as the audit hook. See the audit event catalog for their meaning. The span catalog below lists names and attributes for detailed lookup.
Both options default to TRUE; an exporter is still
needed to collect the data. Disable either signal without changing your
app’s other telemetry:
For async work managed by shinyOAuth, exporter environment settings
(OTEL_* and OTEL_R_*), logging and tracing
options, and trace context are propagated to workers. SDK setup
performed through R code is not replayed automatically; run that setup
in each worker or recreate workers after changing it.
ok; failures have
status error. Revocation and introspection classify their
normalized results on both sync and async spans: HTTP/protocol failures
carry error.type, while missing tokens and unsupported
endpoints leave status unset. An inactive-token response is successful
introspection (oauth.active = FALSE). Thrown errors also
produce an exception event containing the error class.
Condition messages are omitted by default and are included only with
options(shinyOAuth.expose_error_body = TRUE); those
messages may contain provider details and should be handled as sensitive
data.shinyOAuth.telemetry_path_scrubber returning approved route
templates. Opt-in detail has URL userinfo, query strings, fragments, and
control characters removed and is limited to 512 UTF-8 bytes per
field.Use this as a reference for a span you see in your tracing system. Attributes are included when relevant and available; their presence can differ between main-process and worker 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"
or response_mode = "form_post.jwt" 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" for plain form POST
callbacksoauth.response_mode = "form_post.jwt" for JARM POST
callbacksshinyoauth.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