Skip to content

Commit fe96a62

Browse files
committed
Document Observability Support
Issue gh-10964
1 parent 2713075 commit fe96a62

File tree

7 files changed

+482
-0
lines changed

7 files changed

+482
-0
lines changed

Diff for: docs/modules/ROOT/nav.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
*** xref:servlet/integrations/websocket.adoc[WebSocket]
9292
*** xref:servlet/integrations/cors.adoc[Spring's CORS Support]
9393
*** xref:servlet/integrations/jsp-taglibs.adoc[JSP Taglib]
94+
*** xref:servlet/integrations/observability.adoc[Observability]
9495
** Configuration
9596
*** xref:servlet/configuration/java.adoc[Java Configuration]
9697
*** xref:servlet/configuration/kotlin.adoc[Kotlin Configuration]
@@ -147,6 +148,7 @@
147148
** Integrations
148149
*** xref:reactive/integrations/cors.adoc[CORS]
149150
*** xref:reactive/integrations/rsocket.adoc[RSocket]
151+
*** xref:reactive/integrations/observability.adoc[Observability]
150152
** xref:reactive/test/index.adoc[Testing]
151153
*** xref:reactive/test/method.adoc[Testing Method Security]
152154
*** xref:reactive/test/web/index.adoc[Testing Web Security]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
[[webflux-observability]]
2+
= Observability
3+
4+
Spring Security integrates with Spring Observability out-of-the-box for tracing; though it's also quite simple to configure for gathering metrics.
5+
6+
[[webflux-observability-tracing]]
7+
== Tracing
8+
9+
When an `ObservationRegistry` bean is present, Spring Security creates traces for:
10+
11+
* the filter chain
12+
* the `ReactiveAuthenticationManager`, and
13+
* the `ReactiveAuthorizationManager`
14+
15+
[[webflux-observability-tracing-boot]]
16+
=== Boot Integration
17+
18+
For example, consider a simple Boot application:
19+
20+
====
21+
.Java
22+
[source,java,role="primary"]
23+
----
24+
@SpringBootApplication
25+
public class MyApplication {
26+
@Bean
27+
public ReactiveUserDetailsService userDetailsService() {
28+
return new MapReactiveUserDetailsManager(
29+
User.withDefaultPasswordEncoder()
30+
.username("user")
31+
.password("password")
32+
.authorities("app")
33+
.build()
34+
);
35+
}
36+
37+
@Bean
38+
ObservationRegistryCustomizer<ObservationRegistry> addTextHandler() {
39+
return (registry) -> registry.observationConfig().observationHandler(new ObservationTextHandler());
40+
}
41+
42+
public static void main(String[] args) {
43+
SpringApplication.run(ListenerSamplesApplication.class, args);
44+
}
45+
}
46+
----
47+
48+
.Kotlin
49+
[source,kotlin,role="secondary"]
50+
----
51+
@SpringBootApplication
52+
class MyApplication {
53+
@Bean
54+
fun userDetailsService(): ReactiveUserDetailsService {
55+
MapReactiveUserDetailsManager(
56+
User.withDefaultPasswordEncoder()
57+
.username("user")
58+
.password("password")
59+
.authorities("app")
60+
.build()
61+
);
62+
}
63+
64+
@Bean
65+
fun addTextHandler(): ObservationRegistryCustomizer<ObservationRegistry> {
66+
return registry: ObservationRegistry -> registry.observationConfig()
67+
.observationHandler(ObservationTextHandler());
68+
}
69+
70+
fun main(args: Array<String>) {
71+
runApplication<MyApplication>(*args)
72+
}
73+
}
74+
----
75+
====
76+
77+
And a corresponding request:
78+
79+
====
80+
[source,bash]
81+
----
82+
?> http -a user:password :8080
83+
----
84+
====
85+
86+
Will produce the following output (indentation added for clarity):
87+
88+
====
89+
[source,bash]
90+
----
91+
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']
92+
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']
93+
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']
94+
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']
95+
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']
96+
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']
97+
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']
98+
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']
99+
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']
100+
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']
101+
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']
102+
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']
103+
----
104+
====
105+
106+
[[webflux-observability-tracing-manual-configuration]]
107+
=== Manual Configuration
108+
109+
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.
110+
111+
====
112+
.Java
113+
[source,java,role="primary"]
114+
----
115+
@SpringBootApplication
116+
public class MyApplication {
117+
@Bean
118+
public ReactiveUserDetailsService userDetailsService() {
119+
return new MapReactiveUserDetailsManager(
120+
User.withDefaultPasswordEncoder()
121+
.username("user")
122+
.password("password")
123+
.authorities("app")
124+
.build()
125+
);
126+
}
127+
128+
@Bean
129+
ObservationRegistry<ObservationRegistry> observationRegistry() {
130+
ObservationRegistry registry = ObservationRegistry.create();
131+
registry.observationConfig().observationHandler(new ObservationTextHandler());
132+
return registry;
133+
}
134+
135+
public static void main(String[] args) {
136+
SpringApplication.run(ListenerSamplesApplication.class, args);
137+
}
138+
}
139+
----
140+
141+
.Kotlin
142+
[source,kotlin,role="secondary"]
143+
----
144+
@SpringBootApplication
145+
class MyApplication {
146+
@Bean
147+
fun userDetailsService(): ReactiveUserDetailsService {
148+
MapReactiveUserDetailsManager(
149+
User.withDefaultPasswordEncoder()
150+
.username("user")
151+
.password("password")
152+
.authorities("app")
153+
.build()
154+
);
155+
}
156+
157+
@Bean
158+
fun observationRegistry(): ObservationRegistry<ObservationRegistry> {
159+
ObservationRegistry registry = ObservationRegistry.create()
160+
registry.observationConfig().observationHandler(ObservationTextHandler())
161+
return registry
162+
}
163+
164+
fun main(args: Array<String>) {
165+
runApplication<MyApplication>(*args)
166+
}
167+
}
168+
----
169+
170+
.Xml
171+
[source,kotlin,role="secondary"]
172+
----
173+
<sec:http auto-config="true" observation-registry-ref="ref">
174+
<sec:intercept-url pattern="/**" access="authenticated"/>
175+
</sec:http>
176+
177+
<!-- define and configure ObservationRegistry bean -->
178+
----
179+
====
180+
181+
[[webflux-observability-tracing-disable]]
182+
=== Disabling Observability
183+
184+
If you don't want any Spring Security observations, in a Spring Boot application you can publish a `ObservationRegistry.NOOP` `@Bean`.
185+
However, this may turn off observations for more than just Spring Security.
186+
187+
Instead, you can alter the provided `ObservationRegistry` with an `ObservationPredicate` like the following:
188+
189+
====
190+
.Java
191+
[source,java,role="primary"]
192+
----
193+
@Bean
194+
ObservationRegistryCustomizer<ObservationRegistry> noSpringSecurityObservations() {
195+
ObservationPredicate predicate = (name, context) -> name.startsWith("spring.security.")
196+
return (registry) -> registry.observationConfig().observationPredicate(predicate)
197+
}
198+
----
199+
200+
.Kotlin
201+
[source,kotlin,role="secondary"]
202+
----
203+
@Bean
204+
fun noSpringSecurityObservations(): ObservationRegistryCustomizer<ObservationRegistry> {
205+
ObservationPredicate predicate = (name: String, context: Observation.Context) -> name.startsWith("spring.security.")
206+
(registry: ObservationRegistry) -> registry.observationConfig().observationPredicate(predicate)
207+
}
208+
----
209+
====
210+
211+
[TIP]
212+
There is no facility for disabling observations with XML support.
213+
Instead, simply do not set the `observation-registry-ref` attribute.
214+
215+
[[webflux-observability-tracing-listing]]
216+
=== Trace Listing
217+
218+
Spring Security tracks the following spans on each request:
219+
220+
1. `spring.security.http.requests` - a span that wraps the entire filter chain, including the request
221+
2. `spring.security.http.chains.before` - a span that wraps the receiving part of the security filters
222+
3. `spring.security.http.chains.after` - a span that wraps the returning part of the security filters
223+
4. `spring.security.http.secured.requests` - a span that wraps the now-secured application request
224+
5. `spring.security.http.unsecured.requests` - a span that wraps requests that Spring Security does not secure
225+
6. `spring.security.authentications` - a span that wraps authentication attempts
226+
7. `spring.security.authorizations` - a span that wraps authorization attempts
227+
228+
[TIP]
229+
`spring.security.http.chains.before` + `spring.security.http.secured.requests` + `spring.security.http.chains.after` = `spring.security.http.requests`
230+
`spring.security.http.chains.before` + `spring.security.http.chains.after` = Spring Security's part of the request

Diff for: docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ This attribute allows you to define an alias name for the internal instance for
2727
If set to true, the AuthenticationManager will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated.
2828
Literally it maps to the `eraseCredentialsAfterAuthentication` property of the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
2929

30+
[[nsa-authentication-manager-observation-registry-ref]]
31+
* **observation-registry-ref**
32+
A reference to the `ObservationRegistry` used for the `FilterChain` and related components
3033

3134
[[nsa-authentication-manager-id]]
3235
* **id**

Diff for: docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ By default an `AffirmativeBased` implementation is used for with a `RoleVoter` a
4747
* **authentication-manager-ref**
4848
A reference to the `AuthenticationManager` used for the `FilterChain` created by this http element.
4949

50+
[[nsa-http-observation-registry-ref]]
51+
* **observation-registry-ref**
52+
A reference to the `ObservationRegistry` used for the `FilterChain` and related components
5053

5154
[[nsa-http-auto-config]]
5255
* **auto-config**

Diff for: docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Defaults to "false".
3737
Specifies a SecurityContextHolderStrategy to use when retrieving the SecurityContext.
3838
Defaults to the value returned by SecurityContextHolder.getContextHolderStrategy().
3939

40+
[[nsa-method-security-observation-registry-ref]]
41+
* **observation-registry-ref**
42+
A reference to the `ObservationRegistry` used for the `FilterChain` and related components
43+
4044
[[nsa-method-security-children]]
4145
=== Child Elements of <method-security>
4246

0 commit comments

Comments
 (0)