Skip to content

Commit ae06c6d

Browse files
committed
improve code and management interface (actuators)
- move actuator related stuff in its own package - use builtin actuators when possible (remove cache evict) - add /about endpoint to mirror actuator's /info - make the management interface mostly made to run on another port (in production at least)
1 parent 717fa65 commit ae06c6d

12 files changed

Lines changed: 167 additions & 240 deletions

File tree

README.md

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ This repository is the cornerstone of BBData. It contains:
2323
* [Async](#async)
2424
- [Permission system](#permission-system)
2525
- [Actuators](#actuators)
26-
* [Customizing the `/info` endpoint](#customizing-the-info-endpoint)
27-
* [Hidden async task executor endpoint](#hidden-async-task-executor-endpoint)
26+
* [Management interface](#management-interface)
27+
* [Customizing the `/info` endpoint](#customizing-the-about-info-endpoint)
28+
* [Task executor monitoring](#task-executor-monitoring)
2829
* [Changing exposed actuators](#changing-exposed-actuators)
2930

3031
## Development setup
@@ -115,13 +116,16 @@ spring.datasource.url = jdbc:mysql://HOST:PORT/bbdata2?autoReconnect=true&useUni
115116
spring.datasource.username=bbdata-admin
116117
spring.datasource.password=PASSWORD
117118

118-
## Cassandra properties
119+
## Cassandra properties
119120
spring.data.cassandra.contact-points=IP_1,IP_2,IP_X
120121
spring.data.cassandra.consistency-level=quorum
121122

122123
## Kafka properties
123124
spring.kafka.producer.bootstrap-servers=HOST:PORT
124125
spring.kafka.template.default-topic=bbdata2-augmented
126+
127+
## Secured actuators endpoints (ensure the port is not available to the outside world)
128+
management.server.port=SECURED-PORT
125129
```
126130

127131
### Executing the jar
@@ -179,12 +183,8 @@ By default, the cache logger is set to `INFO`. Feel free to change it using:
179183
logging.level.org.springframework.cache=TRACE
180184
```
181185

182-
When caching is enabled, a hidden endpoint is available in order to clear all cache entries: `GET /cache-evict`.
183-
In order to avoid having folks discover this endpoint and play with it, you can configure a secret key to use via the property:
184-
```properties
185-
cache.evict.secret-key=XXX
186-
```
187-
If this property is defined, you will have to use `GET /cache-evict?key=XXX` for the eviction to be performed.
186+
When caching is enabled, you can use the `DELETE /caches` actuator endpoint to clear all cache entries.
187+
It is enabled by default (`management.endpoints.web.exposure.include=caches, ...`).
188188

189189

190190
**IMPORTANT**: in case you deploy the input api and the output api separately (using profiles),
@@ -232,9 +232,26 @@ This is the equivalent of `SUDO`: any admin of this group has read/write access
232232

233233
## Actuators
234234

235-
### Customizing the `/info` endpoint
235+
### Management interface
236+
237+
Actuators are a way to monitor the API. They can leak sensitive information, so the management interface should
238+
run on another port as the API, which only administrators have access to.
239+
240+
By default, the actuators will run on the same port (easier for debug, as they will show in openapi), but this is
241+
definitely unsecure in production. Hence, ensure you select another port for the management interface by setting either:
242+
```properties
243+
# 8111 should be protected by firewall or not exposed to the outside
244+
management.server.port=8111
245+
```
246+
or by disabling unsecure actuators, e.g.:
247+
```properties
248+
management.endpoints.web.exposure.include=info
249+
```
250+
251+
### Customizing the `/about` (`/info`) endpoint
236252

237-
The info endpoint can be customized using properties.
253+
The actuator endpoint `/info` is mirrored in the public `/about` endpoint.
254+
What is actually displayed can be customized using properties.
238255

239256
By default, SpringBoot actuator will add the to the JSON response any property with the prefix `info`.
240257
For example, to configure the `instance-name`, define the following in your `application.properties`:
@@ -257,17 +274,20 @@ dynamic.info.<JSON-KEY-NAME-WITHOUT-DOTS>=<ANOTHER PROPERTY>
257274

258275
To hide a dynamic property that is displayed, simply redefine it with an empty value, e.g. `dynamic.info.cache-type=`.
259276

260-
### Hidden async task executor endpoint
261-
262-
By default, and if `sync.enabled=true`, there is a hidden `/tasks` endpoint that returns statistics about the task executor
263-
(core size, active threads) and the tasks. I is though hidden from the swagger documentation.
277+
### task executor monitoring
264278

265-
To disable this endpoint, see changing exposed actuators.
279+
By default, and if `sync.enabled=true`, there is a custom `/tasks` actuator that returns statistics about the task executor
280+
(core size, active threads) and the tasks. To disable this endpoint, change exposed actuators (`id=tasks`).
266281

267282
### Changing exposed actuators
268283

269-
By default, the exposed actuators are:
284+
[Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready)
285+
286+
By default, the exposed actuators are defined in `application.properties`, using the key
287+
`management.endpoints.web.exposure.include=`. Feel free to change this list as you see fit.
288+
289+
To **turn off** all endpoints, simply add:
270290
```properties
271-
management.endpoints.web.exposure.include=info, health, metrics, tasks
272-
```
273-
Feel free to change this list as you see fit, but remember that they are all public !
291+
management.port=
292+
management.endpoints.web.exposure.include=none
293+
```
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package ch.derlin.bbdata
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.tags.Tag
5+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint
6+
import org.springframework.boot.actuate.info.InfoEndpoint
7+
import org.springframework.web.bind.annotation.GetMapping
8+
import org.springframework.web.bind.annotation.RestController
9+
10+
11+
/**
12+
* date: 29.12.19
13+
* @author Lucy Linder <lucy.derlin@gmail.com>
14+
*/
15+
16+
17+
@RestController
18+
@Tag(name = "About", description = "API Status")
19+
// the following annotation is for tests to run. I dunno why, but in tests InfoEndpoint is not available,
20+
// but in normal mode the /about endpoint is registered even if management.endpoints.web.exposure.include doesn't
21+
// include info ...
22+
@ConditionalOnAvailableEndpoint(endpoint = InfoEndpoint::class)
23+
class AboutController(private val infoEndpoint: InfoEndpoint) {
24+
/**
25+
* Since the management interface (actuator) usually runs on another port,
26+
* add the /about endpoint to the API using a "proxy": just call the actuator and return the result.
27+
* This means everything configured in CustomInfoContributor will still hold.
28+
*/
29+
@GetMapping("/about")
30+
@Operation(description = "Get generic information about the running API instance.")
31+
fun getInfo(): Map<String, Any> = infoEndpoint.info()
32+
}
Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
package ch.derlin.bbdata
22

3-
import io.swagger.v3.oas.annotations.Hidden
4-
import io.swagger.v3.oas.annotations.Operation
53
import org.slf4j.Logger
64
import org.slf4j.LoggerFactory
75
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler
86
import org.springframework.beans.factory.annotation.Value
9-
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation
10-
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint
117
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
128
import org.springframework.boot.task.TaskExecutorCustomizer
139
import org.springframework.context.annotation.Configuration
14-
import org.springframework.core.task.TaskExecutor
1510
import org.springframework.scheduling.annotation.AsyncConfigurer
1611
import org.springframework.scheduling.annotation.EnableAsync
1712
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
@@ -34,8 +29,16 @@ import java.util.concurrent.ThreadPoolExecutor
3429
*/
3530

3631

32+
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
33+
@kotlin.annotation.Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
34+
@ConditionalOnProperty(AsyncProperties.ENABLED, havingValue = "true", matchIfMissing = true)
35+
annotation class OnAsyncEnabled
36+
37+
3738
@Component
3839
class AsyncExecutorCustomizer : TaskExecutorCustomizer {
40+
/** Create a custom taskExecutor with CallerRuns policy */
41+
3942
@Value("\${spring.task.execution.pool.queue-capacity}")
4043
val queueCapacity: Int = -1
4144
val logger: Logger = LoggerFactory.getLogger(AsyncExecutorCustomizer::class.java)
@@ -50,50 +53,29 @@ class AsyncExecutorCustomizer : TaskExecutorCustomizer {
5053
}
5154
}
5255

53-
@ConditionalOnProperty(AsyncProperties.ENABLED, havingValue = "true", matchIfMissing = true)
56+
@OnAsyncEnabled
5457
@EnableAsync
5558
@Configuration
5659
class AsyncConfig : AsyncConfigurer {
60+
/**
61+
* Register a custom exception handler to ensure exceptions thrown asynchronously are still logged.
62+
* Attention: this will be called only on async method with void return type !
63+
*/
64+
5765
override fun getAsyncUncaughtExceptionHandler(): AsyncUncaughtExceptionHandler? {
5866
return AsyncExceptionHandler()
5967
}
6068
}
6169

6270
class AsyncExceptionHandler : AsyncUncaughtExceptionHandler {
71+
/**
72+
* Actual logger used for asynchronous uncaught exceptions (methods with void return type only)
73+
*/
6374

6475
val logger: Logger = LoggerFactory.getLogger(AsyncExceptionHandler::class.java)
6576

66-
/**
67-
* Ensure we get the exception logged.
68-
* Attention: this will be called only on async method with void return type !
69-
*/
7077
override fun handleUncaughtException(throwable: Throwable, method: Method, vararg params: Any) {
7178
val niceParams = params.take(2).joinToString(",") { it.toString() }
7279
logger.error("in ${method.name} with ${params.size} params: $niceParams ...", throwable)
7380
}
74-
}
75-
76-
@Component
77-
@ConditionalOnProperty(AsyncProperties.ENABLED, havingValue = "true", matchIfMissing = true)
78-
@WebEndpoint(id = "tasks")
79-
class AsyncMonitor(private val taskExecutor: TaskExecutor? = null) {
80-
/** add an optional tasks actuator to monitor async executor (hidden) */
81-
82-
@ReadOperation
83-
@Operation(description = "Actuator web endpoint 'tasks', monitor async tasks execution")
84-
@Hidden
85-
fun executorInfo(): Map<String, Any> {
86-
// use linkedMap to preserve insertion order in output
87-
val executorInfo = linkedMapOf<String, Any>()
88-
val tasksInfo = linkedMapOf<String, Any>()
89-
if (taskExecutor is ThreadPoolTaskExecutor)
90-
taskExecutor.threadPoolExecutor.let {
91-
executorInfo["pool-size"] = it.corePoolSize
92-
executorInfo["active-count"] = it.activeCount
93-
executorInfo["queue-size"] = it.queue.size
94-
tasksInfo["task-count"] = it.taskCount
95-
tasksInfo["completed-task-count"] = it.completedTaskCount
96-
}
97-
return linkedMapOf("executor" to executorInfo, "tasks" to tasksInfo)
98-
}
9981
}

src/main/kotlin/ch/derlin/bbdata/CustomProperties.kt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,6 @@ class AsyncProperties {
4646
}
4747
}
4848

49-
@Configuration
50-
@ConfigurationProperties(prefix = "cache.evict")
51-
class CacheEvictProperties {
52-
53-
/** Optional secret key to call cache evict: see CacheEvictController */
54-
var secretKey: String = ""
55-
56-
fun matches(key: String?): Boolean = secretKey.isBlank() || key?.equals(secretKey) ?: false
57-
}
58-
5949
@Configuration
6050
@ConfigurationProperties(prefix = "datetime")
6151
class DateTimeFormatProperties {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package ch.derlin.bbdata.actuators
2+
3+
import ch.derlin.bbdata.OnAsyncEnabled
4+
import io.swagger.v3.oas.annotations.Hidden
5+
import io.swagger.v3.oas.annotations.Operation
6+
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation
7+
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint
8+
import org.springframework.core.task.TaskExecutor
9+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
10+
import org.springframework.stereotype.Component
11+
12+
/**
13+
* date: 21.09.20
14+
* @author Lucy Linder <lucy.derlin@gmail.com>
15+
*/
16+
17+
@Component
18+
@OnAsyncEnabled
19+
@WebEndpoint(id = "tasks")
20+
class AsyncMonitor(private val taskExecutor: TaskExecutor? = null) {
21+
/** add an optional tasks actuator to monitor async executor (hidden) */
22+
23+
@ReadOperation
24+
@Operation(description = "Actuator web endpoint 'tasks', monitor async tasks execution")
25+
@Hidden
26+
fun executorInfo(): Map<String, Any> {
27+
// use linkedMap to preserve insertion order in output
28+
val executorInfo = linkedMapOf<String, Any>()
29+
val tasksInfo = linkedMapOf<String, Any>()
30+
if (taskExecutor is ThreadPoolTaskExecutor)
31+
taskExecutor.threadPoolExecutor.let {
32+
executorInfo["pool-size"] = it.corePoolSize
33+
executorInfo["active-count"] = it.activeCount
34+
executorInfo["queue-size"] = it.queue.size
35+
tasksInfo["task-count"] = it.taskCount
36+
tasksInfo["completed-task-count"] = it.completedTaskCount
37+
}
38+
return linkedMapOf("executor" to executorInfo, "tasks" to tasksInfo)
39+
}
40+
}
41+

src/main/kotlin/ch/derlin/bbdata/output/api/ActuatorInfo.kt renamed to src/main/kotlin/ch/derlin/bbdata/actuators/CustomInfoContributor.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package ch.derlin.bbdata.output.api
1+
package ch.derlin.bbdata.actuators
22

33
import org.joda.time.DateTime
44
import org.springframework.beans.factory.annotation.Autowired
@@ -9,14 +9,16 @@ import org.springframework.core.env.Environment
99
import org.springframework.stereotype.Component
1010
import javax.annotation.PostConstruct
1111

12-
1312
/**
14-
* date: 29.12.19
13+
* date: 21.09.20
1514
* @author Lucy Linder <lucy.derlin@gmail.com>
1615
*/
1716
@Component
1817
@ConfigurationProperties(prefix = "dynamic")
1918
class CustomInfoContributor : InfoContributor {
19+
/**
20+
* Customize the actuator endpoint /info with dynamic properties and server time
21+
*/
2022

2123
// any field in the form dynamic.info.key=property
2224
// the property is the key of another property in the *.properties on the classpath

src/main/kotlin/ch/derlin/bbdata/output/CacheEvictController.kt

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)