From fe96a62dfc6205273aad133ed9aee38eb42e9908 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 4 Oct 2022 15:34:16 -0600 Subject: [PATCH] Document Observability Support Issue gh-10964 --- docs/modules/ROOT/nav.adoc | 2 + .../reactive/integrations/observability.adoc | 230 +++++++++++++++++ .../namespace/authentication-manager.adoc | 3 + .../servlet/appendix/namespace/http.adoc | 3 + .../appendix/namespace/method-security.adoc | 4 + .../servlet/integrations/observability.adoc | 235 ++++++++++++++++++ docs/modules/ROOT/pages/whats-new.adoc | 5 + 7 files changed, 482 insertions(+) create mode 100644 docs/modules/ROOT/pages/reactive/integrations/observability.adoc create mode 100644 docs/modules/ROOT/pages/servlet/integrations/observability.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index d33f092900..7769a742e2 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -91,6 +91,7 @@ *** xref:servlet/integrations/websocket.adoc[WebSocket] *** xref:servlet/integrations/cors.adoc[Spring's CORS Support] *** xref:servlet/integrations/jsp-taglibs.adoc[JSP Taglib] +*** xref:servlet/integrations/observability.adoc[Observability] ** Configuration *** xref:servlet/configuration/java.adoc[Java Configuration] *** xref:servlet/configuration/kotlin.adoc[Kotlin Configuration] @@ -147,6 +148,7 @@ ** Integrations *** xref:reactive/integrations/cors.adoc[CORS] *** xref:reactive/integrations/rsocket.adoc[RSocket] +*** xref:reactive/integrations/observability.adoc[Observability] ** xref:reactive/test/index.adoc[Testing] *** xref:reactive/test/method.adoc[Testing Method Security] *** xref:reactive/test/web/index.adoc[Testing Web Security] diff --git a/docs/modules/ROOT/pages/reactive/integrations/observability.adoc b/docs/modules/ROOT/pages/reactive/integrations/observability.adoc new file mode 100644 index 0000000000..706b15e9cb --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/integrations/observability.adoc @@ -0,0 +1,230 @@ +[[webflux-observability]] += Observability + +Spring Security integrates with Spring Observability out-of-the-box for tracing; though it's also quite simple to configure for gathering metrics. + +[[webflux-observability-tracing]] +== Tracing + +When an `ObservationRegistry` bean is present, Spring Security creates traces for: + +* the filter chain +* the `ReactiveAuthenticationManager`, and +* the `ReactiveAuthorizationManager` + +[[webflux-observability-tracing-boot]] +=== Boot Integration + +For example, consider a simple Boot application: + +==== +.Java +[source,java,role="primary"] +---- +@SpringBootApplication +public class MyApplication { + @Bean + public ReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .authorities("app") + .build() + ); + } + + @Bean + ObservationRegistryCustomizer addTextHandler() { + return (registry) -> registry.observationConfig().observationHandler(new ObservationTextHandler()); + } + + public static void main(String[] args) { + SpringApplication.run(ListenerSamplesApplication.class, args); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@SpringBootApplication +class MyApplication { + @Bean + fun userDetailsService(): ReactiveUserDetailsService { + MapReactiveUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .authorities("app") + .build() + ); + } + + @Bean + fun addTextHandler(): ObservationRegistryCustomizer { + return registry: ObservationRegistry -> registry.observationConfig() + .observationHandler(ObservationTextHandler()); + } + + fun main(args: Array) { + runApplication(*args) + } +} +---- +==== + +And a corresponding request: + +==== +[source,bash] +---- +?> http -a user:password :8080 +---- +==== + +Will produce the following output (indentation added for clarity): + +==== +[source,bash] +---- +START - name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[], highCardinalityKeyValues=[], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@5dfdb78', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.00191856, duration(nanos)=1918560.0, startTimeNanos=101177265022745}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@121549e0'] + START - name='spring.security.http.chains', contextualName='spring.security.http.chains.before', error='null', lowCardinalityKeyValues=[chain.size='14', filter.section='before'], highCardinalityKeyValues=[request.line='/'], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@3932a48c', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=4.65777E-4, duration(nanos)=465777.0, startTimeNanos=101177276300777}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@562db70f'] + STOP - name='spring.security.http.chains', contextualName='spring.security.http.chains.before', error='null', lowCardinalityKeyValues=[chain.size='14', filter.section='before'], highCardinalityKeyValues=[request.line='/'], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@3932a48c', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.003733105, duration(nanos)=3733105.0, startTimeNanos=101177276300777}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@562db70f'] + START - name='spring.security.authentications', contextualName='null', error='null', lowCardinalityKeyValues=[authentication.failure.type='Optional', authentication.method='UserDetailsRepositoryReactiveAuthenticationManager', authentication.request.type='UsernamePasswordAuthenticationToken'], highCardinalityKeyValues=[], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@574ba6cd', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=3.21015E-4, duration(nanos)=321015.0, startTimeNanos=101177336038417}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@49202cc7'] + STOP - name='spring.security.authentications', contextualName='null', error='null', lowCardinalityKeyValues=[authentication.failure.type='Optional', authentication.method='UserDetailsRepositoryReactiveAuthenticationManager', authentication.request.type='UsernamePasswordAuthenticationToken', authentication.result.type='UsernamePasswordAuthenticationToken'], highCardinalityKeyValues=[], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@574ba6cd', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.37574992, duration(nanos)=3.7574992E8, startTimeNanos=101177336038417}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@49202cc7'] + START - name='spring.security.authorizations', contextualName='null', error='null', lowCardinalityKeyValues=[object.type='SecurityContextServerWebExchange'], highCardinalityKeyValues=[], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@6f837332', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=2.65687E-4, duration(nanos)=265687.0, startTimeNanos=101177777941381}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@7f5bc7cb'] + STOP - name='spring.security.authorizations', contextualName='null', error='null', lowCardinalityKeyValues=[authorization.decision='true', object.type='SecurityContextServerWebExchange'], highCardinalityKeyValues=[authentication.authorities='[app]', authorization.decision.details='AuthorizationDecision [granted=true]'], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@6f837332', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.039239047, duration(nanos)=3.9239047E7, startTimeNanos=101177777941381}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@7f5bc7cb'] + START - name='spring.security.http.secured.requests', contextualName='null', error='null', lowCardinalityKeyValues=[], highCardinalityKeyValues=[], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@2f33dfae', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=3.1775E-4, duration(nanos)=317750.0, startTimeNanos=101177821377592}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@63b0d28f'] + STOP - name='spring.security.http.secured.requests', contextualName='null', error='null', lowCardinalityKeyValues=[], highCardinalityKeyValues=[], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@2f33dfae', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.219901971, duration(nanos)=2.19901971E8, startTimeNanos=101177821377592}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@63b0d28f'] + START - name='spring.security.http.chains', contextualName='spring.security.http.chains.after', error='null', lowCardinalityKeyValues=[chain.size='14', filter.section='after'], highCardinalityKeyValues=[request.line='/'], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@40b25623', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=3.25118E-4, duration(nanos)=325118.0, startTimeNanos=101178044824275}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@3b6cec2'] + STOP - name='spring.security.http.chains', contextualName='spring.security.http.chains.after', error='null', lowCardinalityKeyValues=[chain.size='14', filter.section='after'], highCardinalityKeyValues=[request.line='/'], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@40b25623', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.001693146, duration(nanos)=1693146.0, startTimeNanos=101178044824275}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@3b6cec2'] +STOP - name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[], highCardinalityKeyValues=[], map=[class io.micrometer.tracing.handler.TracingObservationHandler$TracingContext='io.micrometer.tracing.handler.TracingObservationHandler$TracingContext@5dfdb78', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.784320641, duration(nanos)=7.84320641E8, startTimeNanos=101177265022745}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@121549e0'] +---- +==== + +[[webflux-observability-tracing-manual-configuration]] +=== Manual Configuration + +For a non-Spring Boot application, or to override the existing Boot configuration, you can publish your own `ObservationRegistry` and Spring Security will still pick it up. + +==== +.Java +[source,java,role="primary"] +---- +@SpringBootApplication +public class MyApplication { + @Bean + public ReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .authorities("app") + .build() + ); + } + + @Bean + ObservationRegistry observationRegistry() { + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(new ObservationTextHandler()); + return registry; + } + + public static void main(String[] args) { + SpringApplication.run(ListenerSamplesApplication.class, args); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@SpringBootApplication +class MyApplication { + @Bean + fun userDetailsService(): ReactiveUserDetailsService { + MapReactiveUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .authorities("app") + .build() + ); + } + + @Bean + fun observationRegistry(): ObservationRegistry { + ObservationRegistry registry = ObservationRegistry.create() + registry.observationConfig().observationHandler(ObservationTextHandler()) + return registry + } + + fun main(args: Array) { + runApplication(*args) + } +} +---- + +.Xml +[source,kotlin,role="secondary"] +---- + + + + + +---- +==== + +[[webflux-observability-tracing-disable]] +=== Disabling Observability + +If you don't want any Spring Security observations, in a Spring Boot application you can publish a `ObservationRegistry.NOOP` `@Bean`. +However, this may turn off observations for more than just Spring Security. + +Instead, you can alter the provided `ObservationRegistry` with an `ObservationPredicate` like the following: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +ObservationRegistryCustomizer noSpringSecurityObservations() { + ObservationPredicate predicate = (name, context) -> name.startsWith("spring.security.") + return (registry) -> registry.observationConfig().observationPredicate(predicate) +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun noSpringSecurityObservations(): ObservationRegistryCustomizer { + ObservationPredicate predicate = (name: String, context: Observation.Context) -> name.startsWith("spring.security.") + (registry: ObservationRegistry) -> registry.observationConfig().observationPredicate(predicate) +} +---- +==== + +[TIP] +There is no facility for disabling observations with XML support. +Instead, simply do not set the `observation-registry-ref` attribute. + +[[webflux-observability-tracing-listing]] +=== Trace Listing + +Spring Security tracks the following spans on each request: + +1. `spring.security.http.requests` - a span that wraps the entire filter chain, including the request +2. `spring.security.http.chains.before` - a span that wraps the receiving part of the security filters +3. `spring.security.http.chains.after` - a span that wraps the returning part of the security filters +4. `spring.security.http.secured.requests` - a span that wraps the now-secured application request +5. `spring.security.http.unsecured.requests` - a span that wraps requests that Spring Security does not secure +6. `spring.security.authentications` - a span that wraps authentication attempts +7. `spring.security.authorizations` - a span that wraps authorization attempts + +[TIP] +`spring.security.http.chains.before` + `spring.security.http.secured.requests` + `spring.security.http.chains.after` = `spring.security.http.requests` +`spring.security.http.chains.before` + `spring.security.http.chains.after` = Spring Security's part of the request diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc index 5452a2b799..3719fe7394 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc @@ -27,6 +27,9 @@ This attribute allows you to define an alias name for the internal instance for If set to true, the AuthenticationManager will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. Literally it maps to the `eraseCredentialsAfterAuthentication` property of the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. +[[nsa-authentication-manager-observation-registry-ref]] +* **observation-registry-ref** +A reference to the `ObservationRegistry` used for the `FilterChain` and related components [[nsa-authentication-manager-id]] * **id** diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 29de571e37..a6ad46b43a 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -47,6 +47,9 @@ By default an `AffirmativeBased` implementation is used for with a `RoleVoter` a * **authentication-manager-ref** A reference to the `AuthenticationManager` used for the `FilterChain` created by this http element. +[[nsa-http-observation-registry-ref]] +* **observation-registry-ref** +A reference to the `ObservationRegistry` used for the `FilterChain` and related components [[nsa-http-auto-config]] * **auto-config** diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc index 4224ce93be..71b8d77150 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc @@ -37,6 +37,10 @@ Defaults to "false". Specifies a SecurityContextHolderStrategy to use when retrieving the SecurityContext. Defaults to the value returned by SecurityContextHolder.getContextHolderStrategy(). +[[nsa-method-security-observation-registry-ref]] +* **observation-registry-ref** +A reference to the `ObservationRegistry` used for the `FilterChain` and related components + [[nsa-method-security-children]] === Child Elements of diff --git a/docs/modules/ROOT/pages/servlet/integrations/observability.adoc b/docs/modules/ROOT/pages/servlet/integrations/observability.adoc new file mode 100644 index 0000000000..63bc157f7d --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/integrations/observability.adoc @@ -0,0 +1,235 @@ +[[observability]] += Observability + +Spring Security integrates with Spring Observability out-of-the-box for tracing; though it's also quite simple to configure for gathering metrics. + +[[observability-tracing]] +== Tracing + +When an `ObservationRegistry` bean is present, Spring Security creates traces for: + +* the filter chain +* the `AuthenticationManager`, and +* the `AuthorizationManager` + +[[observability-tracing-boot]] +=== Boot Integration + +For example, consider a simple Boot application: + +==== +.Java +[source,java,role="primary"] +---- +@SpringBootApplication +public class MyApplication { + @Bean + public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .authorities("app") + .build() + ); + } + + @Bean + ObservationRegistryCustomizer addTextHandler() { + return (registry) -> registry.observationConfig().observationHandler(new ObservationTextHandler()); + } + + public static void main(String[] args) { + SpringApplication.run(ListenerSamplesApplication.class, args); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@SpringBootApplication +class MyApplication { + @Bean + fun userDetailsService(): UserDetailsService { + InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .authorities("app") + .build() + ); + } + + @Bean + fun addTextHandler(): ObservationRegistryCustomizer { + return registry: ObservationRegistry -> registry.observationConfig() + .observationHandler(ObservationTextHandler()); + } + + fun main(args: Array) { + runApplication(*args) + } +} +---- +==== + +And a corresponding request: + +==== +[source,bash] +---- +?> http -a user:password :8080 +---- +==== + +Will produce the following output (indentation added for clarity): + +==== +[source,bash] +---- +START - name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[], highCardinalityKeyValues=[], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@687e16d1', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.001779024, duration(nanos)=1779024.0, startTimeNanos=91695917264958}'] + START - name='spring.security.http.chains', contextualName='spring.security.http.chains.before', error='null', lowCardinalityKeyValues=[chain.position='0', chain.size='17', filter.section='before'], highCardinalityKeyValues=[request.line='GET /'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@79f554a5', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=7.42147E-4, duration(nanos)=742147.0, startTimeNanos=91695947182029}'] + ... skipped for brevity ... + STOP - name='spring.security.http.chains', contextualName='spring.security.http.chains.before', error='null', lowCardinalityKeyValues=[chain.position='0', chain.size='17', filter.section='before'], highCardinalityKeyValues=[request.line='GET /'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@79f554a5', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.014771848, duration(nanos)=1.4771848E7, startTimeNanos=91695947182029}'] + START - name='spring.security.authentications', contextualName='null', error='null', lowCardinalityKeyValues=[authentication.failure.type='Optional', authentication.method='ProviderManager', authentication.request.type='UsernamePasswordAuthenticationToken'], highCardinalityKeyValues=[], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@4d4b2b56', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=7.09759E-4, duration(nanos)=709759.0, startTimeNanos=91696094477504}'] + ... skipped for brevity ... + STOP - name='spring.security.authentications', contextualName='null', error='null', lowCardinalityKeyValues=[authentication.failure.type='Optional', authentication.method='ProviderManager', authentication.request.type='UsernamePasswordAuthenticationToken', authentication.result.type='UsernamePasswordAuthenticationToken'], highCardinalityKeyValues=[], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@4d4b2b56', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.895141386, duration(nanos)=8.95141386E8, startTimeNanos=91696094477504}'] + START - name='spring.security.authorizations', contextualName='null', error='null', lowCardinalityKeyValues=[object.type='Servlet3SecurityContextHolderAwareRequestWrapper'], highCardinalityKeyValues=[], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@6d834cc7', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=3.0965E-4, duration(nanos)=309650.0, startTimeNanos=91697034893983}'] + ... skipped for brevity ... + STOP - name='spring.security.authorizations', contextualName='null', error='null', lowCardinalityKeyValues=[authorization.decision='true', object.type='Servlet3SecurityContextHolderAwareRequestWrapper'], highCardinalityKeyValues=[authentication.authorities='[app]', authorization.decision.details='AuthorizationDecision [granted=true]'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@6d834cc7', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.02084809, duration(nanos)=2.084809E7, startTimeNanos=91697034893983}'] + START - name='spring.security.http.secured.requests', contextualName='null', error='null', lowCardinalityKeyValues=[], highCardinalityKeyValues=[], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@649c5ec3', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=2.67878E-4, duration(nanos)=267878.0, startTimeNanos=91697059819304}'] + ... skipped for brevity ... + STOP - name='spring.security.http.secured.requests', contextualName='null', error='null', lowCardinalityKeyValues=[], highCardinalityKeyValues=[], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@649c5ec3', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.090753322, duration(nanos)=9.0753322E7, startTimeNanos=91697059819304}'] + START - name='spring.security.http.chains', contextualName='spring.security.http.chains.after', error='null', lowCardinalityKeyValues=[chain.position='0', chain.size='17', filter.section='after'], highCardinalityKeyValues=[request.line='GET /'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@47af8207', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=5.31832E-4, duration(nanos)=531832.0, startTimeNanos=91697152857268}'] + ... skipped for brevity ... + STOP - name='spring.security.http.chains', contextualName='spring.security.http.chains.after', error='null', lowCardinalityKeyValues=[chain.position='17', chain.size='17', current.filter.name='DisableEncodeUrlFilter', filter.section='after'], highCardinalityKeyValues=[request.line='GET /'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@47af8207', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.007689382, duration(nanos)=7689382.0, startTimeNanos=91697152857268}'] +STOP - name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[], highCardinalityKeyValues=[request.line='GET /'], map=[class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@687e16d1', class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=1.245858319, duration(nanos)=1.245858319E9, startTimeNanos=91695917264958}'] +---- +==== + +[[observability-tracing-manual-configuration]] +=== Manual Configuration + +For a non-Spring Boot application, or to override the existing Boot configuration, you can publish your own `ObservationRegistry` and Spring Security will still pick it up. + +==== +.Java +[source,java,role="primary"] +---- +@SpringBootApplication +public class MyApplication { + @Bean + public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .authorities("app") + .build() + ); + } + + @Bean + ObservationRegistry observationRegistry() { + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(new ObservationTextHandler()); + return registry; + } + + public static void main(String[] args) { + SpringApplication.run(ListenerSamplesApplication.class, args); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@SpringBootApplication +class MyApplication { + @Bean + fun userDetailsService(): UserDetailsService { + InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .authorities("app") + .build() + ); + } + + @Bean + fun observationRegistry(): ObservationRegistry { + ObservationRegistry registry = ObservationRegistry.create() + registry.observationConfig().observationHandler(ObservationTextHandler()) + return registry + } + + fun main(args: Array) { + runApplication(*args) + } +} +---- + +.Xml +[source,kotlin,role="secondary"] +---- + + + + + +---- +==== + +[[observability-tracing-disable]] +==== Disabling Observability + +If you don't want any Spring Security observations, in a Spring Boot application you can publish a `ObservationRegistry.NOOP` `@Bean`. +However, this may turn off observations for more than just Spring Security. + +Instead, you can alter the provided `ObservationRegistry` with an `ObservationPredicate` like the following: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +ObservationRegistryCustomizer noSpringSecurityObservations() { + ObservationPredicate predicate = (name, context) -> name.startsWith("spring.security.") + return (registry) -> registry.observationConfig().observationPredicate(predicate) +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun noSpringSecurityObservations(): ObservationRegistryCustomizer { + ObservationPredicate predicate = (name: String, context: Observation.Context) -> name.startsWith("spring.security.") + (registry: ObservationRegistry) -> registry.observationConfig().observationPredicate(predicate) +} +---- +==== + +[TIP] +There is no facility for disabling observations with XML support. +Instead, simply do not set the `observation-registry-ref` attribute. + +[[observability-tracing-listing]] +=== Trace Listing + +Spring Security tracks the following spans on each request: + +1. `spring.security.http.requests` - a span that wraps the entire filter chain, including the request +2. `spring.security.http.chains.before` - a span that wraps the receiving part of the security filters +3. `spring.security.http.chains.after` - a span that wraps the returning part of the security filters +4. `spring.security.http.secured.requests` - a span that wraps the now-secured application request +5. `spring.security.http.unsecured.requests` - a span that wraps requests that Spring Security does not secure +6. `spring.security.authentications` - a span that wraps authentication attempts +7. `spring.security.authorizations` - a span that wraps authorization attempts + +[TIP] +`spring.security.http.chains.before` + `spring.security.http.secured.requests` + `spring.security.http.chains.after` = `spring.security.http.requests` +`spring.security.http.chains.before` + `spring.security.http.chains.after` = Spring Security's part of the request diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 95e29aab97..5224da329b 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -29,3 +29,8 @@ Or use `use-authorization-manager="false"` * https://github.com/spring-projects/spring-security/issues/11939[gh-11939] - Remove deprecated `antMatchers`, `mvcMatchers`, `regexMatchers` helper methods from Java Configuration. Instead, use `requestMatchers` or `HttpSecurity#securityMatchers`. * https://github.com/spring-projects/spring-security/issues/11985[gh-11985] - Remove deprecated constructors in `Argon2PasswordEncoder`, `SCryptPasswordEncoder` and `Pbkdf2PasswordEncoder`. + +== Observability + +* xref:servlet/integrations/observability.adoc[Instrumentation] of `AuthenticationManager`, `AuthorizationManager`, and `FilterChainProxy` +* xref:reactive/integrations/observability.adoc[Instrumentation] of `ReactiveAuthenticationManager`, `ReactiveAuthorizationManager`, and `WebFilterChainProxy`