Move Actuator docs into a separate module
This commit is contained in:
committed by
Phillip Webb
parent
37b2cdd5bd
commit
48770cfd16
@@ -0,0 +1,7 @@
|
||||
name: boot
|
||||
version: true
|
||||
ext:
|
||||
zip_contents_collector:
|
||||
include:
|
||||
- name: actuator-rest-api
|
||||
classifier: aggregate-content
|
||||
@@ -0,0 +1 @@
|
||||
include::api:partial$nav-actuator-rest-api.adoc[]
|
||||
@@ -0,0 +1,40 @@
|
||||
[[audit-events]]
|
||||
= Audit Events (`auditevents`)
|
||||
|
||||
The `auditevents` endpoint provides information about the application's audit events.
|
||||
|
||||
|
||||
|
||||
[[audit-events.retrieving]]
|
||||
== Retrieving Audit Events
|
||||
|
||||
To retrieve the audit events, make a `GET` request to `/actuator/auditevents`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/auditevents/filtered/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves `logout` events for the principal, `alice`, that occurred after 09:37 on 7 November 2017 in the UTC timezone.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/auditevents/filtered/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[audit-events.retrieving.query-parameters]]
|
||||
=== Query Parameters
|
||||
|
||||
The endpoint uses query parameters to limit the events that it returns.
|
||||
The following table shows the supported query parameters:
|
||||
|
||||
[cols="2,4"]
|
||||
include::partial$rest/actuator/auditevents/filtered/query-parameters.adoc[]
|
||||
|
||||
|
||||
|
||||
[[audit-events.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of all of the audit events that matched the query.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/auditevents/all/response-fields.adoc[]
|
||||
@@ -0,0 +1,28 @@
|
||||
[[beans]]
|
||||
= Beans (`beans`)
|
||||
|
||||
The `beans` endpoint provides information about the application's beans.
|
||||
|
||||
|
||||
|
||||
[[beans.retrieving]]
|
||||
== Retrieving the Beans
|
||||
|
||||
To retrieve the beans, make a `GET` request to `/actuator/beans`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/beans/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/beans/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[beans.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's beans.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/beans/response-fields.adoc[]
|
||||
@@ -0,0 +1,97 @@
|
||||
[[caches]]
|
||||
= Caches (`caches`)
|
||||
|
||||
The `caches` endpoint provides access to the application's caches.
|
||||
|
||||
|
||||
|
||||
[[caches.all]]
|
||||
== Retrieving All Caches
|
||||
|
||||
To retrieve the application's caches, make a `GET` request to `/actuator/caches`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/caches/all/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/caches/all/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[caches.all.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's caches.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/caches/all/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[caches.named]]
|
||||
== Retrieving Caches by Name
|
||||
|
||||
To retrieve a cache by name, make a `GET` request to `/actuator/caches/\{name}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/caches/named/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves information about the cache named `cities`.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/caches/named/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[caches.named.query-parameters]]
|
||||
=== Query Parameters
|
||||
|
||||
If the requested name is specific enough to identify a single cache, no extra parameter is required.
|
||||
Otherwise, the `cacheManager` must be specified.
|
||||
The following table shows the supported query parameters:
|
||||
|
||||
[cols="2,4"]
|
||||
include::partial$rest/actuator/caches/named/query-parameters.adoc[]
|
||||
|
||||
|
||||
|
||||
[[caches.named.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the requested cache.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/caches/named/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[caches.evict-all]]
|
||||
== Evict All Caches
|
||||
|
||||
To clear all available caches, make a `DELETE` request to `/actuator/caches` as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/caches/evict-all/curl-request.adoc[]
|
||||
|
||||
|
||||
|
||||
[[caches.evict-named]]
|
||||
== Evict a Cache by Name
|
||||
|
||||
To evict a particular cache, make a `DELETE` request to `/actuator/caches/\{name}` as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/caches/evict-named/curl-request.adoc[]
|
||||
|
||||
NOTE: As there are two caches named `countries`, the `cacheManager` has to be provided to specify which `Cache` should be cleared.
|
||||
|
||||
|
||||
|
||||
[[caches.evict-named.request-structure]]
|
||||
=== Request Structure
|
||||
|
||||
If the requested name is specific enough to identify a single cache, no extra parameter is required.
|
||||
Otherwise, the `cacheManager` must be specified.
|
||||
The following table shows the supported query parameters:
|
||||
|
||||
[cols="2,4"]
|
||||
include::partial$rest/actuator/caches/evict-named/query-parameters.adoc[]
|
||||
@@ -0,0 +1,28 @@
|
||||
[[conditions]]
|
||||
= Conditions Evaluation Report (`conditions`)
|
||||
|
||||
The `conditions` endpoint provides information about the evaluation of conditions on configuration and auto-configuration classes.
|
||||
|
||||
|
||||
|
||||
[[conditions.retrieving]]
|
||||
== Retrieving the Report
|
||||
|
||||
To retrieve the report, make a `GET` request to `/actuator/conditions`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/conditions/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/conditions/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[conditions.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's condition evaluation.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/conditions/response-fields.adoc[]
|
||||
@@ -0,0 +1,54 @@
|
||||
[[configprops]]
|
||||
= Configuration Properties (`configprops`)
|
||||
|
||||
The `configprops` endpoint provides information about the application's `@ConfigurationProperties` beans.
|
||||
|
||||
|
||||
|
||||
[[configprops.retrieving]]
|
||||
== Retrieving All @ConfigurationProperties Beans
|
||||
|
||||
To retrieve all of the `@ConfigurationProperties` beans, make a `GET` request to `/actuator/configprops`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/configprops/all/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/configprops/all/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[configprops.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's `@ConfigurationProperties` beans.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/configprops/all/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[configprops.retrieving-by-prefix]]
|
||||
== Retrieving @ConfigurationProperties Beans By Prefix
|
||||
|
||||
To retrieve the `@ConfigurationProperties` beans mapped under a certain prefix, make a `GET` request to `/actuator/configprops/\{prefix}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/configprops/prefixed/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/configprops/prefixed/http-response.adoc[]
|
||||
|
||||
NOTE: The `\{prefix}` does not need to be exact, a more general prefix will return all beans mapped under that prefix stem.
|
||||
|
||||
|
||||
|
||||
[[configprops.retrieving-by-prefix.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's `@ConfigurationProperties` beans.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/configprops/prefixed/response-fields.adoc[]
|
||||
@@ -0,0 +1,57 @@
|
||||
[[env]]
|
||||
= Environment (`env`)
|
||||
|
||||
The `env` endpoint provides information about the application's `Environment`.
|
||||
|
||||
|
||||
|
||||
[[env.entire]]
|
||||
== Retrieving the Entire Environment
|
||||
|
||||
To retrieve the entire environment, make a `GET` request to `/actuator/env`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/env/all/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/env/all/http-response.adoc[]
|
||||
|
||||
NOTE: Sanitization of sensitive values has been switched off for this example.
|
||||
|
||||
|
||||
|
||||
[[env.entire.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's `Environment`.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/env/all/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[env.single-property]]
|
||||
== Retrieving a Single Property
|
||||
|
||||
To retrieve a single property, make a `GET` request to `/actuator/env/{property.name}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/env/single/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves information about the property named `com.example.cache.max-size`.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/env/single/http-response.adoc[]
|
||||
|
||||
NOTE: Sanitization of sensitive values has been switched off for this example.
|
||||
|
||||
|
||||
|
||||
[[env.single-property.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the requested property.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/env/single/response-fields.adoc[]
|
||||
@@ -0,0 +1,28 @@
|
||||
[[flyway]]
|
||||
= Flyway (`flyway`)
|
||||
|
||||
The `flyway` endpoint provides information about database migrations performed by Flyway.
|
||||
|
||||
|
||||
|
||||
[[flyway.retrieving]]
|
||||
== Retrieving the Migrations
|
||||
|
||||
To retrieve the migrations, make a `GET` request to `/actuator/flyway`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/flyway/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/flyway/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[flyway.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's Flyway migrations.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/flyway/response-fields.adoc[]
|
||||
@@ -0,0 +1,82 @@
|
||||
[[health]]
|
||||
= Health (`health`)
|
||||
|
||||
The `health` endpoint provides detailed information about the health of the application.
|
||||
|
||||
|
||||
|
||||
[[health.retrieving]]
|
||||
== Retrieving the Health of the Application
|
||||
|
||||
To retrieve the health of the application, make a `GET` request to `/actuator/health`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/health/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/health/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[health.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the health of the application.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/health/response-fields.adoc[]
|
||||
|
||||
NOTE: The response fields above are for the V3 API.
|
||||
If you need to return V2 JSON you should use an accept header or `application/vnd.spring-boot.actuator.v2+json`
|
||||
|
||||
|
||||
|
||||
[[health.retrieving-component]]
|
||||
== Retrieving the Health of a Component
|
||||
|
||||
To retrieve the health of a particular component of the application's health, make a `GET` request to `/actuator/health/\{component}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/health/component/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/health/component/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[health.retrieving-component.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the health of a particular component of the application's health.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/health/component/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[health.retrieving-component-nested]]
|
||||
== Retrieving the Health of a Nested Component
|
||||
|
||||
If a particular component contains other nested components (as the `broker` indicator in the example above), the health of such a nested component can be retrieved by issuing a `GET` request to `/actuator/health/\{component}/\{subcomponent}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/health/instance/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/health/instance/http-response.adoc[]
|
||||
|
||||
Components of an application's health may be nested arbitrarily deep depending on the application's health indicators and how they have been grouped.
|
||||
The health endpoint supports any number of `/\{component}` identifiers in the URL to allow the health of a component at any depth to be retrieved.
|
||||
|
||||
|
||||
|
||||
[[health.retrieving-component-nested.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the health of an instance of a particular component of the application.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/health/instance/response-fields.adoc[]
|
||||
@@ -0,0 +1,21 @@
|
||||
[[heapdump]]
|
||||
= Heap Dump (`heapdump`)
|
||||
|
||||
The `heapdump` endpoint provides a heap dump from the application's JVM.
|
||||
|
||||
|
||||
|
||||
[[heapdump.retrieving]]
|
||||
== Retrieving the Heap Dump
|
||||
|
||||
To retrieve the heap dump, make a `GET` request to `/actuator/heapdump`.
|
||||
The response is binary data and can be large.
|
||||
Its format depends upon the JVM on which the application is running.
|
||||
When running on a HotSpot JVM the format is https://docs.oracle.com/javase/8/docs/technotes/samples/hprof.html[HPROF]
|
||||
and on OpenJ9 it is https://www.eclipse.org/openj9/docs/dump_heapdump/#portable-heap-dump-phd-format[PHD].
|
||||
Typically, you should save the response to disk for subsequent analysis.
|
||||
When using curl, this can be achieved by using the `-O` option, as shown in the following example:
|
||||
|
||||
include::partial$rest/actuator/heapdump/curl-request.adoc[]
|
||||
|
||||
The preceding example results in a file named `heapdump` being written to the current working directory.
|
||||
@@ -0,0 +1,28 @@
|
||||
[[httpexchanges]]
|
||||
= HTTP Exchanges (`httpexchanges`)
|
||||
|
||||
The `httpexchanges` endpoint provides information about HTTP request-response exchanges.
|
||||
|
||||
|
||||
|
||||
[[httpexchanges.retrieving]]
|
||||
== Retrieving the HTTP Exchanges
|
||||
|
||||
To retrieve the HTTP exchanges, make a `GET` request to `/actuator/httpexchanges`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/httpexchanges/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/httpexchanges/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[httpexchanges.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the traced HTTP request-response exchanges.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/httpexchanges/response-fields.adoc[]
|
||||
@@ -0,0 +1,41 @@
|
||||
:navtitle: Actuator
|
||||
[[overview]]
|
||||
= Actuator REST API
|
||||
|
||||
This API documentation describes Spring Boot Actuators web endpoints.
|
||||
|
||||
Before you proceed, you should read the following topics:
|
||||
|
||||
* xref:#overview.endpoint-urls[]
|
||||
* xref:#overview.timestamps[]
|
||||
|
||||
NOTE: In order to get the correct JSON responses documented below, Jackson must be available.
|
||||
|
||||
|
||||
|
||||
[[overview.endpoint-urls]]
|
||||
== URLs
|
||||
|
||||
By default, all web endpoints are available beneath the path `/actuator` with URLs of
|
||||
the form `/actuator/\{id}`. The `/actuator` base path can be configured by using the
|
||||
`management.endpoints.web.base-path` property, as shown in the following example:
|
||||
|
||||
[source,properties]
|
||||
----
|
||||
management.endpoints.web.base-path=/manage
|
||||
----
|
||||
|
||||
The preceding `application.properties` example changes the form of the endpoint URLs from
|
||||
`/actuator/\{id}` to `/manage/\{id}`. For example, the URL `info` endpoint would become
|
||||
`/manage/info`.
|
||||
|
||||
|
||||
|
||||
[[overview.timestamps]]
|
||||
== Timestamps
|
||||
|
||||
All timestamps that are consumed by the endpoints, either as query parameters or in the
|
||||
request body, must be formatted as an offset date and time as specified in
|
||||
https://en.wikipedia.org/wiki/ISO_8601[ISO 8601].
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
[[info]]
|
||||
= Info (`info`)
|
||||
|
||||
The `info` endpoint provides general information about the application.
|
||||
|
||||
|
||||
|
||||
[[info.retrieving]]
|
||||
== Retrieving the Info
|
||||
|
||||
To retrieve the information about the application, make a `GET` request to `/actuator/info`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/info/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/info/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[info.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains general information about the application.
|
||||
Each section of the response is contributed by an `InfoContributor`.
|
||||
Spring Boot provides several contributors that are described below.
|
||||
|
||||
|
||||
|
||||
[[info.retrieving.response-structure.build]]
|
||||
==== Build Response Structure
|
||||
|
||||
The following table describe the structure of the `build` section of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/info/response-fields-beneath-build.adoc[]
|
||||
|
||||
|
||||
|
||||
[[info.retrieving.response-structure.git]]
|
||||
==== Git Response Structure
|
||||
|
||||
The following table describes the structure of the `git` section of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/info/response-fields-beneath-git.adoc[]
|
||||
|
||||
NOTE: This is the "simple" output.
|
||||
The contributor can also be configured to output all available data.
|
||||
|
||||
|
||||
[[info.retrieving.response-structure.os]]
|
||||
==== OS Response Structure
|
||||
|
||||
The following table describes the structure of the `os` section of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/info/response-fields-beneath-os.adoc[]
|
||||
|
||||
|
||||
|
||||
[[info.retrieving.response-structure.process]]
|
||||
==== Process Response Structure
|
||||
|
||||
The following table describes the structure of the `process` section of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/info/response-fields-beneath-process.adoc[]
|
||||
|
||||
|
||||
|
||||
[[info.retrieving.response-structure.java]]
|
||||
==== Java Response Structure
|
||||
|
||||
The following table describes the structure of the `java` section of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/info/response-fields-beneath-java.adoc[]
|
||||
|
||||
|
||||
|
||||
[[info.retrieving.response-structure.ssl]]
|
||||
==== SSL Response Structure
|
||||
|
||||
The following table describes the structure of the `ssl` section of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/info/response-fields-beneath-ssl.adoc[]
|
||||
@@ -0,0 +1,38 @@
|
||||
[[integrationgraph]]
|
||||
= Spring Integration Graph (`integrationgraph`)
|
||||
|
||||
The `integrationgraph` endpoint exposes a graph containing all Spring Integration components.
|
||||
|
||||
|
||||
|
||||
[[integrationgraph.retrieving]]
|
||||
== Retrieving the Spring Integration Graph
|
||||
|
||||
To retrieve the information about the application, make a `GET` request to `/actuator/integrationgraph`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/integrationgraph/graph/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/integrationgraph/graph/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[integrationgraph.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains all Spring Integration components used within the application, as well as the links between them.
|
||||
More information about the structure can be found in the {url-spring-integration-docs}/graph.html[reference documentation].
|
||||
|
||||
|
||||
|
||||
[[integrationgraph.rebuilding]]
|
||||
== Rebuilding the Spring Integration Graph
|
||||
|
||||
To rebuild the exposed graph, make a `POST` request to `/actuator/integrationgraph`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/integrationgraph/rebuild/curl-request.adoc[]
|
||||
|
||||
This will result in a `204 - No Content` response:
|
||||
|
||||
include::partial$rest/actuator/integrationgraph/rebuild/http-response.adoc[]
|
||||
@@ -0,0 +1,28 @@
|
||||
[[liquibase]]
|
||||
= Liquibase (`liquibase`)
|
||||
|
||||
The `liquibase` endpoint provides information about database change sets applied by Liquibase.
|
||||
|
||||
|
||||
|
||||
[[liquibase.retrieving]]
|
||||
== Retrieving the Changes
|
||||
|
||||
To retrieve the changes, make a `GET` request to `/actuator/liquibase`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/liquibase/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/liquibase/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[liquibase.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's Liquibase change sets.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/liquibase/response-fields.adoc[]
|
||||
@@ -0,0 +1,33 @@
|
||||
[[logfile]]
|
||||
= Log File (`logfile`)
|
||||
|
||||
The `logfile` endpoint provides access to the contents of the application's log file.
|
||||
|
||||
|
||||
|
||||
[[logfile.retrieving]]
|
||||
== Retrieving the Log File
|
||||
|
||||
To retrieve the log file, make a `GET` request to `/actuator/logfile`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/logfile/entire/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/logfile/entire/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[logfile.retrieving-part]]
|
||||
== Retrieving Part of the Log File
|
||||
|
||||
NOTE: Retrieving part of the log file is not supported when using Jersey.
|
||||
|
||||
To retrieve part of the log file, make a `GET` request to `/actuator/logfile` by using the `Range` header, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/logfile/range/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves the first 1024 bytes of the log file.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/logfile/range/http-response.adoc[]
|
||||
@@ -0,0 +1,134 @@
|
||||
[[loggers]]
|
||||
= Loggers (`loggers`)
|
||||
|
||||
The `loggers` endpoint provides access to the application's loggers and the configuration of their levels.
|
||||
|
||||
|
||||
|
||||
[[loggers.all]]
|
||||
== Retrieving All Loggers
|
||||
|
||||
To retrieve the application's loggers, make a `GET` request to `/actuator/loggers`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/loggers/all/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/loggers/all/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[loggers.all.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's loggers.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/loggers/all/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[loggers.single]]
|
||||
== Retrieving a Single Logger
|
||||
|
||||
To retrieve a single logger, make a `GET` request to `/actuator/loggers/{logger.name}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/loggers/single/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves information about the logger named `com.example`.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/loggers/single/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[loggers.single.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the requested logger.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/loggers/single/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[loggers.group]]
|
||||
== Retrieving a Single Group
|
||||
|
||||
To retrieve a single group, make a `GET` request to `/actuator/loggers/{group.name}`,
|
||||
as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/loggers/group/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves information about the logger group named `test`.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/loggers/group/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[loggers.group.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the requested group.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/loggers/group/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[loggers.setting-level]]
|
||||
== Setting a Log Level
|
||||
|
||||
To set the level of a logger, make a `POST` request to `/actuator/loggers/{logger.name}` with a JSON body that specifies the configured level for the logger, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/loggers/set/curl-request.adoc[]
|
||||
|
||||
The preceding example sets the `configuredLevel` of the `com.example` logger to `DEBUG`.
|
||||
|
||||
|
||||
|
||||
[[loggers.setting-level.request-structure]]
|
||||
=== Request Structure
|
||||
|
||||
The request specifies the desired level of the logger.
|
||||
The following table describes the structure of the request:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/loggers/set/request-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[loggers.group-setting-level]]
|
||||
== Setting a Log Level for a Group
|
||||
|
||||
To set the level of a logger, make a `POST` request to `/actuator/loggers/{group.name}` with a JSON body that specifies the configured level for the logger group, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/loggers/setGroup/curl-request.adoc[]
|
||||
|
||||
The preceding example sets the `configuredLevel` of the `test` logger group to `DEBUG`.
|
||||
|
||||
|
||||
|
||||
[[loggers.group-setting-level.request-structure]]
|
||||
=== Request Structure
|
||||
|
||||
The request specifies the desired level of the logger group.
|
||||
The following table describes the structure of the request:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/loggers/set/request-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[loggers.clearing-level]]
|
||||
== Clearing a Log Level
|
||||
|
||||
To clear the level of a logger, make a `POST` request to `/actuator/loggers/{logger.name}` with a JSON body containing an empty object, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/loggers/clear/curl-request.adoc[]
|
||||
|
||||
The preceding example clears the configured level of the `com.example` logger.
|
||||
@@ -0,0 +1,75 @@
|
||||
[[mappings]]
|
||||
= Mappings (`mappings`)
|
||||
|
||||
The `mappings` endpoint provides information about the application's request mappings.
|
||||
|
||||
|
||||
|
||||
[[mappings.retrieving]]
|
||||
== Retrieving the Mappings
|
||||
|
||||
To retrieve the mappings, make a `GET` request to `/actuator/mappings`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/mappings/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/mappings/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[mappings.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's mappings.
|
||||
The items found in the response depend on the type of web application (reactive or Servlet-based).
|
||||
The following table describes the structure of the common elements of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/mappings/response-fields.adoc[]
|
||||
|
||||
The entries that may be found in `contexts.*.mappings` are described in the following sections.
|
||||
|
||||
|
||||
|
||||
[[mappings.retrieving.response-structure-dispatcher-servlets]]
|
||||
=== Dispatcher Servlets Response Structure
|
||||
|
||||
When using Spring MVC, the response contains details of any `DispatcherServlet` request mappings beneath `contexts.*.mappings.dispatcherServlets`.
|
||||
The following table describes the structure of this section of the response:
|
||||
|
||||
[cols="4,1,2"]
|
||||
include::partial$rest/actuator/mappings/response-fields-dispatcher-servlets.adoc[]
|
||||
|
||||
|
||||
|
||||
[[mappings.retrieving.response-structure-servlets]]
|
||||
=== Servlets Response Structure
|
||||
|
||||
When using the Servlet stack, the response contains details of any `Servlet` mappings beneath `contexts.*.mappings.servlets`.
|
||||
The following table describes the structure of this section of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/mappings/response-fields-servlets.adoc[]
|
||||
|
||||
|
||||
|
||||
[[mappings.retrieving.response-structure-servlet-filters]]
|
||||
=== Servlet Filters Response Structure
|
||||
|
||||
When using the Servlet stack, the response contains details of any `Filter` mappings beneath `contexts.*.mappings.servletFilters`.
|
||||
The following table describes the structure of this section of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/mappings/response-fields-servlet-filters.adoc[]
|
||||
|
||||
|
||||
|
||||
[[mappings.retrieving.response-structure-dispatcher-handlers]]
|
||||
=== Dispatcher Handlers Response Structure
|
||||
|
||||
When using Spring WebFlux, the response contains details of any `DispatcherHandler` request mappings beneath `contexts.*.mappings.dispatcherHandlers`.
|
||||
The following table describes the structure of this section of the response:
|
||||
|
||||
[cols="4,1,2"]
|
||||
include::partial$rest/actuator/mappings/response-fields-dispatcher-handlers.adoc[]
|
||||
@@ -0,0 +1,81 @@
|
||||
[[metrics]]
|
||||
= Metrics (`metrics`)
|
||||
|
||||
The `metrics` endpoint provides access to application metrics to diagnose the metrics the application has recorded.
|
||||
This endpoint should not be "scraped" or used as a metrics backend in production.
|
||||
Its purpose is to show the currently registered metrics so users can see what metrics are available, what their current values are, and if triggering certain operations causes any change in certain values.
|
||||
If you want to diagnose your applications through the metrics they collect, you should use an xref:reference:actuator/metrics.adoc[external metrics backend].
|
||||
In this case, the `metrics` endpoint can still be useful.
|
||||
|
||||
|
||||
|
||||
[[metrics.retrieving-names]]
|
||||
== Retrieving Metric Names
|
||||
|
||||
To retrieve the names of the available metrics, make a `GET` request to `/actuator/metrics`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/metrics/names/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/metrics/names/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[metrics.retrieving-names.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the metric names.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,2"]
|
||||
include::partial$rest/actuator/metrics/names/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[metrics.retrieving-metric]]
|
||||
== Retrieving a Metric
|
||||
|
||||
To retrieve a metric, make a `GET` request to `/actuator/metrics/{metric.name}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/metrics/metric/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves information about the metric named `jvm.memory.max`.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/metrics/metric/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[metrics.retrieving-metric.query-parameters]]
|
||||
=== Query Parameters
|
||||
|
||||
The endpoint uses query parameters to xref:rest/actuator/metrics.adoc#metrics.drilling-down[drill down] into a metric by using its tags.
|
||||
The following table shows the single supported query parameter:
|
||||
|
||||
[cols="2,4"]
|
||||
include::partial$rest/actuator/metrics/metric-with-tags/query-parameters.adoc[]
|
||||
|
||||
|
||||
|
||||
[[metrics.retrieving-metric.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the metric.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
include::partial$rest/actuator/metrics/metric/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[metrics.drilling-down]]
|
||||
== Drilling Down
|
||||
|
||||
To drill down into a metric, make a `GET` request to `/actuator/metrics/{metric.name}` using the `tag` query parameter, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/metrics/metric-with-tags/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves the `jvm.memory.max` metric, where the `area` tag has a value of `nonheap` and the `id` attribute has a value of `Compressed Class Space`.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/metrics/metric-with-tags/http-response.adoc[]
|
||||
@@ -0,0 +1,51 @@
|
||||
[[prometheus]]
|
||||
= Prometheus (`prometheus`)
|
||||
|
||||
The `prometheus` endpoint provides Spring Boot application's metrics in the format required for scraping by a Prometheus server.
|
||||
|
||||
|
||||
|
||||
[[prometheus.retrieving]]
|
||||
== Retrieving All Metrics
|
||||
|
||||
To retrieve all metrics, make a `GET` request to `/actuator/prometheus`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/prometheus/all/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/prometheus/all/http-response.adoc[]
|
||||
|
||||
The default response content type is `text/plain;version=0.0.4`.
|
||||
The endpoint can also produce `application/openmetrics-text;version=1.0.0` when called with an appropriate `Accept` header, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/prometheus/openmetrics/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/prometheus/openmetrics/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[prometheus.retrieving.query-parameters]]
|
||||
=== Query Parameters
|
||||
|
||||
The endpoint uses query parameters to limit the samples that it returns.
|
||||
The following table shows the supported query parameters:
|
||||
|
||||
[cols="2,4"]
|
||||
include::partial$rest/actuator/prometheus/names/query-parameters.adoc[]
|
||||
|
||||
|
||||
|
||||
[[prometheus.retrieving-names]]
|
||||
== Retrieving Filtered Metrics
|
||||
|
||||
To retrieve metrics matching specific names, make a `GET` request to `/actuator/prometheus` with the `includedNames` query parameter, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/prometheus/names/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/prometheus/names/http-response.adoc[]
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
[[quartz]]
|
||||
= Quartz (`quartz`)
|
||||
|
||||
The `quartz` endpoint provides information about jobs and triggers that are managed by the Quartz Scheduler.
|
||||
|
||||
|
||||
|
||||
[[quartz.report]]
|
||||
== Retrieving Registered Groups
|
||||
|
||||
Jobs and triggers are managed in groups.
|
||||
To retrieve the list of registered job and trigger groups, make a `GET` request to `/actuator/quartz`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/quartz/report/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/report/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.report.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains the groups names for registered jobs and triggers.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/quartz/report/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.job-groups]]
|
||||
== Retrieving Registered Job Names
|
||||
|
||||
To retrieve the list of registered job names, make a `GET` request to `/actuator/quartz/jobs`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/quartz/jobs/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/jobs/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.job-groups.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains the registered job names for each group.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/quartz/jobs/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger-groups]]
|
||||
== Retrieving Registered Trigger Names
|
||||
|
||||
To retrieve the list of registered trigger names, make a `GET` request to `/actuator/quartz/triggers`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/quartz/triggers/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/triggers/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger-groups.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains the registered trigger names for each group.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/quartz/triggers/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.job-group]]
|
||||
== Retrieving Overview of a Job Group
|
||||
|
||||
To retrieve an overview of the jobs in a particular group, make a `GET` request to `/actuator/quartz/jobs/\{groupName}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/quartz/job-group/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves the summary for jobs in the `samples` group.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/job-group/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.job-group.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains an overview of jobs in a particular group.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/quartz/job-group/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger-group]]
|
||||
== Retrieving Overview of a Trigger Group
|
||||
|
||||
To retrieve an overview of the triggers in a particular group, make a `GET` request to `/actuator/quartz/triggers/\{groupName}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-group/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves the summary for triggers in the `tests` group.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-group/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger-group.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains an overview of triggers in a particular group.
|
||||
Trigger implementation specific details are available.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/quartz/trigger-group/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.job]]
|
||||
== Retrieving Details of a Job
|
||||
|
||||
To retrieve the details about a particular job, make a `GET` request to `/actuator/quartz/jobs/\{groupName}/\{jobName}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/quartz/job-details/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves the details of the job identified by the `samples` group and `jobOne` name.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/job-details/http-response.adoc[]
|
||||
|
||||
If a key in the data map is identified as sensitive, its value is sanitized.
|
||||
|
||||
|
||||
|
||||
[[quartz.job.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains the full details of a job including a summary of the triggers associated with it, if any.
|
||||
The triggers are sorted by next fire time and priority.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/quartz/job-details/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger-job]]
|
||||
== Trigger Quartz Job On Demand
|
||||
|
||||
To trigger a particular Quartz job, make a `POST` request to `/actuator/quartz/jobs/\{groupName}/\{jobName}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-job/curl-request.adoc[]
|
||||
|
||||
The preceding example demonstrates how to trigger a job that belongs to the `samples` group and is named `jobOne`.
|
||||
|
||||
The response will look similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-job/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger-job.request-structure]]
|
||||
=== Request Structure
|
||||
|
||||
The request specifies a desired `state` associated with a particular job.
|
||||
Sending an HTTP request with a `"state": "running"` body indicates that the job should be run now.
|
||||
The following table describes the structure of the request:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/quartz/trigger-job/request-fields.adoc[]
|
||||
|
||||
[[quartz.trigger-job.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains the details of a triggered job.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/quartz/trigger-job/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger]]
|
||||
== Retrieving Details of a Trigger
|
||||
|
||||
To retrieve the details about a particular trigger, make a `GET` request to `/actuator/quartz/triggers/\{groupName}/\{triggerName}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-details-cron/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves the details of trigger identified by the `samples` group and `example` name.
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger.common-response-structure]]
|
||||
=== Common Response Structure
|
||||
|
||||
The response has a common structure and an additional object that is specific to the trigger's type.
|
||||
There are five supported types:
|
||||
|
||||
* `cron` for `CronTrigger`
|
||||
* `simple` for `SimpleTrigger`
|
||||
* `dailyTimeInterval` for `DailyTimeIntervalTrigger`
|
||||
* `calendarInterval` for `CalendarIntervalTrigger`
|
||||
* `custom` for any other trigger implementations
|
||||
|
||||
The following table describes the structure of the common elements of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/quartz/trigger-details-common/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger.cron-response-structure]]
|
||||
=== Cron Trigger Response Structure
|
||||
|
||||
A cron trigger defines the cron expression that is used to determine when it has to fire.
|
||||
The resulting response for such a trigger implementation is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-details-cron/http-response.adoc[]
|
||||
|
||||
|
||||
Much of the response is common to all trigger types.
|
||||
The structure of the common elements of the response was xref:rest/actuator/quartz.adoc#quartz.trigger.common-response-structure[described previously].
|
||||
The following table describes the structure of the parts of the response that are specific to cron triggers:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/quartz/trigger-details-cron/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger.simple-response-structure]]
|
||||
=== Simple Trigger Response Structure
|
||||
|
||||
A simple trigger is used to fire a Job at a given moment in time, and optionally repeated at a specified interval.
|
||||
The resulting response for such a trigger implementation is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-details-simple/http-response.adoc[]
|
||||
|
||||
|
||||
Much of the response is common to all trigger types.
|
||||
The structure of the common elements of the response was xref:rest/actuator/quartz.adoc#quartz.trigger.common-response-structure[described previously].
|
||||
The following table describes the structure of the parts of the response that are specific to simple triggers:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/quartz/trigger-details-simple/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger.daily-time-interval-response-structure]]
|
||||
=== Daily Time Interval Trigger Response Structure
|
||||
|
||||
A daily time interval trigger is used to fire a Job based upon daily repeating time intervals.
|
||||
The resulting response for such a trigger implementation is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-details-daily-time-interval/http-response.adoc[]
|
||||
|
||||
|
||||
Much of the response is common to all trigger types.
|
||||
The structure of the common elements of the response was xref:rest/actuator/quartz.adoc#quartz.trigger.common-response-structure[described previously].
|
||||
The following table describes the structure of the parts of the response that are specific to daily time interval triggers:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/quartz/trigger-details-daily-time-interval/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger.calendar-interval-response-structure]]
|
||||
=== Calendar Interval Trigger Response Structure
|
||||
|
||||
A calendar interval trigger is used to fire a Job based upon repeating calendar time intervals.
|
||||
The resulting response for such a trigger implementation is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-details-calendar-interval/http-response.adoc[]
|
||||
|
||||
|
||||
Much of the response is common to all trigger types.
|
||||
The structure of the common elements of the response was xref:rest/actuator/quartz.adoc#quartz.trigger.common-response-structure[described previously].
|
||||
The following table describes the structure of the parts of the response that are specific to calendar interval triggers:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/quartz/trigger-details-calendar-interval/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[quartz.trigger.custom-response-structure]]
|
||||
=== Custom Trigger Response Structure
|
||||
|
||||
A custom trigger is any other implementation.
|
||||
The resulting response for such a trigger implementation is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/quartz/trigger-details-custom/http-response.adoc[]
|
||||
|
||||
|
||||
Much of the response is common to all trigger types.
|
||||
The structure of the common elements of the response was xref:rest/actuator/quartz.adoc#quartz.trigger.common-response-structure[described previously].
|
||||
The following table describes the structure of the parts of the response that are specific to custom triggers:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/quartz/trigger-details-custom/response-fields.adoc[]
|
||||
@@ -0,0 +1,66 @@
|
||||
[[sbom]]
|
||||
= Software Bill of Materials (`sbom`)
|
||||
|
||||
The `sbom` endpoint provides information about the software bill of materials (SBOM).
|
||||
|
||||
|
||||
|
||||
[[sbom.retrieving-available-sboms]]
|
||||
== Retrieving the Available SBOMs
|
||||
|
||||
To retrieve the available SBOMs, make a `GET` request to `/actuator/sbom`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/sbom/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/sbom/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[sbom.retrieving-available-sboms.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains the available SBOMs.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/sbom/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[sbom.retrieving-single-sbom]]
|
||||
== Retrieving a Single SBOM
|
||||
|
||||
To retrieve the available SBOMs, make a `GET` request to `/actuator/sbom/\{id}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/sbom/id/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves the SBOM named application.
|
||||
The resulting response depends on the format of the SBOM.
|
||||
This example uses the CycloneDX format.
|
||||
|
||||
[source,http,options="nowrap"]
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/vnd.cyclonedx+json
|
||||
Accept-Ranges: bytes
|
||||
Content-Length: 160316
|
||||
|
||||
{
|
||||
"bomFormat" : "CycloneDX",
|
||||
"specVersion" : "1.5",
|
||||
"serialNumber" : "urn:uuid:13862013-3360-43e5-8055-3645aa43c548",
|
||||
"version" : 1,
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[sbom.retrieving-single-sbom.response-structure]]
|
||||
=== Response Structure
|
||||
The response depends on the format of the SBOM:
|
||||
|
||||
* https://cyclonedx.org/specification/overview/[CycloneDX]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[[scheduled-tasks]]
|
||||
= Scheduled Tasks (`scheduledtasks`)
|
||||
|
||||
The `scheduledtasks` endpoint provides information about the application's scheduled tasks.
|
||||
|
||||
|
||||
|
||||
[[scheduled-tasks.retrieving]]
|
||||
== Retrieving the Scheduled Tasks
|
||||
|
||||
To retrieve the scheduled tasks, make a `GET` request to `/actuator/scheduledtasks`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/scheduled-tasks/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/scheduled-tasks/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[scheduled-tasks.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application's scheduled tasks.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/scheduled-tasks/response-fields.adoc[]
|
||||
@@ -0,0 +1,76 @@
|
||||
[[sessions]]
|
||||
= Sessions (`sessions`)
|
||||
|
||||
The `sessions` endpoint provides information about the application's HTTP sessions that are managed by Spring Session.
|
||||
|
||||
|
||||
|
||||
[[sessions.retrieving]]
|
||||
== Retrieving Sessions
|
||||
|
||||
To retrieve the sessions, make a `GET` request to `/actuator/sessions`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/sessions/username/curl-request.adoc[]
|
||||
|
||||
The preceding examples retrieves all of the sessions for the user whose username is `alice`.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/sessions/username/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[sessions.retrieving.query-parameters]]
|
||||
=== Query Parameters
|
||||
|
||||
The endpoint uses query parameters to limit the sessions that it returns.
|
||||
The following table shows the single required query parameter:
|
||||
|
||||
[cols="2,4"]
|
||||
include::partial$rest/actuator/sessions/username/query-parameters.adoc[]
|
||||
|
||||
|
||||
|
||||
[[sessions.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the matching sessions.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/sessions/username/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[sessions.retrieving-id]]
|
||||
== Retrieving a Single Session
|
||||
|
||||
To retrieve a single session, make a `GET` request to `/actuator/sessions/\{id}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/sessions/id/curl-request.adoc[]
|
||||
|
||||
The preceding example retrieves the session with the `id` of `4db5efcc-99cb-4d05-a52c-b49acfbb7ea9`.
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/sessions/id/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[sessions.retrieving-id.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the requested session.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/sessions/id/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[sessions.deleting]]
|
||||
== Deleting a Session
|
||||
|
||||
To delete a session, make a `DELETE` request to `/actuator/sessions/\{id}`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/sessions/delete/curl-request.adoc[]
|
||||
|
||||
The preceding example deletes the session with the `id` of `4db5efcc-99cb-4d05-a52c-b49acfbb7ea9`.
|
||||
@@ -0,0 +1,28 @@
|
||||
[[shutdown]]
|
||||
= Shutdown (`shutdown`)
|
||||
|
||||
The `shutdown` endpoint is used to shut down the application.
|
||||
|
||||
|
||||
|
||||
[[shutdown.shutting-down]]
|
||||
== Shutting Down the Application
|
||||
|
||||
To shut down the application, make a `POST` request to `/actuator/shutdown`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/shutdown/curl-request.adoc[]
|
||||
|
||||
A response similar to the following is produced:
|
||||
|
||||
include::partial$rest/actuator/shutdown/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[shutdown.shutting-down.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the result of the shutdown request.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,3"]
|
||||
include::partial$rest/actuator/shutdown/response-fields.adoc[]
|
||||
@@ -0,0 +1,48 @@
|
||||
[[startup]]
|
||||
= Application Startup (`startup`)
|
||||
|
||||
The `startup` endpoint provides information about the application's startup sequence.
|
||||
|
||||
|
||||
|
||||
[[startup.retrieving]]
|
||||
== Retrieving the Application Startup Steps
|
||||
|
||||
The application startup steps can either be retrieved as a snapshot (`GET`) or drained from the buffer (`POST`).
|
||||
|
||||
|
||||
|
||||
[[startup.retrieving.snapshot]]
|
||||
=== Retrieving a snapshot of the Application Startup Steps
|
||||
|
||||
To retrieve the steps recorded so far during the application startup phase, make a `GET` request to `/actuator/startup`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/startup-snapshot/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/startup-snapshot/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[startup.retrieving.drain]]
|
||||
=== Draining the Application Startup Steps
|
||||
|
||||
To drain and return the steps recorded so far during the application startup phase, make a `POST` request to `/actuator/startup`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/startup/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/startup/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[startup.retrieving.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the application startup steps.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="2,1,3"]
|
||||
include::partial$rest/actuator/startup/response-fields.adoc[]
|
||||
@@ -0,0 +1,42 @@
|
||||
[[threaddump]]
|
||||
= Thread Dump (`threaddump`)
|
||||
|
||||
The `threaddump` endpoint provides a thread dump from the application's JVM.
|
||||
|
||||
|
||||
|
||||
[[threaddump.retrieving-json]]
|
||||
== Retrieving the Thread Dump as JSON
|
||||
|
||||
To retrieve the thread dump as JSON, make a `GET` request to `/actuator/threaddump` with an appropriate `Accept` header, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/threaddump/json/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/threaddump/json/http-response.adoc[]
|
||||
|
||||
|
||||
|
||||
[[threaddump.retrieving-json.response-structure]]
|
||||
=== Response Structure
|
||||
|
||||
The response contains details of the JVM's threads.
|
||||
The following table describes the structure of the response:
|
||||
|
||||
[cols="3,1,2"]
|
||||
include::partial$rest/actuator/threaddump/json/response-fields.adoc[]
|
||||
|
||||
|
||||
|
||||
[[threaddump.retrieving-text]]
|
||||
== Retrieving the Thread Dump as Text
|
||||
|
||||
To retrieve the thread dump as text, make a `GET` request to `/actuator/threaddump` that
|
||||
accepts `text/plain`, as shown in the following curl-based example:
|
||||
|
||||
include::partial$rest/actuator/threaddump/text/curl-request.adoc[]
|
||||
|
||||
The resulting response is similar to the following:
|
||||
|
||||
include::partial$rest/actuator/threaddump/text/http-response.adoc[]
|
||||
@@ -0,0 +1,26 @@
|
||||
* xref:api:rest/actuator/index.adoc[]
|
||||
** xref:api:rest/actuator/auditevents.adoc[]
|
||||
** xref:api:rest/actuator/beans.adoc[]
|
||||
** xref:api:rest/actuator/caches.adoc[]
|
||||
** xref:api:rest/actuator/conditions.adoc[]
|
||||
** xref:api:rest/actuator/configprops.adoc[]
|
||||
** xref:api:rest/actuator/env.adoc[]
|
||||
** xref:api:rest/actuator/flyway.adoc[]
|
||||
** xref:api:rest/actuator/health.adoc[]
|
||||
** xref:api:rest/actuator/heapdump.adoc[]
|
||||
** xref:api:rest/actuator/httpexchanges.adoc[]
|
||||
** xref:api:rest/actuator/info.adoc[]
|
||||
** xref:api:rest/actuator/integrationgraph.adoc[]
|
||||
** xref:api:rest/actuator/liquibase.adoc[]
|
||||
** xref:api:rest/actuator/logfile.adoc[]
|
||||
** xref:api:rest/actuator/loggers.adoc[]
|
||||
** xref:api:rest/actuator/mappings.adoc[]
|
||||
** xref:api:rest/actuator/metrics.adoc[]
|
||||
** xref:api:rest/actuator/prometheus.adoc[]
|
||||
** xref:api:rest/actuator/quartz.adoc[]
|
||||
** xref:api:rest/actuator/sbom.adoc[]
|
||||
** xref:api:rest/actuator/scheduledtasks.adoc[]
|
||||
** xref:api:rest/actuator/sessions.adoc[]
|
||||
** xref:api:rest/actuator/shutdown.adoc[]
|
||||
** xref:api:rest/actuator/startup.adoc[]
|
||||
** xref:api:rest/actuator/threaddump.adoc[]
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration;
|
||||
import org.springframework.boot.actuate.docs.AbstractEndpointDocumentationTests.BaseDocumentationConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.webflux.autoconfigure.HttpHandlerAutoConfiguration;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.restdocs.operation.preprocess.ContentModifyingOperationPreprocessor;
|
||||
import org.springframework.restdocs.operation.preprocess.OperationPreprocessor;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
|
||||
/**
|
||||
* Abstract base class for tests that generate endpoint documentation using Spring REST
|
||||
* Docs.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@TestPropertySource(properties = { "management.endpoints.web.exposure.include=*" })
|
||||
@Import(BaseDocumentationConfiguration.class)
|
||||
public abstract class AbstractEndpointDocumentationTests {
|
||||
|
||||
protected static String describeEnumValues(Class<? extends Enum<?>> enumType) {
|
||||
return StringUtils.collectionToDelimitedString(
|
||||
Stream.of(enumType.getEnumConstants()).map((constant) -> "`" + constant.name() + "`").toList(), ", ");
|
||||
}
|
||||
|
||||
protected OperationPreprocessor limit(String... keys) {
|
||||
return limit((candidate) -> true, keys);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T> OperationPreprocessor limit(Predicate<T> filter, String... keys) {
|
||||
return new ContentModifyingOperationPreprocessor((content, mediaType) -> {
|
||||
ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||
try {
|
||||
Map<String, Object> payload = objectMapper.readValue(content, Map.class);
|
||||
Object target = payload;
|
||||
Map<Object, Object> parent = null;
|
||||
for (String key : keys) {
|
||||
if (!(target instanceof Map)) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
parent = (Map<Object, Object>) target;
|
||||
target = parent.get(key);
|
||||
}
|
||||
if (target instanceof Map) {
|
||||
parent.put(keys[keys.length - 1], select((Map<String, Object>) target, filter));
|
||||
}
|
||||
else {
|
||||
parent.put(keys[keys.length - 1], select((List<Object>) target, filter));
|
||||
}
|
||||
return objectMapper.writeValueAsBytes(payload);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected FieldDescriptor parentIdField() {
|
||||
return fieldWithPath("contexts.*.parentId").description("Id of the parent application context, if any.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> Map<String, Object> select(Map<String, Object> candidates, Predicate<T> filter) {
|
||||
Map<String, Object> selected = new HashMap<>();
|
||||
candidates.entrySet()
|
||||
.stream()
|
||||
.filter((candidate) -> filter.test((T) candidate))
|
||||
.limit(3)
|
||||
.forEach((entry) -> selected.put(entry.getKey(), entry.getValue()));
|
||||
return selected;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> List<Object> select(List<Object> candidates, Predicate<T> filter) {
|
||||
return candidates.stream().filter((candidate) -> filter.test((T) candidate)).limit(3).toList();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportAutoConfiguration({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
|
||||
WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class,
|
||||
WebEndpointAutoConfiguration.class, WebMvcEndpointManagementContextConfiguration.class,
|
||||
WebFluxEndpointManagementContextConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
|
||||
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class,
|
||||
JacksonEndpointAutoConfiguration.class })
|
||||
static class BaseDocumentationConfiguration {
|
||||
|
||||
@Bean
|
||||
static BeanPostProcessor endpointObjectMapperBeanPostProcessor() {
|
||||
return new BeanPostProcessor() {
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof EndpointObjectMapper) {
|
||||
return (EndpointObjectMapper) () -> ((EndpointObjectMapper) bean).get()
|
||||
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.restdocs.RestDocumentationContextProvider;
|
||||
import org.springframework.restdocs.RestDocumentationExtension;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* Abstract base class for tests that generate endpoint documentation using Spring REST
|
||||
* Docs and {@link MockMvcTester}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@ExtendWith(RestDocumentationExtension.class)
|
||||
@SpringBootTest
|
||||
public abstract class MockMvcEndpointDocumentationTests extends AbstractEndpointDocumentationTests {
|
||||
|
||||
protected MockMvcTester mvc;
|
||||
|
||||
@Autowired
|
||||
private WebApplicationContext applicationContext;
|
||||
|
||||
@BeforeEach
|
||||
void setup(RestDocumentationContextProvider restDocumentation) {
|
||||
this.mvc = MockMvcTester.from(this.applicationContext,
|
||||
(builder) -> builder
|
||||
.apply(MockMvcRestDocumentation.documentationConfiguration(restDocumentation).uris())
|
||||
.build());
|
||||
}
|
||||
|
||||
protected WebApplicationContext getApplicationContext() {
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.audit;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
||||
import org.springframework.boot.actuate.audit.AuditEventsEndpoint;
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing {@link AuditEventsEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class AuditEventsEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@MockitoBean
|
||||
private AuditEventRepository repository;
|
||||
|
||||
@Test
|
||||
void allAuditEvents() {
|
||||
String queryTimestamp = "2017-11-07T09:37Z";
|
||||
given(this.repository.find(any(), any(), any()))
|
||||
.willReturn(List.of(new AuditEvent("alice", "logout", Collections.emptyMap())));
|
||||
assertThat(this.mvc.get().uri("/actuator/auditevents").param("after", queryTimestamp)).hasStatusOk()
|
||||
.apply(document("auditevents/all",
|
||||
responseFields(fieldWithPath("events").description("An array of audit events."),
|
||||
fieldWithPath("events.[].timestamp")
|
||||
.description("The timestamp of when the event occurred."),
|
||||
fieldWithPath("events.[].principal").description("The principal that triggered the event."),
|
||||
fieldWithPath("events.[].type").description("The type of the event."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void filteredAuditEvents() {
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
String queryTimestamp = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(now);
|
||||
given(this.repository.find("alice", now.toInstant(), "logout"))
|
||||
.willReturn(List.of(new AuditEvent("alice", "logout", Collections.emptyMap())));
|
||||
assertThat(this.mvc.get()
|
||||
.uri("/actuator/auditevents")
|
||||
.param("principal", "alice")
|
||||
.param("after", queryTimestamp)
|
||||
.param("type", "logout"))
|
||||
.hasStatusOk()
|
||||
.apply(document("auditevents/filtered",
|
||||
queryParameters(
|
||||
parameterWithName("after").description(
|
||||
"Restricts the events to those that occurred after the given time. Optional."),
|
||||
parameterWithName("principal")
|
||||
.description("Restricts the events to those with the given principal. Optional."),
|
||||
parameterWithName("type")
|
||||
.description("Restricts the events to those with the given type. Optional."))));
|
||||
then(this.repository).should().find("alice", now.toInstant(), "logout");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
AuditEventsEndpoint auditEventsEndpoint(AuditEventRepository repository) {
|
||||
return new AuditEventsEndpoint(repository);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.beans;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.beans.BeansEndpoint;
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.restdocs.payload.ResponseFieldsSnippet;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing {@link BeansEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class BeansEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void beans() {
|
||||
List<FieldDescriptor> beanFields = List.of(fieldWithPath("aliases").description("Names of any aliases."),
|
||||
fieldWithPath("scope").description("Scope of the bean."),
|
||||
fieldWithPath("type").description("Fully qualified type of the bean."),
|
||||
fieldWithPath("resource").description("Resource in which the bean was defined, if any.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("dependencies").description("Names of any dependencies."));
|
||||
ResponseFieldsSnippet responseFields = responseFields(
|
||||
fieldWithPath("contexts").description("Application contexts keyed by id."), parentIdField(),
|
||||
fieldWithPath("contexts.*.beans").description("Beans in the application context keyed by name."))
|
||||
.andWithPrefix("contexts.*.beans.*.", beanFields);
|
||||
assertThat(this.mvc.get().uri("/actuator/beans")).hasStatusOk()
|
||||
.apply(document("beans",
|
||||
preprocessResponse(
|
||||
limit(this::isIndependentBean, "contexts", getApplicationContext().getId(), "beans")),
|
||||
responseFields));
|
||||
}
|
||||
|
||||
private boolean isIndependentBean(Entry<String, Map<String, Object>> bean) {
|
||||
return CollectionUtils.isEmpty((Collection<?>) bean.getValue().get("aliases"))
|
||||
&& CollectionUtils.isEmpty((Collection<?>) bean.getValue().get("dependencies"));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
BeansEndpoint beansEndpoint(ConfigurableApplicationContext context) {
|
||||
return new BeansEndpoint(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.cache;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.cache.CachesEndpoint;
|
||||
import org.springframework.boot.actuate.cache.CachesEndpointWebExtension;
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.request.ParameterDescriptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link CachesEndpoint}
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class CachesEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
private static final List<FieldDescriptor> levelFields = List.of(fieldWithPath("name").description("Cache name."),
|
||||
fieldWithPath("cacheManager").description("Cache manager name."),
|
||||
fieldWithPath("target").description("Fully qualified name of the native cache."));
|
||||
|
||||
private static final List<ParameterDescriptor> queryParameters = Collections
|
||||
.singletonList(parameterWithName("cacheManager")
|
||||
.description("Name of the cacheManager to qualify the cache. May be omitted if the cache name is unique.")
|
||||
.optional());
|
||||
|
||||
@Test
|
||||
void allCaches() {
|
||||
assertThat(this.mvc.get().uri("/actuator/caches")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("caches/all",
|
||||
responseFields(fieldWithPath("cacheManagers").description("Cache managers keyed by id."),
|
||||
fieldWithPath("cacheManagers.*.caches")
|
||||
.description("Caches in the application context keyed by name."))
|
||||
.andWithPrefix("cacheManagers.*.caches.*.",
|
||||
fieldWithPath("target").description("Fully qualified name of the native cache."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void namedCache() {
|
||||
assertThat(this.mvc.get().uri("/actuator/caches/cities")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("caches/named", queryParameters(queryParameters),
|
||||
responseFields(levelFields)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void evictAllCaches() {
|
||||
assertThat(this.mvc.delete().uri("/actuator/caches")).hasStatus(HttpStatus.NO_CONTENT)
|
||||
.apply(MockMvcRestDocumentation.document("caches/evict-all"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void evictNamedCache() {
|
||||
assertThat(this.mvc.delete().uri("/actuator/caches/countries?cacheManager=anotherCacheManager"))
|
||||
.hasStatus(HttpStatus.NO_CONTENT)
|
||||
.apply(MockMvcRestDocumentation.document("caches/evict-named", queryParameters(queryParameters)));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
CachesEndpoint endpoint() {
|
||||
Map<String, CacheManager> cacheManagers = new HashMap<>();
|
||||
cacheManagers.put("cacheManager", new ConcurrentMapCacheManager("countries", "cities"));
|
||||
cacheManagers.put("anotherCacheManager", new ConcurrentMapCacheManager("countries"));
|
||||
return new CachesEndpoint(cacheManagers);
|
||||
}
|
||||
|
||||
@Bean
|
||||
CachesEndpointWebExtension endpointWebExtension(CachesEndpoint endpoint) {
|
||||
return new CachesEndpointWebExtension(endpoint);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.condition;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoint;
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing {@link ConditionsReportEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ConditionsReportEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void conditions() {
|
||||
List<FieldDescriptor> positiveMatchFields = List.of(
|
||||
fieldWithPath("").description("Classes and methods with conditions that were matched."),
|
||||
fieldWithPath(".*.[].condition").description("Name of the condition."),
|
||||
fieldWithPath(".*.[].message").description("Details of why the condition was matched."));
|
||||
List<FieldDescriptor> negativeMatchFields = List.of(
|
||||
fieldWithPath("").description("Classes and methods with conditions that were not matched."),
|
||||
fieldWithPath(".*.notMatched").description("Conditions that were matched."),
|
||||
fieldWithPath(".*.notMatched.[].condition").description("Name of the condition."),
|
||||
fieldWithPath(".*.notMatched.[].message").description("Details of why the condition was not matched."),
|
||||
fieldWithPath(".*.matched").description("Conditions that were matched."),
|
||||
fieldWithPath(".*.matched.[].condition").description("Name of the condition.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fieldWithPath(".*.matched.[].message").description("Details of why the condition was matched.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional());
|
||||
FieldDescriptor unconditionalClassesField = fieldWithPath("contexts.*.unconditionalClasses")
|
||||
.description("Names of unconditional auto-configuration classes if any.");
|
||||
assertThat(this.mvc.get().uri("/actuator/conditions")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("conditions",
|
||||
preprocessResponse(limit("contexts", getApplicationContext().getId(), "positiveMatches"),
|
||||
limit("contexts", getApplicationContext().getId(), "negativeMatches")),
|
||||
responseFields(fieldWithPath("contexts").description("Application contexts keyed by id."))
|
||||
.andWithPrefix("contexts.*.positiveMatches", positiveMatchFields)
|
||||
.andWithPrefix("contexts.*.negativeMatches", negativeMatchFields)
|
||||
.and(unconditionalClassesField, parentIdField())));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
ConditionsReportEndpoint autoConfigurationReportEndpoint(ConfigurableApplicationContext context) {
|
||||
ConditionEvaluationReport conditionEvaluationReport = ConditionEvaluationReport
|
||||
.get(context.getBeanFactory());
|
||||
conditionEvaluationReport
|
||||
.recordEvaluationCandidates(List.of(PropertyPlaceholderAutoConfiguration.class.getName()));
|
||||
return new ConditionsReportEndpoint(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.context;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.context.ShutdownEndpoint;
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link ShutdownEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@TestPropertySource(properties = "management.endpoint.shutdown.access=unrestricted")
|
||||
class ShutdownEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void shutdown() {
|
||||
assertThat(this.mvc.post().uri("/actuator/shutdown")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("shutdown", responseFields(
|
||||
fieldWithPath("message").description("Message describing the result of the request."))));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
ShutdownEndpoint endpoint() {
|
||||
ShutdownEndpoint endpoint = new ShutdownEndpoint();
|
||||
endpoint.setApplicationContext(new AnnotationConfigApplicationContext());
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.context.properties;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint;
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.endpoint.Show;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing
|
||||
* {@link ConfigurationPropertiesReportEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class ConfigurationPropertiesReportEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void configProps() {
|
||||
assertThat(this.mvc.get().uri("/actuator/configprops")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("configprops/all",
|
||||
preprocessResponse(limit("contexts", getApplicationContext().getId(), "beans")),
|
||||
responseFields(fieldWithPath("contexts").description("Application contexts keyed by id."),
|
||||
fieldWithPath("contexts.*.beans.*")
|
||||
.description("`@ConfigurationProperties` beans keyed by bean name."),
|
||||
fieldWithPath("contexts.*.beans.*.prefix")
|
||||
.description("Prefix applied to the names of the bean's properties."),
|
||||
subsectionWithPath("contexts.*.beans.*.properties")
|
||||
.description("Properties of the bean as name-value pairs."),
|
||||
subsectionWithPath("contexts.*.beans.*.inputs").description(
|
||||
"Origin and value of the configuration property used when binding to this bean."),
|
||||
parentIdField())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configPropsFilterByPrefix() {
|
||||
assertThat(this.mvc.get().uri("/actuator/configprops/spring.jackson")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("configprops/prefixed",
|
||||
preprocessResponse(limit("contexts", getApplicationContext().getId(), "beans")),
|
||||
responseFields(fieldWithPath("contexts").description("Application contexts keyed by id."),
|
||||
fieldWithPath("contexts.*.beans.*")
|
||||
.description("`@ConfigurationProperties` beans keyed by bean name."),
|
||||
fieldWithPath("contexts.*.beans.*.prefix")
|
||||
.description("Prefix applied to the names of the bean's properties."),
|
||||
subsectionWithPath("contexts.*.beans.*.properties")
|
||||
.description("Properties of the bean as name-value pairs."),
|
||||
subsectionWithPath("contexts.*.beans.*.inputs").description(
|
||||
"Origin and value of the configuration property used when binding to this bean."),
|
||||
parentIdField())));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
ConfigurationPropertiesReportEndpoint endpoint() {
|
||||
return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.env;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.endpoint.Show;
|
||||
import org.springframework.boot.actuate.env.EnvironmentEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.EnumerablePropertySource;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.operation.preprocess.ContentModifyingOperationPreprocessor;
|
||||
import org.springframework.restdocs.operation.preprocess.OperationPreprocessor;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link EnvironmentEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@TestPropertySource(properties = "spring.config.location=classpath:/org/springframework/boot/actuate/docs/env/")
|
||||
class EnvironmentEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
private static final FieldDescriptor activeProfiles = fieldWithPath("activeProfiles")
|
||||
.description("Names of the active profiles, if any.");
|
||||
|
||||
private static final FieldDescriptor defaultProfiles = fieldWithPath("defaultProfiles")
|
||||
.description("Names of the default profiles, if any.");
|
||||
|
||||
private static final FieldDescriptor propertySources = fieldWithPath("propertySources")
|
||||
.description("Property sources in order of precedence.");
|
||||
|
||||
private static final FieldDescriptor propertySourceName = fieldWithPath("propertySources.[].name")
|
||||
.description("Name of the property source.");
|
||||
|
||||
@Test
|
||||
void env() {
|
||||
assertThat(this.mvc.get().uri("/actuator/env")).hasStatusOk()
|
||||
.apply(document("env/all",
|
||||
preprocessResponse(
|
||||
replacePattern(Pattern.compile(
|
||||
"org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/"), ""),
|
||||
filterProperties()),
|
||||
responseFields(activeProfiles, defaultProfiles, propertySources, propertySourceName,
|
||||
fieldWithPath("propertySources.[].properties")
|
||||
.description("Properties in the property source keyed by property name."),
|
||||
fieldWithPath("propertySources.[].properties.*.value")
|
||||
.description("Value of the property."),
|
||||
fieldWithPath("propertySources.[].properties.*.origin")
|
||||
.description("Origin of the property, if any.")
|
||||
.optional())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singlePropertyFromEnv() {
|
||||
assertThat(this.mvc.get().uri("/actuator/env/com.example.cache.max-size")).hasStatusOk()
|
||||
.apply(document("env/single",
|
||||
preprocessResponse(replacePattern(Pattern
|
||||
.compile("org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/"), "")),
|
||||
responseFields(
|
||||
fieldWithPath("property").description("Property from the environment, if found.")
|
||||
.optional(),
|
||||
fieldWithPath("property.source").description("Name of the source of the property."),
|
||||
fieldWithPath("property.value").description("Value of the property."), activeProfiles,
|
||||
defaultProfiles, propertySources, propertySourceName,
|
||||
fieldWithPath("propertySources.[].property")
|
||||
.description("Property in the property source, if any.")
|
||||
.optional(),
|
||||
fieldWithPath("propertySources.[].property.value").description("Value of the property."),
|
||||
fieldWithPath("propertySources.[].property.origin")
|
||||
.description("Origin of the property, if any.")
|
||||
.optional())));
|
||||
}
|
||||
|
||||
private OperationPreprocessor filterProperties() {
|
||||
return new ContentModifyingOperationPreprocessor(this::filterProperties);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private byte[] filterProperties(byte[] content, MediaType mediaType) {
|
||||
ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||
try {
|
||||
Map<String, Object> payload = objectMapper.readValue(content, Map.class);
|
||||
List<Map<String, Object>> propertySources = (List<Map<String, Object>>) payload.get("propertySources");
|
||||
for (Map<String, Object> propertySource : propertySources) {
|
||||
Map<String, String> properties = (Map<String, String>) propertySource.get("properties");
|
||||
Set<String> filteredKeys = properties.keySet()
|
||||
.stream()
|
||||
.filter(this::retainKey)
|
||||
.limit(3)
|
||||
.collect(Collectors.toSet());
|
||||
properties.keySet().retainAll(filteredKeys);
|
||||
}
|
||||
return objectMapper.writeValueAsBytes(payload);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean retainKey(String key) {
|
||||
return key.startsWith("java.") || key.equals("JAVA_HOME") || key.startsWith("com.example.");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
EnvironmentEndpoint endpoint(ConfigurableEnvironment environment) {
|
||||
return new EnvironmentEndpoint(new AbstractEnvironment() {
|
||||
|
||||
@Override
|
||||
protected void customizePropertySources(MutablePropertySources propertySources) {
|
||||
environment.getPropertySources()
|
||||
.stream()
|
||||
.filter(this::includedPropertySource)
|
||||
.forEach(propertySources::addLast);
|
||||
}
|
||||
|
||||
private boolean includedPropertySource(PropertySource<?> propertySource) {
|
||||
return propertySource instanceof EnumerablePropertySource
|
||||
&& !"Inlined Test Properties".equals(propertySource.getName());
|
||||
}
|
||||
|
||||
}, Collections.emptyList(), Show.ALWAYS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.flyway;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.flywaydb.core.api.MigrationState;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.flyway.FlywayEndpoint;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration;
|
||||
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link FlywayEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@TestPropertySource(properties = "spring.flyway.locations=classpath:org/springframework/boot/actuate/docs/flyway")
|
||||
class FlywayEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void flyway() {
|
||||
assertThat(this.mvc.get().uri("/actuator/flyway")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("flyway",
|
||||
responseFields(fieldWithPath("contexts").description("Application contexts keyed by id"),
|
||||
fieldWithPath("contexts.*.flywayBeans.*.migrations")
|
||||
.description("Migrations performed by the Flyway instance, keyed by Flyway bean name."))
|
||||
.andWithPrefix("contexts.*.flywayBeans.*.migrations.[].", migrationFieldDescriptors())
|
||||
.and(parentIdField())));
|
||||
}
|
||||
|
||||
private List<FieldDescriptor> migrationFieldDescriptors() {
|
||||
return List.of(fieldWithPath("checksum").description("Checksum of the migration, if any.").optional(),
|
||||
fieldWithPath("description").description("Description of the migration, if any.").optional(),
|
||||
fieldWithPath("executionTime").description("Execution time in milliseconds of an applied migration.")
|
||||
.optional(),
|
||||
fieldWithPath("installedBy").description("User that installed the applied migration, if any.")
|
||||
.optional(),
|
||||
fieldWithPath("installedOn")
|
||||
.description("Timestamp of when the applied migration was installed, if any.")
|
||||
.optional(),
|
||||
fieldWithPath("installedRank")
|
||||
.description("Rank of the applied migration, if any. Later migrations have higher ranks.")
|
||||
.optional(),
|
||||
fieldWithPath("script").description("Name of the script used to execute the migration, if any.")
|
||||
.optional(),
|
||||
fieldWithPath("state")
|
||||
.description("State of the migration. (" + describeEnumValues(MigrationState.class) + ")"),
|
||||
fieldWithPath("type").description("Type of the migration."),
|
||||
fieldWithPath("version").description("Version of the database after applying the migration, if any.")
|
||||
.optional());
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportAutoConfiguration(FlywayAutoConfiguration.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder().generateUniqueName(true)
|
||||
.setType(EmbeddedDatabaseConnection.get(getClass().getClassLoader()).getType())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
FlywayEndpoint endpoint(ApplicationContext context) {
|
||||
return new FlywayEndpoint(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.health;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
|
||||
import org.springframework.boot.actuate.health.CompositeHealthContributor;
|
||||
import org.springframework.boot.actuate.health.DefaultHealthContributorRegistry;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthContributor;
|
||||
import org.springframework.boot.actuate.health.HealthContributorRegistry;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointGroup;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointGroups;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
|
||||
import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper;
|
||||
import org.springframework.boot.actuate.health.SimpleStatusAggregator;
|
||||
import org.springframework.boot.actuate.health.StatusAggregator;
|
||||
import org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator;
|
||||
import org.springframework.boot.actuate.system.DiskSpaceHealthIndicator;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link HealthEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
private static final List<FieldDescriptor> componentFields = List.of(
|
||||
fieldWithPath("status").description("Status of a specific part of the application"),
|
||||
subsectionWithPath("details").description("Details of the health of a specific part of the application."));
|
||||
|
||||
@Test
|
||||
void health() {
|
||||
FieldDescriptor status = fieldWithPath("status").description("Overall status of the application.");
|
||||
FieldDescriptor components = fieldWithPath("components").description("The components that make up the health.");
|
||||
FieldDescriptor componentStatus = fieldWithPath("components.*.status")
|
||||
.description("Status of a specific part of the application.");
|
||||
FieldDescriptor nestedComponents = subsectionWithPath("components.*.components")
|
||||
.description("The nested components that make up the health.")
|
||||
.optional();
|
||||
FieldDescriptor componentDetails = subsectionWithPath("components.*.details")
|
||||
.description("Details of the health of a specific part of the application. "
|
||||
+ "Presence is controlled by `management.endpoint.health.show-details`.")
|
||||
.optional();
|
||||
assertThat(this.mvc.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON)).hasStatusOk()
|
||||
.apply(document("health",
|
||||
responseFields(status, components, componentStatus, nestedComponents, componentDetails)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void healthComponent() {
|
||||
assertThat(this.mvc.get().uri("/actuator/health/db").accept(MediaType.APPLICATION_JSON)).hasStatusOk()
|
||||
.apply(document("health/component", responseFields(componentFields)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void healthComponentInstance() {
|
||||
assertThat(this.mvc.get().uri("/actuator/health/broker/us1").accept(MediaType.APPLICATION_JSON)).hasStatusOk()
|
||||
.apply(document("health/instance", responseFields(componentFields)));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportAutoConfiguration(DataSourceAutoConfiguration.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
HealthEndpoint healthEndpoint(Map<String, HealthContributor> healthContributors) {
|
||||
HealthContributorRegistry registry = new DefaultHealthContributorRegistry(healthContributors);
|
||||
HealthEndpointGroup primary = new TestHealthEndpointGroup();
|
||||
HealthEndpointGroups groups = HealthEndpointGroups.of(primary, Collections.emptyMap());
|
||||
return new HealthEndpoint(registry, groups, null);
|
||||
}
|
||||
|
||||
@Bean
|
||||
DiskSpaceHealthIndicator diskSpaceHealthIndicator() {
|
||||
return new DiskSpaceHealthIndicator(new File("."), DataSize.ofMegabytes(10));
|
||||
}
|
||||
|
||||
@Bean
|
||||
DataSourceHealthIndicator dbHealthIndicator(DataSource dataSource) {
|
||||
return new DataSourceHealthIndicator(dataSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
CompositeHealthContributor brokerHealthContributor() {
|
||||
Map<String, HealthIndicator> indicators = new LinkedHashMap<>();
|
||||
indicators.put("us1", () -> Health.up().withDetail("version", "1.0.2").build());
|
||||
indicators.put("us2", () -> Health.up().withDetail("version", "1.0.4").build());
|
||||
return CompositeHealthContributor.fromMap(indicators);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class TestHealthEndpointGroup implements HealthEndpointGroup {
|
||||
|
||||
private final StatusAggregator statusAggregator = new SimpleStatusAggregator();
|
||||
|
||||
private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper();
|
||||
|
||||
@Override
|
||||
public boolean isMember(String name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean showComponents(SecurityContext securityContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean showDetails(SecurityContext securityContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusAggregator getStatusAggregator() {
|
||||
return this.statusAggregator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpCodeStatusMapper getHttpCodeStatusMapper() {
|
||||
return this.httpCodeStatusMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdditionalHealthEndpointPath getAdditionalPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.info;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.info.BuildInfoContributor;
|
||||
import org.springframework.boot.actuate.info.GitInfoContributor;
|
||||
import org.springframework.boot.actuate.info.InfoContributor;
|
||||
import org.springframework.boot.actuate.info.InfoEndpoint;
|
||||
import org.springframework.boot.actuate.info.JavaInfoContributor;
|
||||
import org.springframework.boot.actuate.info.OsInfoContributor;
|
||||
import org.springframework.boot.actuate.info.ProcessInfoContributor;
|
||||
import org.springframework.boot.actuate.info.SslInfoContributor;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.boot.info.GitProperties;
|
||||
import org.springframework.boot.info.SslInfo;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.restdocs.payload.ResponseFieldsSnippet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link InfoEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class InfoEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void info() {
|
||||
assertThat(this.mvc.get().uri("/actuator/info")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("info", gitInfo(), buildInfo(), osInfo(), processInfo(),
|
||||
javaInfo(), sslInfo()));
|
||||
}
|
||||
|
||||
private ResponseFieldsSnippet gitInfo() {
|
||||
return responseFields(beneathPath("git"),
|
||||
fieldWithPath("branch").description("Name of the Git branch, if any."),
|
||||
fieldWithPath("commit").description("Details of the Git commit, if any."),
|
||||
fieldWithPath("commit.time").description("Timestamp of the commit, if any.").type(JsonFieldType.VARIES),
|
||||
fieldWithPath("commit.id").description("ID of the commit, if any."));
|
||||
}
|
||||
|
||||
private ResponseFieldsSnippet buildInfo() {
|
||||
return responseFields(beneathPath("build"),
|
||||
fieldWithPath("artifact").description("Artifact ID of the application, if any.").optional(),
|
||||
fieldWithPath("group").description("Group ID of the application, if any.").optional(),
|
||||
fieldWithPath("name").description("Name of the application, if any.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fieldWithPath("version").description("Version of the application, if any.").optional(),
|
||||
fieldWithPath("time").description("Timestamp of when the application was built, if any.")
|
||||
.type(JsonFieldType.VARIES)
|
||||
.optional());
|
||||
}
|
||||
|
||||
private ResponseFieldsSnippet osInfo() {
|
||||
return responseFields(beneathPath("os"), osInfoField("name", "Name of the operating system"),
|
||||
osInfoField("version", "Version of the operating system"),
|
||||
osInfoField("arch", "Architecture of the operating system"));
|
||||
}
|
||||
|
||||
private FieldDescriptor osInfoField(String field, String desc) {
|
||||
return fieldWithPath(field).description(desc + " (as obtained from the 'os." + field + "' system property).")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional();
|
||||
}
|
||||
|
||||
private ResponseFieldsSnippet processInfo() {
|
||||
return responseFields(beneathPath("process"),
|
||||
fieldWithPath("pid").description("Process ID.").type(JsonFieldType.NUMBER),
|
||||
fieldWithPath("parentPid").description("Parent Process ID (or -1).").type(JsonFieldType.NUMBER),
|
||||
fieldWithPath("owner").description("Process owner.").type(JsonFieldType.STRING),
|
||||
fieldWithPath("cpus").description("Number of CPUs available to the process.")
|
||||
.type(JsonFieldType.NUMBER),
|
||||
fieldWithPath("memory").description("Memory information."),
|
||||
fieldWithPath("memory.heap").description("Heap memory."),
|
||||
fieldWithPath("memory.heap.init").description("Number of bytes initially requested by the JVM."),
|
||||
fieldWithPath("memory.heap.used").description("Number of bytes currently being used."),
|
||||
fieldWithPath("memory.heap.committed").description("Number of bytes committed for JVM use."),
|
||||
fieldWithPath("memory.heap.max")
|
||||
.description("Maximum number of bytes that can be used by the JVM (or -1)."),
|
||||
fieldWithPath("memory.nonHeap").description("Non-heap memory."),
|
||||
fieldWithPath("memory.nonHeap.init").description("Number of bytes initially requested by the JVM."),
|
||||
fieldWithPath("memory.nonHeap.used").description("Number of bytes currently being used."),
|
||||
fieldWithPath("memory.nonHeap.committed").description("Number of bytes committed for JVM use."),
|
||||
fieldWithPath("memory.nonHeap.max")
|
||||
.description("Maximum number of bytes that can be used by the JVM (or -1)."),
|
||||
fieldWithPath("memory.garbageCollectors").description("Details for garbage collectors."),
|
||||
fieldWithPath("memory.garbageCollectors[].name").description("Name of of the garbage collector."),
|
||||
fieldWithPath("memory.garbageCollectors[].collectionCount")
|
||||
.description("Total number of collections that have occurred."),
|
||||
fieldWithPath("virtualThreads")
|
||||
.description("Virtual thread information (if VirtualThreadSchedulerMXBean is available)")
|
||||
.type(JsonFieldType.OBJECT)
|
||||
.optional(),
|
||||
fieldWithPath("virtualThreads.mounted")
|
||||
.description("Estimate of the number of virtual threads currently mounted by the scheduler.")
|
||||
.type(JsonFieldType.NUMBER)
|
||||
.optional(),
|
||||
fieldWithPath("virtualThreads.queued").description(
|
||||
"Estimate of the number of virtual threads queued to the scheduler to start or continue execution.")
|
||||
.type(JsonFieldType.NUMBER)
|
||||
.optional(),
|
||||
fieldWithPath("virtualThreads.parallelism").description("Scheduler's target parallelism.")
|
||||
.type(JsonFieldType.NUMBER)
|
||||
.optional(),
|
||||
fieldWithPath("virtualThreads.poolSize")
|
||||
.description(
|
||||
"Current number of platform threads that the scheduler has started but have not terminated")
|
||||
.type(JsonFieldType.NUMBER)
|
||||
.optional());
|
||||
}
|
||||
|
||||
private ResponseFieldsSnippet javaInfo() {
|
||||
return responseFields(beneathPath("java"),
|
||||
fieldWithPath("version").description("Java version, if available.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fieldWithPath("vendor").description("Vendor details."),
|
||||
fieldWithPath("vendor.name").description("Vendor name, if available.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fieldWithPath("vendor.version").description("Vendor version, if available.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fieldWithPath("runtime").description("Runtime details."),
|
||||
fieldWithPath("runtime.name").description("Runtime name, if available.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fieldWithPath("runtime.version").description("Runtime version, if available.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fieldWithPath("jvm").description("JVM details."),
|
||||
fieldWithPath("jvm.name").description("JVM name, if available.").type(JsonFieldType.STRING).optional(),
|
||||
fieldWithPath("jvm.vendor").description("JVM vendor, if available.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fieldWithPath("jvm.version").description("JVM version, if available.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional());
|
||||
}
|
||||
|
||||
private ResponseFieldsSnippet sslInfo() {
|
||||
return responseFields(beneathPath("ssl"),
|
||||
fieldWithPath("bundles").description("SSL bundles information.").type(JsonFieldType.ARRAY),
|
||||
fieldWithPath("bundles[].name").description("Name of the SSL bundle.").type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].certificateChains").description("Certificate chains in the bundle.")
|
||||
.type(JsonFieldType.ARRAY),
|
||||
fieldWithPath("bundles[].certificateChains[].alias").description("Alias of the certificate chain.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates").description("Certificates in the chain.")
|
||||
.type(JsonFieldType.ARRAY),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates[].subject")
|
||||
.description("Subject of the certificate.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates[].version")
|
||||
.description("Version of the certificate.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates[].issuer")
|
||||
.description("Issuer of the certificate.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates[].validityStarts")
|
||||
.description("Certificate validity start date.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates[].serialNumber")
|
||||
.description("Serial number of the certificate.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates[].validityEnds")
|
||||
.description("Certificate validity end date.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates[].validity")
|
||||
.description("Certificate validity information.")
|
||||
.type(JsonFieldType.OBJECT),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates[].validity.status")
|
||||
.description("Certificate validity status.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].certificateChains[].certificates[].signatureAlgorithmName")
|
||||
.description("Signature algorithm name.")
|
||||
.type(JsonFieldType.STRING));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
InfoEndpoint endpoint(List<InfoContributor> infoContributors) {
|
||||
return new InfoEndpoint(infoContributors);
|
||||
}
|
||||
|
||||
@Bean
|
||||
GitInfoContributor gitInfoContributor() {
|
||||
Properties properties = new Properties();
|
||||
properties.put("branch", "main");
|
||||
properties.put("commit.id", "df027cf1ec5aeba2d4fedd7b8c42b88dc5ce38e5");
|
||||
properties.put("commit.id.abbrev", "df027cf");
|
||||
properties.put("commit.time", Long.toString(Instant.now().getEpochSecond()));
|
||||
GitProperties gitProperties = new GitProperties(properties);
|
||||
return new GitInfoContributor(gitProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
BuildInfoContributor buildInfoContributor() {
|
||||
Properties properties = new Properties();
|
||||
properties.put("group", "com.example");
|
||||
properties.put("artifact", "application");
|
||||
properties.put("version", "1.0.3");
|
||||
BuildProperties buildProperties = new BuildProperties(properties);
|
||||
return new BuildInfoContributor(buildProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OsInfoContributor osInfoContributor() {
|
||||
return new OsInfoContributor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ProcessInfoContributor processInfoContributor() {
|
||||
return new ProcessInfoContributor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
JavaInfoContributor javaInfoContributor() {
|
||||
return new JavaInfoContributor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
SslInfo sslInfo() {
|
||||
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
|
||||
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("classpath:test.p12")
|
||||
.withPassword("secret");
|
||||
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null);
|
||||
sslBundleRegistry.registerBundle("test-0", SslBundle.of(sslStoreBundle));
|
||||
return new SslInfo(sslBundleRegistry);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SslInfoContributor sslInfoContributor(SslInfo sslInfo) {
|
||||
return new SslInfoContributor(sslInfo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.integration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.integration.IntegrationGraphEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.integration.config.EnableIntegration;
|
||||
import org.springframework.integration.graph.IntegrationGraphServer;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link IntegrationGraphEndpoint}.
|
||||
*
|
||||
* @author Tim Ysewyn
|
||||
*/
|
||||
class IntegrationGraphEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void graph() {
|
||||
assertThat(this.mvc.get().uri("/actuator/integrationgraph")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("integrationgraph/graph"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rebuild() {
|
||||
assertThat(this.mvc.post().uri("/actuator/integrationgraph")).hasStatus(HttpStatus.NO_CONTENT)
|
||||
.apply(MockMvcRestDocumentation.document("integrationgraph/rebuild"));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableIntegration
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
IntegrationGraphServer integrationGraphServer() {
|
||||
return new IntegrationGraphServer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
IntegrationGraphEndpoint endpoint(IntegrationGraphServer integrationGraphServer) {
|
||||
return new IntegrationGraphEndpoint(integrationGraphServer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.liquibase;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import liquibase.changelog.ChangeSet.ExecType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.liquibase.LiquibaseEndpoint;
|
||||
import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration;
|
||||
import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link LiquibaseEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@TestPropertySource(
|
||||
properties = "spring.liquibase.change-log=classpath:org/springframework/boot/actuate/docs/liquibase/db.changelog-master.yaml")
|
||||
class LiquibaseEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void liquibase() {
|
||||
FieldDescriptor changeSetsField = fieldWithPath("contexts.*.liquibaseBeans.*.changeSets")
|
||||
.description("Change sets made by the Liquibase beans, keyed by bean name.");
|
||||
assertThat(this.mvc.get().uri("/actuator/liquibase")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("liquibase",
|
||||
responseFields(fieldWithPath("contexts").description("Application contexts keyed by id"),
|
||||
changeSetsField)
|
||||
.andWithPrefix("contexts.*.liquibaseBeans.*.changeSets[].", getChangeSetFieldDescriptors())
|
||||
.and(parentIdField())));
|
||||
}
|
||||
|
||||
private List<FieldDescriptor> getChangeSetFieldDescriptors() {
|
||||
return List.of(fieldWithPath("author").description("Author of the change set."),
|
||||
fieldWithPath("changeLog").description("Change log that contains the change set."),
|
||||
fieldWithPath("comments").description("Comments on the change set."),
|
||||
fieldWithPath("contexts").description("Contexts of the change set."),
|
||||
fieldWithPath("dateExecuted").description("Timestamp of when the change set was executed."),
|
||||
fieldWithPath("deploymentId").description("ID of the deployment that ran the change set."),
|
||||
fieldWithPath("description").description("Description of the change set."),
|
||||
fieldWithPath("execType")
|
||||
.description("Execution type of the change set (" + describeEnumValues(ExecType.class) + ")."),
|
||||
fieldWithPath("id").description("ID of the change set."),
|
||||
fieldWithPath("labels").description("Labels associated with the change set."),
|
||||
fieldWithPath("checksum").description("Checksum of the change set."),
|
||||
fieldWithPath("orderExecuted").description("Order of the execution of the change set."),
|
||||
fieldWithPath("tag").description("Tag associated with the change set, if any.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import({ EmbeddedDataSourceConfiguration.class, LiquibaseAutoConfiguration.class })
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
LiquibaseEndpoint endpoint(ApplicationContext context) {
|
||||
return new LiquibaseEndpoint(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.logging;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.logging.LogFileWebEndpoint;
|
||||
import org.springframework.boot.logging.LogFile;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link LogFileWebEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class LogFileWebEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void logFile() {
|
||||
assertThat(this.mvc.get().uri("/actuator/logfile")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("logfile/entire"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void logFileRange() {
|
||||
assertThat(this.mvc.get().uri("/actuator/logfile").header("Range", "bytes=0-1023"))
|
||||
.hasStatus(HttpStatus.PARTIAL_CONTENT)
|
||||
.apply(MockMvcRestDocumentation.document("logfile/range"));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
LogFileWebEndpoint endpoint() {
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
environment.setProperty("logging.file.name",
|
||||
"src/test/resources/org/springframework/boot/actuate/docs/logging/sample.log");
|
||||
return new LogFileWebEndpoint(LogFile.get(environment), null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.logging;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.logging.LoggersEndpoint;
|
||||
import org.springframework.boot.logging.LogLevel;
|
||||
import org.springframework.boot.logging.LoggerConfiguration;
|
||||
import org.springframework.boot.logging.LoggerGroups;
|
||||
import org.springframework.boot.logging.LoggingSystem;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link LoggersEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class LoggersEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
private static final List<FieldDescriptor> levelFields = List.of(
|
||||
fieldWithPath("configuredLevel").description("Configured level of the logger, if any.").optional(),
|
||||
fieldWithPath("effectiveLevel").description("Effective level of the logger."));
|
||||
|
||||
private static final List<FieldDescriptor> groupLevelFields = List
|
||||
.of(fieldWithPath("configuredLevel").description("Configured level of the logger group, if any.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(), fieldWithPath("members").description("Loggers that are part of this group"));
|
||||
|
||||
@MockitoBean
|
||||
private LoggingSystem loggingSystem;
|
||||
|
||||
@Autowired
|
||||
private LoggerGroups loggerGroups;
|
||||
|
||||
@Test
|
||||
void allLoggers() {
|
||||
given(this.loggingSystem.getSupportedLogLevels()).willReturn(EnumSet.allOf(LogLevel.class));
|
||||
given(this.loggingSystem.getLoggerConfigurations())
|
||||
.willReturn(List.of(new LoggerConfiguration("ROOT", LogLevel.INFO, LogLevel.INFO),
|
||||
new LoggerConfiguration("com.example", LogLevel.DEBUG, LogLevel.DEBUG)));
|
||||
assertThat(this.mvc.get().uri("/actuator/loggers")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("loggers/all",
|
||||
responseFields(fieldWithPath("levels").description("Levels support by the logging system."),
|
||||
fieldWithPath("loggers").description("Loggers keyed by name."),
|
||||
fieldWithPath("groups").description("Logger groups keyed by name"))
|
||||
.andWithPrefix("loggers.*.", levelFields)
|
||||
.andWithPrefix("groups.*.", groupLevelFields)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void logger() {
|
||||
given(this.loggingSystem.getLoggerConfiguration("com.example"))
|
||||
.willReturn(new LoggerConfiguration("com.example", LogLevel.INFO, LogLevel.INFO));
|
||||
assertThat(this.mvc.get().uri("/actuator/loggers/com.example")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("loggers/single", responseFields(levelFields)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loggerGroups() {
|
||||
this.loggerGroups.get("test").configureLogLevel(LogLevel.INFO, (member, level) -> {
|
||||
});
|
||||
assertThat(this.mvc.get().uri("/actuator/loggers/test")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("loggers/group", responseFields(groupLevelFields)));
|
||||
resetLogger();
|
||||
}
|
||||
|
||||
@Test
|
||||
void setLogLevel() {
|
||||
assertThat(this.mvc.post()
|
||||
.uri("/actuator/loggers/com.example")
|
||||
.content("{\"configuredLevel\":\"debug\"}")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.hasStatus(HttpStatus.NO_CONTENT)
|
||||
.apply(MockMvcRestDocumentation.document("loggers/set",
|
||||
requestFields(fieldWithPath("configuredLevel")
|
||||
.description("Level for the logger. May be omitted to clear the level.")
|
||||
.optional())));
|
||||
then(this.loggingSystem).should().setLogLevel("com.example", LogLevel.DEBUG);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setLogLevelOfLoggerGroup() {
|
||||
assertThat(this.mvc.post()
|
||||
.uri("/actuator/loggers/test")
|
||||
.content("{\"configuredLevel\":\"debug\"}")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.hasStatus(HttpStatus.NO_CONTENT)
|
||||
.apply(MockMvcRestDocumentation.document("loggers/setGroup",
|
||||
requestFields(fieldWithPath("configuredLevel")
|
||||
.description("Level for the logger group. May be omitted to clear the level of the loggers.")
|
||||
.optional())));
|
||||
then(this.loggingSystem).should().setLogLevel("test.member1", LogLevel.DEBUG);
|
||||
then(this.loggingSystem).should().setLogLevel("test.member2", LogLevel.DEBUG);
|
||||
resetLogger();
|
||||
}
|
||||
|
||||
private void resetLogger() {
|
||||
this.loggerGroups.get("test").configureLogLevel(LogLevel.INFO, (a, b) -> {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearLogLevel() {
|
||||
assertThat(this.mvc.post()
|
||||
.uri("/actuator/loggers/com.example")
|
||||
.content("{}")
|
||||
.contentType(MediaType.APPLICATION_JSON)).hasStatus(HttpStatus.NO_CONTENT)
|
||||
.apply(MockMvcRestDocumentation.document("loggers/clear"));
|
||||
then(this.loggingSystem).should().setLogLevel("com.example", null);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
LoggersEndpoint endpoint(LoggingSystem loggingSystem, LoggerGroups groups) {
|
||||
groups.putAll(getLoggerGroups());
|
||||
groups.get("test").configureLogLevel(LogLevel.INFO, (member, level) -> {
|
||||
});
|
||||
return new LoggersEndpoint(loggingSystem, groups);
|
||||
}
|
||||
|
||||
private Map<String, List<String>> getLoggerGroups() {
|
||||
return Collections.singletonMap("test", List.of("test.member1", "test.member2"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.management;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.management.HeapDumpWebEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.restdocs.cli.CliDocumentation;
|
||||
import org.springframework.restdocs.cli.CurlRequestSnippet;
|
||||
import org.springframework.restdocs.operation.Operation;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link HeapDumpWebEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@TestPropertySource(properties = "management.endpoint.heapdump.access=unrestricted")
|
||||
class HeapDumpWebEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void heapDump() {
|
||||
assertThat(this.mvc.get().uri("/actuator/heapdump")).hasStatusOk()
|
||||
.apply(document("heapdump", new CurlRequestSnippet(CliDocumentation.multiLineFormat()) {
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> createModel(Operation operation) {
|
||||
Map<String, Object> model = super.createModel(operation);
|
||||
model.put("options", "-O");
|
||||
return model;
|
||||
}
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
HeapDumpWebEndpoint endpoint() {
|
||||
return new HeapDumpWebEndpoint() {
|
||||
|
||||
@Override
|
||||
protected HeapDumper createHeapDumper() {
|
||||
return (live) -> {
|
||||
File file = Files.createTempFile("heap-", ".hprof").toFile();
|
||||
FileCopyUtils.copy("<<binary content>>", new FileWriter(file));
|
||||
return file;
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.management;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.management.ThreadDumpEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.restdocs.operation.preprocess.ContentModifyingOperationPreprocessor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing {@link ThreadDumpEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ThreadDumpEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void jsonThreadDump() {
|
||||
ReentrantLock lock = new ReentrantLock();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
lock.lock();
|
||||
try {
|
||||
latch.await();
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}).start();
|
||||
assertThat(this.mvc.get().uri("/actuator/threaddump").accept(MediaType.APPLICATION_JSON)).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation
|
||||
.document("threaddump/json", preprocessResponse(limit("threads")), responseFields(
|
||||
fieldWithPath("threads").description("JVM's threads."),
|
||||
fieldWithPath("threads.[].blockedCount")
|
||||
.description("Total number of times that the thread has been blocked."),
|
||||
fieldWithPath("threads.[].blockedTime")
|
||||
.description("Time in milliseconds that the thread has spent "
|
||||
+ "blocked. -1 if thread contention " + "monitoring is disabled."),
|
||||
fieldWithPath("threads.[].daemon")
|
||||
.description(
|
||||
"Whether the thread is a daemon " + "thread. Only available on Java 9 or later.")
|
||||
.optional()
|
||||
.type(JsonFieldType.BOOLEAN),
|
||||
fieldWithPath("threads.[].inNative")
|
||||
.description("Whether the thread is executing native code."),
|
||||
fieldWithPath("threads.[].lockName")
|
||||
.description("Description of the object on which the " + "thread is blocked, if any.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("threads.[].lockInfo")
|
||||
.description("Object for which the thread is blocked waiting.")
|
||||
.optional()
|
||||
.type(JsonFieldType.OBJECT),
|
||||
fieldWithPath("threads.[].lockInfo.className")
|
||||
.description("Fully qualified class name of the lock object.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("threads.[].lockInfo.identityHashCode")
|
||||
.description("Identity hash code of the lock object.")
|
||||
.optional()
|
||||
.type(JsonFieldType.NUMBER),
|
||||
fieldWithPath("threads.[].lockedMonitors")
|
||||
.description("Monitors locked by this thread, if any"),
|
||||
fieldWithPath("threads.[].lockedMonitors.[].className")
|
||||
.description("Class name of the lock object.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("threads.[].lockedMonitors.[].identityHashCode")
|
||||
.description("Identity hash code of the lock object.")
|
||||
.optional()
|
||||
.type(JsonFieldType.NUMBER),
|
||||
fieldWithPath("threads.[].lockedMonitors.[].lockedStackDepth")
|
||||
.description("Stack depth where the monitor was locked.")
|
||||
.optional()
|
||||
.type(JsonFieldType.NUMBER),
|
||||
subsectionWithPath("threads.[].lockedMonitors.[].lockedStackFrame")
|
||||
.description("Stack frame that locked the monitor.")
|
||||
.optional()
|
||||
.type(JsonFieldType.OBJECT),
|
||||
fieldWithPath("threads.[].lockedSynchronizers")
|
||||
.description("Synchronizers locked by this thread."),
|
||||
fieldWithPath("threads.[].lockedSynchronizers.[].className")
|
||||
.description("Class name of the locked synchronizer.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("threads.[].lockedSynchronizers.[].identityHashCode")
|
||||
.description("Identity hash code of the locked synchronizer.")
|
||||
.optional()
|
||||
.type(JsonFieldType.NUMBER),
|
||||
fieldWithPath("threads.[].lockOwnerId")
|
||||
.description("ID of the thread that owns the object on which "
|
||||
+ "the thread is blocked. `-1` if the " + "thread is not blocked."),
|
||||
fieldWithPath("threads.[].lockOwnerName")
|
||||
.description("Name of the thread that owns the "
|
||||
+ "object on which the thread is blocked, if any.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("threads.[].priority")
|
||||
.description("Priority of the thread. Only " + "available on Java 9 or later.")
|
||||
.optional()
|
||||
.type(JsonFieldType.NUMBER),
|
||||
fieldWithPath("threads.[].stackTrace").description("Stack trace of the thread."),
|
||||
fieldWithPath("threads.[].stackTrace.[].classLoaderName")
|
||||
.description("Name of the class loader of the " + "class that contains the execution "
|
||||
+ "point identified by this entry, if " + "any. Only available on Java 9 or later.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("threads.[].stackTrace.[].className").description(
|
||||
"Name of the class that contains the " + "execution point identified by this entry."),
|
||||
fieldWithPath("threads.[].stackTrace.[].fileName")
|
||||
.description("Name of the source file that " + "contains the execution point "
|
||||
+ "identified by this entry, if any.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("threads.[].stackTrace.[].lineNumber").description("Line number of the execution "
|
||||
+ "point identified by this entry. " + "Negative if unknown."),
|
||||
fieldWithPath("threads.[].stackTrace.[].methodName").description("Name of the method."),
|
||||
fieldWithPath("threads.[].stackTrace.[].moduleName")
|
||||
.description("Name of the module that contains " + "the execution point identified by "
|
||||
+ "this entry, if any. Only available " + "on Java 9 or later.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("threads.[].stackTrace.[].moduleVersion")
|
||||
.description("Version of the module that " + "contains the execution point "
|
||||
+ "identified by this entry, if any. " + "Only available on Java 9 or later.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("threads.[].stackTrace.[].nativeMethod")
|
||||
.description("Whether the execution point is a native method."),
|
||||
fieldWithPath("threads.[].suspended").description("Whether the thread is suspended."),
|
||||
fieldWithPath("threads.[].threadId").description("ID of the thread."),
|
||||
fieldWithPath("threads.[].threadName").description("Name of the thread."),
|
||||
fieldWithPath("threads.[].threadState")
|
||||
.description("State of the thread (" + describeEnumValues(Thread.State.class) + ")."),
|
||||
fieldWithPath("threads.[].waitedCount")
|
||||
.description("Total number of times that the thread has waited" + " for notification."),
|
||||
fieldWithPath("threads.[].waitedTime")
|
||||
.description("Time in milliseconds that the thread has spent "
|
||||
+ "waiting. -1 if thread contention " + "monitoring is disabled"))));
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
void textThreadDump() {
|
||||
assertThat(this.mvc.get().uri("/actuator/threaddump").accept(MediaType.TEXT_PLAIN)).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("threaddump/text",
|
||||
preprocessResponse(new ContentModifyingOperationPreprocessor((bytes, mediaType) -> {
|
||||
String content = new String(bytes, StandardCharsets.UTF_8);
|
||||
int mainThreadIndex = content.indexOf("\"main\" - Thread");
|
||||
String truncatedContent = (mainThreadIndex >= 0) ? content.substring(0, mainThreadIndex)
|
||||
: content;
|
||||
return truncatedContent.getBytes();
|
||||
}))));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
ThreadDumpEndpoint endpoint() {
|
||||
return new ThreadDumpEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.Statistic;
|
||||
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.metrics.MetricsEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link MetricsEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class MetricsEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void metricNames() {
|
||||
assertThat(this.mvc.get().uri("/actuator/metrics")).hasStatusOk()
|
||||
.apply(document("metrics/names",
|
||||
responseFields(fieldWithPath("names").description("Names of the known metrics."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void metric() {
|
||||
assertThat(this.mvc.get().uri("/actuator/metrics/jvm.memory.max")).hasStatusOk()
|
||||
.apply(document("metrics/metric",
|
||||
responseFields(fieldWithPath("name").description("Name of the metric"),
|
||||
fieldWithPath("description").description("Description of the metric"),
|
||||
fieldWithPath("baseUnit").description("Base unit of the metric"),
|
||||
fieldWithPath("measurements").description("Measurements of the metric"),
|
||||
fieldWithPath("measurements[].statistic").description(
|
||||
"Statistic of the measurement. (" + describeEnumValues(Statistic.class) + ")."),
|
||||
fieldWithPath("measurements[].value").description("Value of the measurement."),
|
||||
fieldWithPath("availableTags").description("Tags that are available for drill-down."),
|
||||
fieldWithPath("availableTags[].tag").description("Name of the tag."),
|
||||
fieldWithPath("availableTags[].values").description("Possible values of the tag."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void metricWithTags() {
|
||||
assertThat(this.mvc.get()
|
||||
.uri("/actuator/metrics/jvm.memory.max")
|
||||
.param("tag", "area:nonheap")
|
||||
.param("tag", "id:Compressed Class Space")).hasStatusOk()
|
||||
.apply(document("metrics/metric-with-tags", queryParameters(
|
||||
parameterWithName("tag").description("A tag to use for drill-down in the form `name:value`."))));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
MetricsEndpoint endpoint() {
|
||||
SimpleMeterRegistry registry = new SimpleMeterRegistry();
|
||||
new JvmMemoryMetrics().bindTo(registry);
|
||||
return new MetricsEndpoint(registry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.metrics.export.prometheus;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import io.micrometer.core.instrument.Clock;
|
||||
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
|
||||
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
|
||||
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
|
||||
import io.prometheus.metrics.model.registry.PrometheusRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link PrometheusScrapeEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Johnny Lim
|
||||
*/
|
||||
class PrometheusScrapeEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void prometheus() {
|
||||
assertThat(this.mvc.get().uri("/actuator/prometheus")).hasStatusOk().apply(document("prometheus/all"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void prometheusOpenmetrics() {
|
||||
assertThat(this.mvc.get().uri("/actuator/prometheus").accept(OpenMetricsTextFormatWriter.CONTENT_TYPE))
|
||||
.satisfies((result) -> {
|
||||
assertThat(result).hasStatusOk()
|
||||
.headers()
|
||||
.hasValue("Content-Type", "application/openmetrics-text;version=1.0.0;charset=utf-8");
|
||||
assertThat(result).apply(document("prometheus/openmetrics"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void filteredPrometheus() {
|
||||
assertThat(this.mvc.get()
|
||||
.uri("/actuator/prometheus")
|
||||
.param("includedNames", "jvm_memory_used_bytes,jvm_memory_committed_bytes"))
|
||||
.hasStatusOk()
|
||||
.apply(document("prometheus/names",
|
||||
queryParameters(parameterWithName("includedNames")
|
||||
.description("Restricts the samples to those that match the names. Optional.")
|
||||
.optional())));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
PrometheusScrapeEndpoint endpoint() {
|
||||
PrometheusRegistry prometheusRegistry = new PrometheusRegistry();
|
||||
PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry((key) -> null, prometheusRegistry,
|
||||
Clock.SYSTEM);
|
||||
new JvmMemoryMetrics().bindTo(meterRegistry);
|
||||
return new PrometheusScrapeEndpoint(prometheusRegistry, new Properties());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.quartz;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.quartz.CalendarIntervalScheduleBuilder;
|
||||
import org.quartz.CalendarIntervalTrigger;
|
||||
import org.quartz.CronScheduleBuilder;
|
||||
import org.quartz.CronTrigger;
|
||||
import org.quartz.DailyTimeIntervalScheduleBuilder;
|
||||
import org.quartz.DailyTimeIntervalTrigger;
|
||||
import org.quartz.DateBuilder.IntervalUnit;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobBuilder;
|
||||
import org.quartz.JobDetail;
|
||||
import org.quartz.JobKey;
|
||||
import org.quartz.Scheduler;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.quartz.SimpleScheduleBuilder;
|
||||
import org.quartz.SimpleTrigger;
|
||||
import org.quartz.TimeOfDay;
|
||||
import org.quartz.Trigger;
|
||||
import org.quartz.Trigger.TriggerState;
|
||||
import org.quartz.TriggerBuilder;
|
||||
import org.quartz.TriggerKey;
|
||||
import org.quartz.impl.matchers.GroupMatcher;
|
||||
import org.quartz.spi.OperableTrigger;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.endpoint.Show;
|
||||
import org.springframework.boot.actuate.quartz.QuartzEndpoint;
|
||||
import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension;
|
||||
import org.springframework.boot.json.JsonWriter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.scheduling.quartz.DelegatingJob;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link QuartzEndpoint}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class QuartzEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
private static final TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
|
||||
|
||||
private static final JobDetail jobOne = JobBuilder.newJob(DelegatingJob.class)
|
||||
.withIdentity("jobOne", "samples")
|
||||
.withDescription("A sample job")
|
||||
.usingJobData("user", "admin")
|
||||
.usingJobData("password", "secret")
|
||||
.build();
|
||||
|
||||
private static final JobDetail jobTwo = JobBuilder.newJob(Job.class).withIdentity("jobTwo", "samples").build();
|
||||
|
||||
private static final JobDetail jobThree = JobBuilder.newJob(Job.class).withIdentity("jobThree", "tests").build();
|
||||
|
||||
private static final CronTrigger cronTrigger = TriggerBuilder.newTrigger()
|
||||
.forJob(jobOne)
|
||||
.withPriority(3)
|
||||
.withDescription("3AM on weekdays")
|
||||
.withIdentity("3am-weekdays", "samples")
|
||||
.withSchedule(CronScheduleBuilder.atHourAndMinuteOnGivenDaysOfWeek(3, 0, 1, 2, 3, 4, 5).inTimeZone(timeZone))
|
||||
.build();
|
||||
|
||||
private static final SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
|
||||
.forJob(jobOne)
|
||||
.withPriority(7)
|
||||
.withDescription("Once a day")
|
||||
.withIdentity("every-day", "samples")
|
||||
.withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24))
|
||||
.build();
|
||||
|
||||
private static final CalendarIntervalTrigger calendarIntervalTrigger = TriggerBuilder.newTrigger()
|
||||
.forJob(jobTwo)
|
||||
.withDescription("Once a week")
|
||||
.withIdentity("once-a-week", "samples")
|
||||
.withSchedule(
|
||||
CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1).inTimeZone(timeZone))
|
||||
.build();
|
||||
|
||||
private static final DailyTimeIntervalTrigger dailyTimeIntervalTrigger = TriggerBuilder.newTrigger()
|
||||
.forJob(jobThree)
|
||||
.withDescription("Every hour between 9AM and 6PM on Tuesday and Thursday")
|
||||
.withIdentity("every-hour-tue-thu")
|
||||
.withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
|
||||
.onDaysOfTheWeek(Calendar.TUESDAY, Calendar.THURSDAY)
|
||||
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0))
|
||||
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 0))
|
||||
.withInterval(1, IntervalUnit.HOUR))
|
||||
.build();
|
||||
|
||||
private static final List<FieldDescriptor> triggerSummary = List.of(previousFireTime(""), nextFireTime(""),
|
||||
priority(""));
|
||||
|
||||
private static final List<FieldDescriptor> cronTriggerSummary = List.of(
|
||||
fieldWithPath("expression").description("Cron expression to use."),
|
||||
fieldWithPath("timeZone").type(JsonFieldType.STRING)
|
||||
.optional()
|
||||
.description("Time zone for which the expression will be resolved, if any."));
|
||||
|
||||
private static final List<FieldDescriptor> simpleTriggerSummary = Collections
|
||||
.singletonList(fieldWithPath("interval").description("Interval, in milliseconds, between two executions."));
|
||||
|
||||
private static final List<FieldDescriptor> dailyTimeIntervalTriggerSummary = Arrays
|
||||
.asList(fieldWithPath("interval").description(
|
||||
"Interval, in milliseconds, added to the fire time in order to calculate the time of the next trigger repeat."),
|
||||
fieldWithPath("daysOfWeek").type(JsonFieldType.ARRAY)
|
||||
.description("An array of days of the week upon which to fire."),
|
||||
fieldWithPath("startTimeOfDay").type(JsonFieldType.STRING)
|
||||
.description("Time of day to start firing at the given interval, if any."),
|
||||
fieldWithPath("endTimeOfDay").type(JsonFieldType.STRING)
|
||||
.description("Time of day to complete firing at the given interval, if any."));
|
||||
|
||||
private static final List<FieldDescriptor> calendarIntervalTriggerSummary = Arrays
|
||||
.asList(fieldWithPath("interval").description(
|
||||
"Interval, in milliseconds, added to the fire time in order to calculate the time of the next trigger repeat."),
|
||||
fieldWithPath("timeZone").type(JsonFieldType.STRING)
|
||||
.description("Time zone within which time calculations will be performed, if any."));
|
||||
|
||||
private static final List<FieldDescriptor> customTriggerSummary = List
|
||||
.of(fieldWithPath("trigger").description("A toString representation of the custom trigger instance."));
|
||||
|
||||
private static final FieldDescriptor[] commonCronDetails = new FieldDescriptor[] {
|
||||
fieldWithPath("group").description("Name of the group."),
|
||||
fieldWithPath("name").description("Name of the trigger."),
|
||||
fieldWithPath("description").description("Description of the trigger, if any."),
|
||||
fieldWithPath("state")
|
||||
.description("State of the trigger (" + describeEnumValues(TriggerState.class) + ")."),
|
||||
fieldWithPath("type").description(
|
||||
"Type of the trigger (`calendarInterval`, `cron`, `custom`, `dailyTimeInterval`, `simple`). "
|
||||
+ "Determines the key of the object containing type-specific details."),
|
||||
fieldWithPath("calendarName").description("Name of the Calendar associated with this Trigger, if any."),
|
||||
startTime(""), endTime(""), previousFireTime(""), nextFireTime(""), priority(""),
|
||||
fieldWithPath("finalFireTime").optional()
|
||||
.type(JsonFieldType.STRING)
|
||||
.description("Last time at which the Trigger will fire, if any."),
|
||||
fieldWithPath("data").optional()
|
||||
.type(JsonFieldType.OBJECT)
|
||||
.description("Job data map keyed by name, if any.") };
|
||||
|
||||
@MockitoBean
|
||||
private Scheduler scheduler;
|
||||
|
||||
@Test
|
||||
void quartzReport() throws Exception {
|
||||
mockJobs(jobOne, jobTwo, jobThree);
|
||||
mockTriggers(cronTrigger, simpleTrigger, calendarIntervalTrigger, dailyTimeIntervalTrigger);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz")).hasStatusOk()
|
||||
.apply(document("quartz/report",
|
||||
responseFields(fieldWithPath("jobs.groups").description("An array of job group names."),
|
||||
fieldWithPath("triggers.groups").description("An array of trigger group names."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzJobs() throws Exception {
|
||||
mockJobs(jobOne, jobTwo, jobThree);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/jobs")).hasStatusOk()
|
||||
.apply(document("quartz/jobs",
|
||||
responseFields(fieldWithPath("groups").description("Job groups keyed by name."),
|
||||
fieldWithPath("groups.*.jobs").description("An array of job names."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggers() throws Exception {
|
||||
mockTriggers(cronTrigger, simpleTrigger, calendarIntervalTrigger, dailyTimeIntervalTrigger);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/triggers")).hasStatusOk()
|
||||
.apply(document("quartz/triggers",
|
||||
responseFields(fieldWithPath("groups").description("Trigger groups keyed by name."),
|
||||
fieldWithPath("groups.*.paused").description("Whether this trigger group is paused."),
|
||||
fieldWithPath("groups.*.triggers").description("An array of trigger names."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzJobGroup() throws Exception {
|
||||
mockJobs(jobOne, jobTwo, jobThree);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/jobs/samples")).hasStatusOk()
|
||||
.apply(document("quartz/job-group", responseFields(fieldWithPath("group").description("Name of the group."),
|
||||
fieldWithPath("jobs").description("Job details keyed by name."),
|
||||
fieldWithPath("jobs.*.className").description("Fully qualified name of the job implementation."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggerGroup() throws Exception {
|
||||
CronTrigger cron = cronTrigger.getTriggerBuilder()
|
||||
.startAt(fromUtc("2020-11-30T17:00:00Z"))
|
||||
.endAt(fromUtc("2020-12-30T03:00:00Z"))
|
||||
.withIdentity("3am-week", "tests")
|
||||
.build();
|
||||
setPreviousNextFireTime(cron, "2020-12-04T03:00:00Z", "2020-12-07T03:00:00Z");
|
||||
SimpleTrigger simple = simpleTrigger.getTriggerBuilder().withIdentity("every-day", "tests").build();
|
||||
setPreviousNextFireTime(simple, null, "2020-12-04T12:00:00Z");
|
||||
CalendarIntervalTrigger calendarInterval = calendarIntervalTrigger.getTriggerBuilder()
|
||||
.withIdentity("once-a-week", "tests")
|
||||
.startAt(fromUtc("2019-07-10T14:00:00Z"))
|
||||
.endAt(fromUtc("2023-01-01T12:00:00Z"))
|
||||
.build();
|
||||
setPreviousNextFireTime(calendarInterval, "2020-12-02T14:00:00Z", "2020-12-08T14:00:00Z");
|
||||
DailyTimeIntervalTrigger tueThuTrigger = dailyTimeIntervalTrigger.getTriggerBuilder()
|
||||
.withIdentity("tue-thu", "tests")
|
||||
.build();
|
||||
Trigger customTrigger = mock(Trigger.class);
|
||||
given(customTrigger.getKey()).willReturn(TriggerKey.triggerKey("once-a-year-custom", "tests"));
|
||||
given(customTrigger.toString()).willReturn("com.example.CustomTrigger@fdsfsd");
|
||||
given(customTrigger.getPriority()).willReturn(10);
|
||||
given(customTrigger.getPreviousFireTime()).willReturn(fromUtc("2020-07-14T16:00:00Z"));
|
||||
given(customTrigger.getNextFireTime()).willReturn(fromUtc("2021-07-14T16:00:00Z"));
|
||||
mockTriggers(cron, simple, calendarInterval, tueThuTrigger, customTrigger);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/triggers/tests")).hasStatusOk()
|
||||
.apply(document("quartz/trigger-group",
|
||||
responseFields(fieldWithPath("group").description("Name of the group."),
|
||||
fieldWithPath("paused").description("Whether the group is paused."),
|
||||
fieldWithPath("triggers.cron").description("Cron triggers keyed by name, if any."),
|
||||
fieldWithPath("triggers.simple").description("Simple triggers keyed by name, if any."),
|
||||
fieldWithPath("triggers.dailyTimeInterval")
|
||||
.description("Daily time interval triggers keyed by name, if any."),
|
||||
fieldWithPath("triggers.calendarInterval")
|
||||
.description("Calendar interval triggers keyed by name, if any."),
|
||||
fieldWithPath("triggers.custom").description("Any other triggers keyed by name, if any."))
|
||||
.andWithPrefix("triggers.cron.*.", concat(triggerSummary, cronTriggerSummary))
|
||||
.andWithPrefix("triggers.simple.*.", concat(triggerSummary, simpleTriggerSummary))
|
||||
.andWithPrefix("triggers.dailyTimeInterval.*.",
|
||||
concat(triggerSummary, dailyTimeIntervalTriggerSummary))
|
||||
.andWithPrefix("triggers.calendarInterval.*.",
|
||||
concat(triggerSummary, calendarIntervalTriggerSummary))
|
||||
.andWithPrefix("triggers.custom.*.", concat(triggerSummary, customTriggerSummary))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzJob() throws Exception {
|
||||
mockJobs(jobOne);
|
||||
CronTrigger firstTrigger = cronTrigger.getTriggerBuilder().build();
|
||||
setPreviousNextFireTime(firstTrigger, null, "2020-12-07T03:00:00Z");
|
||||
SimpleTrigger secondTrigger = simpleTrigger.getTriggerBuilder().build();
|
||||
setPreviousNextFireTime(secondTrigger, "2020-12-04T03:00:00Z", "2020-12-04T12:00:00Z");
|
||||
mockTriggers(firstTrigger, secondTrigger);
|
||||
given(this.scheduler.getTriggersOfJob(jobOne.getKey()))
|
||||
.willAnswer((invocation) -> List.of(firstTrigger, secondTrigger));
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/jobs/samples/jobOne")).hasStatusOk()
|
||||
.apply(document("quartz/job-details", responseFields(
|
||||
fieldWithPath("group").description("Name of the group."),
|
||||
fieldWithPath("name").description("Name of the job."),
|
||||
fieldWithPath("description").description("Description of the job, if any."),
|
||||
fieldWithPath("className").description("Fully qualified name of the job implementation."),
|
||||
fieldWithPath("durable").description("Whether the job should remain stored after it is orphaned."),
|
||||
fieldWithPath("requestRecovery").description(
|
||||
"Whether the job should be re-executed if a 'recovery' or 'fail-over' situation is encountered."),
|
||||
fieldWithPath("data.*").description("Job data map as key/value pairs, if any."),
|
||||
fieldWithPath("triggers").description("An array of triggers associated to the job, if any."),
|
||||
fieldWithPath("triggers.[].group").description("Name of the trigger group."),
|
||||
fieldWithPath("triggers.[].name").description("Name of the trigger."),
|
||||
previousFireTime("triggers.[]."), nextFireTime("triggers.[]."), priority("triggers.[]."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggerCommon() throws Exception {
|
||||
setupTriggerDetails(cronTrigger.getTriggerBuilder(), TriggerState.NORMAL);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/triggers/samples/example")).hasStatusOk()
|
||||
.apply(document("quartz/trigger-details-common", responseFields(commonCronDetails).and(subsectionWithPath(
|
||||
"calendarInterval")
|
||||
.description(
|
||||
"Calendar time interval trigger details, if any. Present when `type` is `calendarInterval`.")
|
||||
.optional()
|
||||
.type(JsonFieldType.OBJECT),
|
||||
subsectionWithPath("custom")
|
||||
.description("Custom trigger details, if any. Present when `type` is `custom`.")
|
||||
.optional()
|
||||
.type(JsonFieldType.OBJECT),
|
||||
subsectionWithPath("cron")
|
||||
.description("Cron trigger details, if any. Present when `type` is `cron`.")
|
||||
.optional()
|
||||
.type(JsonFieldType.OBJECT),
|
||||
subsectionWithPath("dailyTimeInterval").description(
|
||||
"Daily time interval trigger details, if any. Present when `type` is `dailyTimeInterval`.")
|
||||
.optional()
|
||||
.type(JsonFieldType.OBJECT),
|
||||
subsectionWithPath("simple")
|
||||
.description("Simple trigger details, if any. Present when `type` is `simple`.")
|
||||
.optional()
|
||||
.type(JsonFieldType.OBJECT))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggerCron() throws Exception {
|
||||
setupTriggerDetails(cronTrigger.getTriggerBuilder(), TriggerState.NORMAL);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/triggers/samples/example")).hasStatusOk()
|
||||
.apply(document("quartz/trigger-details-cron",
|
||||
relaxedResponseFields(fieldWithPath("cron").description("Cron trigger specific details."))
|
||||
.andWithPrefix("cron.", cronTriggerSummary)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggerSimple() throws Exception {
|
||||
setupTriggerDetails(simpleTrigger.getTriggerBuilder(), TriggerState.NORMAL);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/triggers/samples/example")).hasStatusOk()
|
||||
.apply(document("quartz/trigger-details-simple",
|
||||
relaxedResponseFields(fieldWithPath("simple").description("Simple trigger specific details."))
|
||||
.andWithPrefix("simple.", simpleTriggerSummary)
|
||||
.and(repeatCount("simple."), timesTriggered("simple."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggerCalendarInterval() throws Exception {
|
||||
setupTriggerDetails(calendarIntervalTrigger.getTriggerBuilder(), TriggerState.NORMAL);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/triggers/samples/example")).hasStatusOk()
|
||||
.apply(document("quartz/trigger-details-calendar-interval",
|
||||
relaxedResponseFields(fieldWithPath("calendarInterval")
|
||||
.description("Calendar interval trigger specific details."))
|
||||
.andWithPrefix("calendarInterval.", calendarIntervalTriggerSummary)
|
||||
.and(timesTriggered("calendarInterval."),
|
||||
fieldWithPath("calendarInterval.preserveHourOfDayAcrossDaylightSavings").description(
|
||||
"Whether to fire the trigger at the same time of day, regardless of daylight "
|
||||
+ "saving time transitions."),
|
||||
fieldWithPath("calendarInterval.skipDayIfHourDoesNotExist").description(
|
||||
"Whether to skip if the hour of the day does not exist on a given day."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggerDailyTimeInterval() throws Exception {
|
||||
setupTriggerDetails(dailyTimeIntervalTrigger.getTriggerBuilder(), TriggerState.PAUSED);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/triggers/samples/example")).hasStatusOk()
|
||||
.apply(document("quartz/trigger-details-daily-time-interval",
|
||||
relaxedResponseFields(fieldWithPath("dailyTimeInterval")
|
||||
.description("Daily time interval trigger specific details."))
|
||||
.andWithPrefix("dailyTimeInterval.", dailyTimeIntervalTriggerSummary)
|
||||
.and(repeatCount("dailyTimeInterval."), timesTriggered("dailyTimeInterval."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggerCustom() throws Exception {
|
||||
Trigger trigger = mock(Trigger.class);
|
||||
given(trigger.getKey()).willReturn(TriggerKey.triggerKey("example", "samples"));
|
||||
given(trigger.getDescription()).willReturn("Example trigger.");
|
||||
given(trigger.toString()).willReturn("com.example.CustomTrigger@fdsfsd");
|
||||
given(trigger.getPriority()).willReturn(10);
|
||||
given(trigger.getStartTime()).willReturn(fromUtc("2020-11-30T17:00:00Z"));
|
||||
given(trigger.getEndTime()).willReturn(fromUtc("2020-12-30T03:00:00Z"));
|
||||
given(trigger.getCalendarName()).willReturn("bankHolidays");
|
||||
given(trigger.getPreviousFireTime()).willReturn(fromUtc("2020-12-04T03:00:00Z"));
|
||||
given(trigger.getNextFireTime()).willReturn(fromUtc("2020-12-07T03:00:00Z"));
|
||||
given(this.scheduler.getTriggerState(trigger.getKey())).willReturn(TriggerState.NORMAL);
|
||||
mockTriggers(trigger);
|
||||
assertThat(this.mvc.get().uri("/actuator/quartz/triggers/samples/example")).hasStatusOk()
|
||||
.apply(document("quartz/trigger-details-custom",
|
||||
relaxedResponseFields(fieldWithPath("custom").description("Custom trigger specific details."))
|
||||
.andWithPrefix("custom.", customTriggerSummary)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzTriggerJob() throws Exception {
|
||||
mockJobs(jobOne);
|
||||
String json = JsonWriter.standard().writeToString(Map.of("state", "running"));
|
||||
assertThat(this.mvc.post()
|
||||
.content(json)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.uri("/actuator/quartz/jobs/samples/jobOne"))
|
||||
.hasStatusOk()
|
||||
.apply(document("quartz/trigger-job", preprocessRequest(), preprocessResponse(prettyPrint()),
|
||||
requestFields(fieldWithPath("state").description("The desired state of the job.")),
|
||||
responseFields(fieldWithPath("group").description("Name of the group."),
|
||||
fieldWithPath("name").description("Name of the job."),
|
||||
fieldWithPath("className").description("Fully qualified name of the job implementation."),
|
||||
fieldWithPath("triggerTime").description("Time the job is triggered."))));
|
||||
}
|
||||
|
||||
private <T extends Trigger> void setupTriggerDetails(TriggerBuilder<T> builder, TriggerState state)
|
||||
throws SchedulerException {
|
||||
T trigger = builder.withIdentity("example", "samples")
|
||||
.withDescription("Example trigger")
|
||||
.startAt(fromUtc("2020-11-30T17:00:00Z"))
|
||||
.modifiedByCalendar("bankHolidays")
|
||||
.endAt(fromUtc("2020-12-30T03:00:00Z"))
|
||||
.build();
|
||||
setPreviousNextFireTime(trigger, "2020-12-04T03:00:00Z", "2020-12-07T03:00:00Z");
|
||||
given(this.scheduler.getTriggerState(trigger.getKey())).willReturn(state);
|
||||
mockTriggers(trigger);
|
||||
}
|
||||
|
||||
private static FieldDescriptor startTime(String prefix) {
|
||||
return fieldWithPath(prefix + "startTime").description("Time at which the Trigger should take effect, if any.");
|
||||
}
|
||||
|
||||
private static FieldDescriptor endTime(String prefix) {
|
||||
return fieldWithPath(prefix + "endTime").description(
|
||||
"Time at which the Trigger should quit repeating, regardless of any remaining repeats, if any.");
|
||||
}
|
||||
|
||||
private static FieldDescriptor previousFireTime(String prefix) {
|
||||
return fieldWithPath(prefix + "previousFireTime").optional()
|
||||
.type(JsonFieldType.STRING)
|
||||
.description("Last time the trigger fired, if any.");
|
||||
}
|
||||
|
||||
private static FieldDescriptor nextFireTime(String prefix) {
|
||||
return fieldWithPath(prefix + "nextFireTime").optional()
|
||||
.type(JsonFieldType.STRING)
|
||||
.description("Next time at which the Trigger is scheduled to fire, if any.");
|
||||
}
|
||||
|
||||
private static FieldDescriptor priority(String prefix) {
|
||||
return fieldWithPath(prefix + "priority")
|
||||
.description("Priority to use if two triggers have the same scheduled fire time.");
|
||||
}
|
||||
|
||||
private static FieldDescriptor repeatCount(String prefix) {
|
||||
return fieldWithPath(prefix + "repeatCount")
|
||||
.description("Number of times the trigger should repeat, or -1 to repeat indefinitely.");
|
||||
}
|
||||
|
||||
private static FieldDescriptor timesTriggered(String prefix) {
|
||||
return fieldWithPath(prefix + "timesTriggered").description("Number of times the trigger has already fired.");
|
||||
}
|
||||
|
||||
private static List<FieldDescriptor> concat(List<FieldDescriptor> initial, List<FieldDescriptor> additionalFields) {
|
||||
List<FieldDescriptor> result = new ArrayList<>(initial);
|
||||
result.addAll(additionalFields);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void mockJobs(JobDetail... jobs) throws SchedulerException {
|
||||
MultiValueMap<String, JobKey> jobKeys = new LinkedMultiValueMap<>();
|
||||
for (JobDetail jobDetail : jobs) {
|
||||
JobKey key = jobDetail.getKey();
|
||||
given(this.scheduler.getJobDetail(key)).willReturn(jobDetail);
|
||||
jobKeys.add(key.getGroup(), key);
|
||||
}
|
||||
given(this.scheduler.getJobGroupNames()).willReturn(new ArrayList<>(jobKeys.keySet()));
|
||||
for (Entry<String, List<JobKey>> entry : jobKeys.entrySet()) {
|
||||
given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(entry.getKey())))
|
||||
.willReturn(new LinkedHashSet<>(entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
private void mockTriggers(Trigger... triggers) throws SchedulerException {
|
||||
MultiValueMap<String, TriggerKey> triggerKeys = new LinkedMultiValueMap<>();
|
||||
for (Trigger trigger : triggers) {
|
||||
TriggerKey key = trigger.getKey();
|
||||
given(this.scheduler.getTrigger(key)).willReturn(trigger);
|
||||
triggerKeys.add(key.getGroup(), key);
|
||||
}
|
||||
given(this.scheduler.getTriggerGroupNames()).willReturn(new ArrayList<>(triggerKeys.keySet()));
|
||||
for (Entry<String, List<TriggerKey>> entry : triggerKeys.entrySet()) {
|
||||
given(this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(entry.getKey())))
|
||||
.willReturn(new LinkedHashSet<>(entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Trigger> void setPreviousNextFireTime(T trigger, String previousFireTime, String nextFireTime) {
|
||||
OperableTrigger operableTrigger = (OperableTrigger) trigger;
|
||||
if (previousFireTime != null) {
|
||||
operableTrigger.setPreviousFireTime(fromUtc(previousFireTime));
|
||||
}
|
||||
if (nextFireTime != null) {
|
||||
operableTrigger.setNextFireTime(fromUtc(nextFireTime));
|
||||
}
|
||||
}
|
||||
|
||||
private static Date fromUtc(String utcTime) {
|
||||
return Date.from(Instant.parse(utcTime));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
QuartzEndpoint endpoint(Scheduler scheduler) {
|
||||
return new QuartzEndpoint(scheduler, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
QuartzEndpointWebExtension endpointWebExtension(QuartzEndpoint endpoint) {
|
||||
return new QuartzEndpointWebExtension(endpoint, Show.ALWAYS, Collections.emptySet());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.sbom;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.sbom.SbomEndpoint;
|
||||
import org.springframework.boot.actuate.sbom.SbomEndpointWebExtension;
|
||||
import org.springframework.boot.actuate.sbom.SbomProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link SbomEndpoint}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SbomEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void sbom() {
|
||||
assertThat(this.mvc.get().uri("/actuator/sbom")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("sbom",
|
||||
responseFields(fieldWithPath("ids").description("An array of available SBOM ids."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sboms() {
|
||||
assertThat(this.mvc.get().uri("/actuator/sbom/application")).hasStatusOk()
|
||||
.apply(MockMvcRestDocumentation.document("sbom/id"));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
SbomProperties sbomProperties() {
|
||||
SbomProperties properties = new SbomProperties();
|
||||
properties.getApplication()
|
||||
.setLocation("classpath:org/springframework/boot/actuate/docs/sbom/cyclonedx.json");
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpoint endpoint(SbomProperties properties, ResourceLoader resourceLoader) {
|
||||
return new SbomEndpoint(properties, resourceLoader);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpointWebExtension sbomEndpointWebExtension(SbomEndpoint endpoint, SbomProperties properties) {
|
||||
return new SbomEndpointWebExtension(endpoint, properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.scheduling;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.scheduling.Trigger;
|
||||
import org.springframework.scheduling.TriggerContext;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.scheduling.annotation.SchedulingConfigurer;
|
||||
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
|
||||
import org.springframework.scheduling.config.ScheduledTaskHolder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link ScheduledTasksEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
void scheduledTasks() {
|
||||
assertThat(this.mvc.get().uri("/actuator/scheduledtasks")).hasStatusOk()
|
||||
.apply(document("scheduled-tasks",
|
||||
preprocessResponse(replacePattern(
|
||||
Pattern.compile("org.*\\.ScheduledTasksEndpointDocumentationTests\\$TestConfiguration"),
|
||||
"com.example.Processor")),
|
||||
responseFields(fieldWithPath("cron").description("Cron tasks, if any."),
|
||||
targetFieldWithPrefix("cron.[]."),
|
||||
nextExecutionWithPrefix("cron.[].").description("Time of the next scheduled execution."),
|
||||
fieldWithPath("cron.[].expression").description("Cron expression."),
|
||||
fieldWithPath("fixedDelay").description("Fixed delay tasks, if any."),
|
||||
targetFieldWithPrefix("fixedDelay.[]."), initialDelayWithPrefix("fixedDelay.[]."),
|
||||
nextExecutionWithPrefix("fixedDelay.[]."),
|
||||
fieldWithPath("fixedDelay.[].interval")
|
||||
.description("Interval, in milliseconds, between the end of the last"
|
||||
+ " execution and the start of the next."),
|
||||
fieldWithPath("fixedRate").description("Fixed rate tasks, if any."),
|
||||
targetFieldWithPrefix("fixedRate.[]."),
|
||||
fieldWithPath("fixedRate.[].interval")
|
||||
.description("Interval, in milliseconds, between the start of each execution."),
|
||||
initialDelayWithPrefix("fixedRate.[]."), nextExecutionWithPrefix("fixedRate.[]."),
|
||||
fieldWithPath("custom").description("Tasks with custom triggers, if any."),
|
||||
targetFieldWithPrefix("custom.[]."),
|
||||
fieldWithPath("custom.[].trigger").description("Trigger for the task."))
|
||||
.andWithPrefix("*.[].",
|
||||
fieldWithPath("lastExecution").description("Last execution of this task, if any.")
|
||||
.optional()
|
||||
.type(JsonFieldType.OBJECT))
|
||||
.andWithPrefix("*.[].lastExecution.", lastExecution())));
|
||||
}
|
||||
|
||||
private FieldDescriptor targetFieldWithPrefix(String prefix) {
|
||||
return fieldWithPath(prefix + "runnable.target").description("Target that will be executed.");
|
||||
}
|
||||
|
||||
private FieldDescriptor initialDelayWithPrefix(String prefix) {
|
||||
return fieldWithPath(prefix + "initialDelay").description("Delay, in milliseconds, before first execution.");
|
||||
}
|
||||
|
||||
private FieldDescriptor nextExecutionWithPrefix(String prefix) {
|
||||
return fieldWithPath(prefix + "nextExecution.time")
|
||||
.description("Time of the next scheduled execution, if known.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional();
|
||||
}
|
||||
|
||||
private FieldDescriptor[] lastExecution() {
|
||||
return new FieldDescriptor[] {
|
||||
fieldWithPath("status").description("Status of the last execution (STARTED, SUCCESS, ERROR).")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("time").description("Time of the last execution.").type(JsonFieldType.STRING),
|
||||
fieldWithPath("exception.type").description("Exception type thrown by the task, if any.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional(),
|
||||
fieldWithPath("exception.message").description("Message of the exception thrown by the task, if any.")
|
||||
.type(JsonFieldType.STRING)
|
||||
.optional() };
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableScheduling
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
ScheduledTasksEndpoint endpoint(Collection<ScheduledTaskHolder> holders) {
|
||||
return new ScheduledTasksEndpoint(holders);
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 0/3 1/1 * ?")
|
||||
void processOrders() {
|
||||
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 5000, initialDelay = 0)
|
||||
void purge() {
|
||||
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 3000, initialDelay = 10000)
|
||||
void retrieveIssues() {
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
SchedulingConfigurer schedulingConfigurer() {
|
||||
return (registrar) -> {
|
||||
registrar.setTaskScheduler(new TestTaskScheduler());
|
||||
registrar.addTriggerTask(new CustomTriggeredRunnable(), new CustomTrigger());
|
||||
};
|
||||
}
|
||||
|
||||
static class CustomTrigger implements Trigger {
|
||||
|
||||
@Override
|
||||
public Instant nextExecution(TriggerContext triggerContext) {
|
||||
return Instant.now();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomTriggeredRunnable implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
throw new IllegalStateException("Failed while running custom task");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestTaskScheduler extends SimpleAsyncTaskScheduler {
|
||||
|
||||
TestTaskScheduler() {
|
||||
setThreadNamePrefix("test-");
|
||||
// do not log task errors
|
||||
setErrorHandler((throwable) -> {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.session;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.context.ShutdownEndpoint;
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.session.SessionsEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
|
||||
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing the {@link ShutdownEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@TestPropertySource(properties = "spring.jackson.serialization.write-dates-as-timestamps=false")
|
||||
class SessionsEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
private static final Session sessionOne = createSession(Instant.now().minusSeconds(60 * 60 * 12),
|
||||
Instant.now().minusSeconds(45));
|
||||
|
||||
private static final Session sessionTwo = createSession("4db5efcc-99cb-4d05-a52c-b49acfbb7ea9",
|
||||
Instant.now().minusSeconds(60 * 60 * 5), Instant.now().minusSeconds(37));
|
||||
|
||||
private static final Session sessionThree = createSession(Instant.now().minusSeconds(60 * 60 * 2),
|
||||
Instant.now().minusSeconds(12));
|
||||
|
||||
private static final List<FieldDescriptor> sessionFields = List.of(
|
||||
fieldWithPath("id").description("ID of the session."),
|
||||
fieldWithPath("attributeNames").description("Names of the attributes stored in the session."),
|
||||
fieldWithPath("creationTime").description("Timestamp of when the session was created."),
|
||||
fieldWithPath("lastAccessedTime").description("Timestamp of when the session was last accessed."),
|
||||
fieldWithPath("maxInactiveInterval")
|
||||
.description("Maximum permitted period of inactivity, in seconds, before the session will expire."),
|
||||
fieldWithPath("expired").description("Whether the session has expired."));
|
||||
|
||||
@MockitoBean
|
||||
private FindByIndexNameSessionRepository<Session> sessionRepository;
|
||||
|
||||
@Test
|
||||
void sessionsForUsername() {
|
||||
Map<String, Session> sessions = new HashMap<>();
|
||||
sessions.put(sessionOne.getId(), sessionOne);
|
||||
sessions.put(sessionTwo.getId(), sessionTwo);
|
||||
sessions.put(sessionThree.getId(), sessionThree);
|
||||
given(this.sessionRepository.findByPrincipalName("alice")).willReturn(sessions);
|
||||
assertThat(this.mvc.get().uri("/actuator/sessions").param("username", "alice")).hasStatusOk()
|
||||
.apply(document("sessions/username",
|
||||
responseFields(fieldWithPath("sessions").description("Sessions for the given username."))
|
||||
.andWithPrefix("sessions.[].", sessionFields),
|
||||
queryParameters(parameterWithName("username").description("Name of the user."))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionWithId() {
|
||||
given(this.sessionRepository.findById(sessionTwo.getId())).willReturn(sessionTwo);
|
||||
assertThat(this.mvc.get().uri("/actuator/sessions/{id}", sessionTwo.getId())).hasStatusOk()
|
||||
.apply(document("sessions/id", responseFields(sessionFields)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteASession() {
|
||||
assertThat(this.mvc.delete().uri("/actuator/sessions/{id}", sessionTwo.getId()))
|
||||
.hasStatus(HttpStatus.NO_CONTENT)
|
||||
.apply(document("sessions/delete"));
|
||||
then(this.sessionRepository).should().deleteById(sessionTwo.getId());
|
||||
}
|
||||
|
||||
private static MapSession createSession(Instant creationTime, Instant lastAccessedTime) {
|
||||
return createSession(UUID.randomUUID().toString(), creationTime, lastAccessedTime);
|
||||
}
|
||||
|
||||
private static MapSession createSession(String id, Instant creationTime, Instant lastAccessedTime) {
|
||||
MapSession session = new MapSession(id);
|
||||
session.setCreationTime(creationTime);
|
||||
session.setLastAccessedTime(lastAccessedTime);
|
||||
return session;
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
SessionsEndpoint endpoint(FindByIndexNameSessionRepository<?> sessionRepository) {
|
||||
return new SessionsEndpoint(sessionRepository, sessionRepository);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.startup;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.startup.StartupEndpoint;
|
||||
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.metrics.StartupStep;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.restdocs.payload.PayloadDocumentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing {@link StartupEndpoint}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@BeforeEach
|
||||
void appendSampleStartupSteps(@Autowired BufferingApplicationStartup applicationStartup) {
|
||||
StartupStep starting = applicationStartup.start("spring.boot.application.starting");
|
||||
starting.tag("mainApplicationClass", "com.example.startup.StartupApplication");
|
||||
StartupStep instantiate = applicationStartup.start("spring.beans.instantiate");
|
||||
instantiate.tag("beanName", "homeController");
|
||||
instantiate.end();
|
||||
starting.end();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startupSnapshot() {
|
||||
assertThat(this.mvc.get().uri("/actuator/startup")).hasStatusOk()
|
||||
.apply(document("startup-snapshot", PayloadDocumentation.responseFields(responseFields())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void startup() {
|
||||
assertThat(this.mvc.post().uri("/actuator/startup")).hasStatusOk()
|
||||
.apply(document("startup", PayloadDocumentation.responseFields(responseFields())));
|
||||
}
|
||||
|
||||
private FieldDescriptor[] responseFields() {
|
||||
return new FieldDescriptor[] {
|
||||
fieldWithPath("springBootVersion").type(JsonFieldType.STRING)
|
||||
.description("Spring Boot version for this application.")
|
||||
.optional(),
|
||||
fieldWithPath("timeline.startTime").description("Start time of the application."),
|
||||
fieldWithPath("timeline.events")
|
||||
.description("An array of steps collected during application startup so far."),
|
||||
fieldWithPath("timeline.events.[].startTime").description("The timestamp of the start of this event."),
|
||||
fieldWithPath("timeline.events.[].endTime").description("The timestamp of the end of this event."),
|
||||
fieldWithPath("timeline.events.[].duration").description("The precise duration of this event."),
|
||||
fieldWithPath("timeline.events.[].startupStep.name").description("The name of the StartupStep."),
|
||||
fieldWithPath("timeline.events.[].startupStep.id").description("The id of this StartupStep."),
|
||||
fieldWithPath("timeline.events.[].startupStep.parentId")
|
||||
.description("The parent id for this StartupStep.")
|
||||
.optional(),
|
||||
fieldWithPath("timeline.events.[].startupStep.tags")
|
||||
.description("An array of key/value pairs with additional step info."),
|
||||
fieldWithPath("timeline.events.[].startupStep.tags[].key")
|
||||
.description("The key of the StartupStep Tag."),
|
||||
fieldWithPath("timeline.events.[].startupStep.tags[].value")
|
||||
.description("The value of the StartupStep Tag.") };
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
StartupEndpoint startupEndpoint(BufferingApplicationStartup startup) {
|
||||
return new StartupEndpoint(startup);
|
||||
}
|
||||
|
||||
@Bean
|
||||
BufferingApplicationStartup bufferingApplicationStartup() {
|
||||
return new BufferingApplicationStartup(16);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.web.exchanges;
|
||||
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.docs.MockMvcEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
|
||||
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
|
||||
import org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint;
|
||||
import org.springframework.boot.actuate.web.exchanges.Include;
|
||||
import org.springframework.boot.actuate.web.exchanges.RecordableHttpRequest;
|
||||
import org.springframework.boot.actuate.web.exchanges.RecordableHttpResponse;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing {@link HttpExchangesEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class HttpExchangesEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@MockitoBean
|
||||
private HttpExchangeRepository repository;
|
||||
|
||||
@Test
|
||||
void httpExchanges() {
|
||||
RecordableHttpRequest request = mock(RecordableHttpRequest.class);
|
||||
given(request.getUri()).willReturn(URI.create("https://api.example.com"));
|
||||
given(request.getMethod()).willReturn("GET");
|
||||
given(request.getHeaders())
|
||||
.willReturn(Collections.singletonMap(HttpHeaders.ACCEPT, List.of("application/json")));
|
||||
RecordableHttpResponse response = mock(RecordableHttpResponse.class);
|
||||
given(response.getStatus()).willReturn(200);
|
||||
given(response.getHeaders())
|
||||
.willReturn(Collections.singletonMap(HttpHeaders.CONTENT_TYPE, List.of("application/json")));
|
||||
Principal principal = mock(Principal.class);
|
||||
given(principal.getName()).willReturn("alice");
|
||||
Instant instant = Instant.parse("2022-12-22T13:43:41.00Z");
|
||||
Clock start = Clock.fixed(instant, ZoneId.systemDefault());
|
||||
Clock end = Clock.offset(start, Duration.ofMillis(23));
|
||||
HttpExchange exchange = HttpExchange.start(start, request)
|
||||
.finish(end, response, () -> principal, () -> UUID.randomUUID().toString(), EnumSet.allOf(Include.class));
|
||||
given(this.repository.findAll()).willReturn(List.of(exchange));
|
||||
assertThat(this.mvc.get().uri("/actuator/httpexchanges")).hasStatusOk()
|
||||
.apply(document("httpexchanges", responseFields(
|
||||
fieldWithPath("exchanges").description("An array of HTTP request-response exchanges."),
|
||||
fieldWithPath("exchanges.[].timestamp").description("Timestamp of when the exchange occurred."),
|
||||
fieldWithPath("exchanges.[].principal").description("Principal of the exchange, if any.")
|
||||
.optional(),
|
||||
fieldWithPath("exchanges.[].principal.name").description("Name of the principal.").optional(),
|
||||
fieldWithPath("exchanges.[].request.method").description("HTTP method of the request."),
|
||||
fieldWithPath("exchanges.[].request.remoteAddress")
|
||||
.description("Remote address from which the request was received, if known.")
|
||||
.optional()
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("exchanges.[].request.uri").description("URI of the request."),
|
||||
fieldWithPath("exchanges.[].request.headers")
|
||||
.description("Headers of the request, keyed by header name."),
|
||||
fieldWithPath("exchanges.[].request.headers.*.[]").description("Values of the header"),
|
||||
fieldWithPath("exchanges.[].response.status").description("Status of the response"),
|
||||
fieldWithPath("exchanges.[].response.headers")
|
||||
.description("Headers of the response, keyed by header name."),
|
||||
fieldWithPath("exchanges.[].response.headers.*.[]").description("Values of the header"),
|
||||
fieldWithPath("exchanges.[].session").description("Session associated with the exchange, if any.")
|
||||
.optional(),
|
||||
fieldWithPath("exchanges.[].session.id").description("ID of the session."),
|
||||
fieldWithPath("exchanges.[].timeTaken").description("Time taken to handle the exchange."))));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
HttpExchangesEndpoint httpExchangesEndpoint(HttpExchangeRepository repository) {
|
||||
return new HttpExchangesEndpoint(repository);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.web.mappings;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.actuate.docs.AbstractEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.web.mappings.MappingDescriptionProvider;
|
||||
import org.springframework.boot.actuate.web.mappings.MappingsEndpoint;
|
||||
import org.springframework.boot.actuate.web.mappings.reactive.DispatcherHandlersMappingDescriptionProvider;
|
||||
import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.web.server.test.LocalServerPort;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.RestDocumentationContextProvider;
|
||||
import org.springframework.restdocs.RestDocumentationExtension;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
|
||||
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing {@link MappingsEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@ExtendWith(RestDocumentationExtension.class)
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.main.web-application-type=reactive")
|
||||
class MappingsEndpointReactiveDocumentationTests extends AbstractEndpointDocumentationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private WebTestClient client;
|
||||
|
||||
@BeforeEach
|
||||
void webTestClient(RestDocumentationContextProvider restDocumentation) {
|
||||
this.client = WebTestClient.bindToServer()
|
||||
.filter(documentationConfiguration(restDocumentation).snippets().withDefaults())
|
||||
.baseUrl("http://localhost:" + this.port)
|
||||
.responseTimeout(Duration.ofMinutes(5))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappings() {
|
||||
List<FieldDescriptor> requestMappingConditions = List.of(
|
||||
requestMappingConditionField("").description("Details of the request mapping conditions.").optional(),
|
||||
requestMappingConditionField(".consumes").description("Details of the consumes condition"),
|
||||
requestMappingConditionField(".consumes.[].mediaType").description("Consumed media type."),
|
||||
requestMappingConditionField(".consumes.[].negated").description("Whether the media type is negated."),
|
||||
requestMappingConditionField(".headers").description("Details of the headers condition."),
|
||||
requestMappingConditionField(".headers.[].name").description("Name of the header."),
|
||||
requestMappingConditionField(".headers.[].value").description("Required value of the header, if any."),
|
||||
requestMappingConditionField(".headers.[].negated").description("Whether the value is negated."),
|
||||
requestMappingConditionField(".methods").description("HTTP methods that are handled."),
|
||||
requestMappingConditionField(".params").description("Details of the params condition."),
|
||||
requestMappingConditionField(".params.[].name").description("Name of the parameter."),
|
||||
requestMappingConditionField(".params.[].value")
|
||||
.description("Required value of the parameter, if any."),
|
||||
requestMappingConditionField(".params.[].negated").description("Whether the value is negated."),
|
||||
requestMappingConditionField(".patterns")
|
||||
.description("Patterns identifying the paths handled by the mapping."),
|
||||
requestMappingConditionField(".produces").description("Details of the produces condition."),
|
||||
requestMappingConditionField(".produces.[].mediaType").description("Produced media type."),
|
||||
requestMappingConditionField(".produces.[].negated").description("Whether the media type is negated."));
|
||||
List<FieldDescriptor> handlerMethod = List.of(
|
||||
fieldWithPath("*.[].details.handlerMethod").optional()
|
||||
.type(JsonFieldType.OBJECT)
|
||||
.description("Details of the method, if any, that will handle requests to this mapping."),
|
||||
fieldWithPath("*.[].details.handlerMethod.className").type(JsonFieldType.STRING)
|
||||
.description("Fully qualified name of the class of the method."),
|
||||
fieldWithPath("*.[].details.handlerMethod.name").type(JsonFieldType.STRING)
|
||||
.description("Name of the method."),
|
||||
fieldWithPath("*.[].details.handlerMethod.descriptor").type(JsonFieldType.STRING)
|
||||
.description("Descriptor of the method as specified in the Java Language Specification."));
|
||||
List<FieldDescriptor> handlerFunction = List.of(
|
||||
fieldWithPath("*.[].details.handlerFunction").optional()
|
||||
.type(JsonFieldType.OBJECT)
|
||||
.description("Details of the function, if any, that will handle requests to this mapping."),
|
||||
fieldWithPath("*.[].details.handlerFunction.className").type(JsonFieldType.STRING)
|
||||
.description("Fully qualified name of the class of the function."));
|
||||
List<FieldDescriptor> dispatcherHandlerFields = new ArrayList<>(List.of(
|
||||
fieldWithPath("*")
|
||||
.description("Dispatcher handler mappings, if any, keyed by dispatcher handler bean name."),
|
||||
fieldWithPath("*.[].details").optional()
|
||||
.type(JsonFieldType.OBJECT)
|
||||
.description("Additional implementation-specific details about the mapping. Optional."),
|
||||
fieldWithPath("*.[].handler").description("Handler for the mapping."),
|
||||
fieldWithPath("*.[].predicate").description("Predicate for the mapping.")));
|
||||
dispatcherHandlerFields.addAll(requestMappingConditions);
|
||||
dispatcherHandlerFields.addAll(handlerMethod);
|
||||
dispatcherHandlerFields.addAll(handlerFunction);
|
||||
this.client.get()
|
||||
.uri("/actuator/mappings")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody()
|
||||
.consumeWith(document("mappings", responseFields(
|
||||
beneathPath("contexts.*.mappings.dispatcherHandlers").withSubsectionId("dispatcher-handlers"),
|
||||
dispatcherHandlerFields)));
|
||||
}
|
||||
|
||||
private FieldDescriptor requestMappingConditionField(String path) {
|
||||
return fieldWithPath("*.[].details.requestMappingConditions" + path);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
NettyReactiveWebServerFactory netty() {
|
||||
return new NettyReactiveWebServerFactory(0);
|
||||
}
|
||||
|
||||
@Bean
|
||||
DispatcherHandlersMappingDescriptionProvider dispatcherHandlersMappingDescriptionProvider() {
|
||||
return new DispatcherHandlersMappingDescriptionProvider();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MappingsEndpoint mappingsEndpoint(Collection<MappingDescriptionProvider> descriptionProviders,
|
||||
ConfigurableApplicationContext context) {
|
||||
return new MappingsEndpoint(descriptionProviders, context);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> exampleRouter() {
|
||||
return route(GET("/foo"), (request) -> ServerResponse.ok().build());
|
||||
}
|
||||
|
||||
@Bean
|
||||
ExampleController exampleController() {
|
||||
return new ExampleController();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class ExampleController {
|
||||
|
||||
@PostMapping(path = "/", consumes = { MediaType.APPLICATION_JSON_VALUE, "!application/xml" },
|
||||
produces = MediaType.TEXT_PLAIN_VALUE, headers = "X-Custom=Foo", params = "a!=alpha")
|
||||
String example() {
|
||||
return "Hello World";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.docs.web.mappings;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.actuate.docs.AbstractEndpointDocumentationTests;
|
||||
import org.springframework.boot.actuate.web.mappings.MappingDescriptionProvider;
|
||||
import org.springframework.boot.actuate.web.mappings.MappingsEndpoint;
|
||||
import org.springframework.boot.actuate.web.mappings.servlet.DispatcherServletsMappingDescriptionProvider;
|
||||
import org.springframework.boot.actuate.web.mappings.servlet.FiltersMappingDescriptionProvider;
|
||||
import org.springframework.boot.actuate.web.mappings.servlet.ServletsMappingDescriptionProvider;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.test.LocalServerPort;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.RestDocumentationContextProvider;
|
||||
import org.springframework.restdocs.RestDocumentationExtension;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.JsonFieldType;
|
||||
import org.springframework.restdocs.payload.ResponseFieldsSnippet;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunctions;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
|
||||
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
|
||||
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
|
||||
import static org.springframework.web.servlet.function.RequestPredicates.GET;
|
||||
|
||||
/**
|
||||
* Tests for generating documentation describing {@link MappingsEndpoint}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Xiong Tang
|
||||
*/
|
||||
@ExtendWith(RestDocumentationExtension.class)
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
class MappingsEndpointServletDocumentationTests extends AbstractEndpointDocumentationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private WebTestClient client;
|
||||
|
||||
@BeforeEach
|
||||
void webTestClient(RestDocumentationContextProvider restDocumentation) {
|
||||
this.client = WebTestClient.bindToServer()
|
||||
.filter(documentationConfiguration(restDocumentation))
|
||||
.baseUrl("http://localhost:" + this.port)
|
||||
.responseTimeout(Duration.ofMinutes(5))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappings() {
|
||||
ResponseFieldsSnippet commonResponseFields = responseFields(
|
||||
fieldWithPath("contexts").description("Application contexts keyed by id."),
|
||||
fieldWithPath("contexts.*.mappings").description("Mappings in the context, keyed by mapping type."),
|
||||
subsectionWithPath("contexts.*.mappings.dispatcherServlets")
|
||||
.description("Dispatcher servlet mappings, if any."),
|
||||
subsectionWithPath("contexts.*.mappings.servletFilters")
|
||||
.description("Servlet filter mappings, if any."),
|
||||
subsectionWithPath("contexts.*.mappings.servlets").description("Servlet mappings, if any."),
|
||||
subsectionWithPath("contexts.*.mappings.dispatcherHandlers")
|
||||
.description("Dispatcher handler mappings, if any.")
|
||||
.optional()
|
||||
.type(JsonFieldType.OBJECT),
|
||||
parentIdField());
|
||||
List<FieldDescriptor> dispatcherServletFields = new ArrayList<>(List.of(
|
||||
fieldWithPath("*")
|
||||
.description("Dispatcher servlet mappings, if any, keyed by dispatcher servlet bean name."),
|
||||
fieldWithPath("*.[].details").optional()
|
||||
.type(JsonFieldType.OBJECT)
|
||||
.description("Additional implementation-specific details about the mapping. Optional."),
|
||||
fieldWithPath("*.[].handler").description("Handler for the mapping."),
|
||||
fieldWithPath("*.[].predicate").description("Predicate for the mapping.")));
|
||||
List<FieldDescriptor> requestMappingConditions = List.of(
|
||||
requestMappingConditionField("").description("Details of the request mapping conditions.").optional(),
|
||||
requestMappingConditionField(".consumes").description("Details of the consumes condition"),
|
||||
requestMappingConditionField(".consumes.[].mediaType").description("Consumed media type."),
|
||||
requestMappingConditionField(".consumes.[].negated").description("Whether the media type is negated."),
|
||||
requestMappingConditionField(".headers").description("Details of the headers condition."),
|
||||
requestMappingConditionField(".headers.[].name").description("Name of the header."),
|
||||
requestMappingConditionField(".headers.[].value").description("Required value of the header, if any."),
|
||||
requestMappingConditionField(".headers.[].negated").description("Whether the value is negated."),
|
||||
requestMappingConditionField(".methods").description("HTTP methods that are handled."),
|
||||
requestMappingConditionField(".params").description("Details of the params condition."),
|
||||
requestMappingConditionField(".params.[].name").description("Name of the parameter."),
|
||||
requestMappingConditionField(".params.[].value")
|
||||
.description("Required value of the parameter, if any."),
|
||||
requestMappingConditionField(".params.[].negated").description("Whether the value is negated."),
|
||||
requestMappingConditionField(".patterns")
|
||||
.description("Patterns identifying the paths handled by the mapping."),
|
||||
requestMappingConditionField(".produces").description("Details of the produces condition."),
|
||||
requestMappingConditionField(".produces.[].mediaType").description("Produced media type."),
|
||||
requestMappingConditionField(".produces.[].negated").description("Whether the media type is negated."));
|
||||
List<FieldDescriptor> handlerMethod = List.of(
|
||||
fieldWithPath("*.[].details.handlerMethod").optional()
|
||||
.type(JsonFieldType.OBJECT)
|
||||
.description("Details of the method, if any, that will handle requests to this mapping."),
|
||||
fieldWithPath("*.[].details.handlerMethod.className")
|
||||
.description("Fully qualified name of the class of the method."),
|
||||
fieldWithPath("*.[].details.handlerMethod.name").description("Name of the method."),
|
||||
fieldWithPath("*.[].details.handlerMethod.descriptor")
|
||||
.description("Descriptor of the method as specified in the Java Language Specification."));
|
||||
List<FieldDescriptor> handlerFunction = List.of(
|
||||
fieldWithPath("*.[].details.handlerFunction").optional()
|
||||
.type(JsonFieldType.OBJECT)
|
||||
.description("Details of the function, if any, that will handle requests to this mapping."),
|
||||
fieldWithPath("*.[].details.handlerFunction.className").type(JsonFieldType.STRING)
|
||||
.description("Fully qualified name of the class of the function."));
|
||||
dispatcherServletFields.addAll(handlerFunction);
|
||||
dispatcherServletFields.addAll(handlerMethod);
|
||||
dispatcherServletFields.addAll(requestMappingConditions);
|
||||
this.client.get()
|
||||
.uri("/actuator/mappings")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.consumeWith(document("mappings", commonResponseFields,
|
||||
responseFields(beneathPath("contexts.*.mappings.dispatcherServlets")
|
||||
.withSubsectionId("dispatcher-servlets"), dispatcherServletFields),
|
||||
responseFields(
|
||||
beneathPath("contexts.*.mappings.servletFilters").withSubsectionId("servlet-filters"),
|
||||
fieldWithPath("[].servletNameMappings")
|
||||
.description("Names of the servlets to which the filter is mapped."),
|
||||
fieldWithPath("[].urlPatternMappings")
|
||||
.description("URL pattern to which the filter is mapped."),
|
||||
fieldWithPath("[].name").description("Name of the filter."),
|
||||
fieldWithPath("[].className").description("Class name of the filter")),
|
||||
responseFields(beneathPath("contexts.*.mappings.servlets").withSubsectionId("servlets"),
|
||||
fieldWithPath("[].mappings").description("Mappings of the servlet."),
|
||||
fieldWithPath("[].name").description("Name of the servlet."),
|
||||
fieldWithPath("[].className").description("Class name of the servlet"))));
|
||||
}
|
||||
|
||||
private FieldDescriptor requestMappingConditionField(String path) {
|
||||
return fieldWithPath("*.[].details.requestMappingConditions" + path);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
TomcatServletWebServerFactory tomcat() {
|
||||
return new TomcatServletWebServerFactory(0);
|
||||
}
|
||||
|
||||
@Bean
|
||||
DispatcherServletsMappingDescriptionProvider dispatcherServletsMappingDescriptionProvider() {
|
||||
return new DispatcherServletsMappingDescriptionProvider();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ServletsMappingDescriptionProvider servletsMappingDescriptionProvider() {
|
||||
return new ServletsMappingDescriptionProvider();
|
||||
}
|
||||
|
||||
@Bean
|
||||
FiltersMappingDescriptionProvider filtersMappingDescriptionProvider() {
|
||||
return new FiltersMappingDescriptionProvider();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MappingsEndpoint mappingsEndpoint(Collection<MappingDescriptionProvider> descriptionProviders,
|
||||
ConfigurableApplicationContext context) {
|
||||
return new MappingsEndpoint(descriptionProviders, context);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ExampleController exampleController() {
|
||||
return new ExampleController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> exampleRouter() {
|
||||
return RouterFunctions.route(GET("/foo"), (request) -> ServerResponse.ok().build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class ExampleController {
|
||||
|
||||
@PostMapping(path = "/", consumes = { MediaType.APPLICATION_JSON_VALUE, "!application/xml" },
|
||||
produces = MediaType.TEXT_PLAIN_VALUE, headers = "X-Custom=Foo", params = "a!=alpha")
|
||||
String example() {
|
||||
return "Hello World";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.example.cache.max-size: 1000
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS TEST;
|
||||
@@ -0,0 +1,20 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 1
|
||||
author: marceloverdijk
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: customer
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: int
|
||||
autoIncrement: true
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
- column:
|
||||
name: name
|
||||
type: varchar(50)
|
||||
constraints:
|
||||
nullable: false
|
||||
@@ -0,0 +1,31 @@
|
||||
. ____ _ __ _ _
|
||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
||||
=========|_|==============|___/=/_/_/_/
|
||||
:: Spring Boot ::
|
||||
|
||||
2017-08-08 17:12:30.910 INFO 19866 --- [ main] s.f.SampleWebFreeMarkerApplication : Starting SampleWebFreeMarkerApplication with PID 19866
|
||||
2017-08-08 17:12:30.913 INFO 19866 --- [ main] s.f.SampleWebFreeMarkerApplication : No active profile set, falling back to default profiles: default
|
||||
2017-08-08 17:12:30.952 INFO 19866 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@76b10754: startup date [Tue Aug 08 17:12:30 BST 2017]; root of context hierarchy
|
||||
2017-08-08 17:12:31.878 INFO 19866 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
|
||||
2017-08-08 17:12:31.889 INFO 19866 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||
2017-08-08 17:12:31.890 INFO 19866 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.16
|
||||
2017-08-08 17:12:31.978 INFO 19866 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
|
||||
2017-08-08 17:12:31.978 INFO 19866 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1028 ms
|
||||
2017-08-08 17:12:32.080 INFO 19866 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
|
||||
2017-08-08 17:12:32.084 INFO 19866 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
|
||||
2017-08-08 17:12:32.084 INFO 19866 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
|
||||
2017-08-08 17:12:32.084 INFO 19866 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
|
||||
2017-08-08 17:12:32.084 INFO 19866 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
|
||||
2017-08-08 17:12:32.349 INFO 19866 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@76b10754: startup date [Tue Aug 08 17:12:30 BST 2017]; root of context hierarchy
|
||||
2017-08-08 17:12:32.420 INFO 19866 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(jakarta.servlet.http.HttpServletRequest)
|
||||
2017-08-08 17:12:32.421 INFO 19866 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(jakarta.servlet.http.HttpServletRequest,jakarta.servlet.http.HttpServletResponse)
|
||||
2017-08-08 17:12:32.444 INFO 19866 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
|
||||
2017-08-08 17:12:32.444 INFO 19866 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
|
||||
2017-08-08 17:12:32.471 INFO 19866 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
|
||||
2017-08-08 17:12:32.600 INFO 19866 --- [ main] o.s.w.s.v.f.FreeMarkerConfigurer : ClassTemplateLoader for Spring macros added to FreeMarker configuration
|
||||
2017-08-08 17:12:32.681 INFO 19866 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
|
||||
2017-08-08 17:12:32.744 INFO 19866 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http)
|
||||
2017-08-08 17:12:32.750 INFO 19866 --- [ main] s.f.SampleWebFreeMarkerApplication : Started SampleWebFreeMarkerApplication in 2.172 seconds (JVM running for 2.479)
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user