(以下簡稱
spring-boot-admin
)與Spring Boot、Spring cloud項目以starter得方式自動內建,包括Server端和Client端
SBA
SBA
監控包括應用的基本資訊、logfile(線上實時浏覽或者download)、JVM資訊(線程資訊、堆資訊、非堆資訊)、Web(API接口資訊、最近100次API調用的資訊)、應用中使用者登入資訊;監控名額很全面,但針對具體項目就要增加符合自己項目的内容了,比如如下兩點:
自定義HttpTrace增加入參和出參
結果:
在
spring-boot-admin
中HttpTrace顯示的資訊包括session、principal、request、response、timeTaken和timestamp,但session、principal對該項目完全無用,request是
HttpTrace
的内部類顯示資訊包括:
1private final String method;
2private final URI uri;
3//唯一可以擴充的地方
4private final Map<String, List<String>> headers;
5private final String remoteAddress;
response也是
HttpTrace
的内部類:
1private final int status;
2//唯一可以擴充的地方
3private final Map<String, List<String>> headers;
唯一缺少的就是請求的
入參
和
出參
,而Headers的資訊是無用的。是以擴充HttpTrace顯示請求中的
入參
和
出參
勢在必行,大緻的思路是:自定義Filter-->裝飾模式轉換成自定義的request和response對象,内部擷取請求和相應内容-->HttpExchangeTracer建立HttpTrace對象-->InmemoryHttpTraceRepository儲存100次請求的HttpTrace對象,供server端使用。由于Filter中使用的部分對象是先建立的是以我們先從需要的零部件開始
- 第一步:包裝HttpServletRequest擷取請求内容:
1public class RequestWrapper extends HttpServletRequestWrapper {
2//存放請求的消息體(先緩存一份)
3 private byte[] body;
4//自定義輸入流的包裝類,将緩存資料再寫入到流中
5 private ServletInputStreamWrapper wrapper;
6 private final Logger logger = LoggerFactory.getLogger(RequestWrapper.class);
7
8 public RequestWrapper(HttpServletRequest request) {
9 super(request);
10 try {
11//使用Apache的commons-io工具從request中先讀取資料
12 body = IOUtils.toByteArray(request.getInputStream());
13 } catch (IOException e) {
14 logger.error("從請求中擷取請求參數出現異常:", e);
15 }
16//将讀取出來的記憶體再寫入流中
17 wrapper = new ServletInputStreamWrapper(new ByteArrayInputStream(body));
18 }
19//轉換成String 供外部調用,并替換轉義字元
20 public String body() {
21 return new String(body).replaceAll("[ntr]","");
22 }
23//将我們的自定義的流包裝類傳回,供系統調用 讀取資料
24 @Override
25 public ServletInputStream getInputStream() throws IOException {
26 return this.wrapper;
27 }
28//将我們的自定義的流包裝類傳回,供系統調用 讀取資料
29 @Override
30 public BufferedReader getReader() throws IOException {
31 return new BufferedReader(new InputStreamReader(this.wrapper));
32 }
33 //從給定的輸入流中讀取資料
34 static final class ServletInputStreamWrapper extends ServletInputStream {
35
36 private InputStream inputStream;
37
38 public ServletInputStreamWrapper(InputStream inputStream) {
39 this.inputStream = inputStream;
40 }
41
42 @Override
43 public boolean isFinished() {
44 return true;
45 }
46
47 @Override
48 public boolean isReady() {
49 return false;
50 }
51
52 @Override
53 public void setReadListener(ReadListener listener) {
54
55 }
56//讀取緩存資料
57 @Override
58 public int read() throws IOException {
59 return this.inputStream.read();
60 }
61
62 public InputStream getInputStream() {
63 return inputStream;
64 }
65
66 public void setInputStream(InputStream inputStream) {
67 this.inputStream = inputStream;
68 }
69 }
70}
- 第二步:包裝HttpServletResponse類擷取響應内容:
1public class ResponseWrapper extends HttpServletResponseWrapper {
2
3 private HttpServletResponse response;
4//緩存響應内容的輸出流
5 private ByteArrayOutputStream result = new ByteArrayOutputStream();
6
7 public ResponseWrapper(HttpServletResponse response) {
8 super(response);
9 this.response = response;
10 }
11
12 /**
13 * 響應的内容 供外部調用
14 *針對 體積較大的響應内容 很容易發生 OOM(比如:/actuator/logfile 接口),可在調用該方法的地方就行api過濾
15 *解決方法在第四步
16 */
17 public String body(){
18 return result.toString();
19 }
20
21 @Override
22 public ServletOutputStream getOutputStream() throws IOException {
23 return new ServletOutputStreamWrapper(this.response,this.result);
24 }
25
26 @Override
27 public PrintWriter getWriter() throws IOException {
28 return new PrintWriter(new OutputStreamWriter(this.result,this.response.getCharacterEncoding()));
29 }
30
31//自定義輸出流的包裝類 内部類
32 static final class ServletOutputStreamWrapper extends ServletOutputStream{
33
34 private HttpServletResponse response;
35 private ByteArrayOutputStream byteArrayOutputStream;
36
37 public ServletOutputStreamWrapper(HttpServletResponse response, ByteArrayOutputStream byteArrayOutputStream) {
38 this.response = response;
39 this.byteArrayOutputStream = byteArrayOutputStream;
40 }
41
42 @Override
43 public boolean isReady() {
44 return true;
45 }
46
47 @Override
48 public void setWriteListener(WriteListener listener) {
49
50 }
51
52 @Override
53 public void write(int b) throws IOException {
54 this.byteArrayOutputStream.write(b);
55 }
56
57 /**
58 * 将内容重新重新整理到傳回的對象中 并且避免多次重新整理
59 */
60 @Override
61 public void flush() throws IOException {
62 if(!response.isCommitted()){
63 byte[] bytes = this.byteArrayOutputStream.toByteArray();
64 ServletOutputStream outputStream = response.getOutputStream();
65 outputStream.write(bytes);
66 outputStream.flush();
67 }
68 }
69 }
70}
- 第三步:擴充
,該接口中的方法會在建立TraceableRequest
内部類時調用,自定義實作裡面的方法,再在過濾器中引用該類就可以達到自定義顯示内容的目的, 該類中的Request是我們第一步建立的裝飾類,不能使用HttpServletRequestHttpTrace#Request
1public class CustomerTraceableRequest implements TraceableRequest {
2//自定義的Request裝飾類,不能使用HttpServletRequest
3 private RequestWrapper request;
4
5 public CustomerTraceableRequest(RequestWrapper request) {
6 this.request = request;
7 }
8//HttpTrace類中getMethod會調用
9 @Override
10 public String getMethod() {
11 return request.getMethod();
12 }
13
14 /**
15 * @return POST 或者 GET 方式 都傳回 {ip}:{port}/uir的形式傳回
16 */
17 @Override
18 public URI getUri() {
19 return URI.create(request.getRequestURL().toString());
20 }
21
22//因為在HttpTrace中可擴充的隻有headers的Map,是以我們自定義屬性RequestParam存入headers中,作為入參資訊展示
23 @Override
24 public Map<String, List<String>> getHeaders() {
25 Map<String, List<String>> headerParam = new HashMap<>(1);
26 headerParam.put("RequestParam",getParams());
27 return headerParam;
28 }
29
30//該方法也要重寫,預設的太簡單無法擷取真是的IP
31 @Override
32 public String getRemoteAddress() {
33 return IpUtils.getIpAddress(request);
34 }
35//根據GET或者POST的請求方式不同,擷取不同情況下的請求參數
36 public List<String> getParams() {
37 String params = null;
38 String method = this.getMethod();
39 if(HttpMethod.GET.matches(method)){
40 params = request.getQueryString();
41 }else if(HttpMethod.POST.matches(method)){
42 params = this.request.body();
43 }
44 List<String> result = new ArrayList<>(1);
45 result.add(params);
46 return result;
47 }
48}
- 第四步:擴充
,該接口中方法在建立TraceableResponse
内部類時引用,自定義實作裡面的方法:HttpTrace#Response
1public class CustomerTraceableResponse implements TraceableResponse {
2 //自定義的HttpServletResponse包裝類
3 private ResponseWrapper response;
4 private HttpServletRequest request;
5
6 public CustomerTraceableResponse(ResponseWrapper response, HttpServletRequest request) {
7 this.response = response;
8 this.request = request;
9 }
10//傳回響應狀态
11 @Override
12 public int getStatus() {
13 return response.getStatus();
14 }
15//擴充Response headers添加Response Body屬性,展示響應内容,但是需要排除`/actuator/`開頭的請求,這裡面部分響應内容太大,容易OOM
16 @Override
17 public Map<String, List<String>> getHeaders() {
18 if(isActuatorUri()){
19 return extractHeaders();
20 }else{
21 Map<String, List<String>> result = new LinkedHashMap<>(1);
22 List<String> responseBody = new ArrayList<>(1);
23 responseBody.add(this.response.body());
24 result.put("ResponseBody", responseBody);
25 result.put("Content-Type", getContentType());
26 return result;
27 }
28 }
29//是否是需要過濾的請求uri
30 private boolean isActuatorUri() {
31 String requestUri = request.getRequestURI();
32 AntPathMatcher matcher = new AntPathMatcher();
33 return matcher.match("/actuator/**", requestUri);
34 }
35//server端頁面展示的Content-Type以及Length是從Response中擷取的
36 private List<String> getContentType() {
37 List<String> list = new ArrayList<>(1);
38 list.add(this.response.getContentType());
39 return list;
40 }
41//針對/actuator/**的請求傳回預設的headers内容獲
42 private Map<String, List<String>> extractHeaders() {
43 Map<String, List<String>> headers = new LinkedHashMap<>();
44 for (String name : this.response.getHeaderNames()) {
45 headers.put(name, new ArrayList<>(this.response.getHeaders(name)));
46 }
47 return headers;
48 }
49}
- 第五步:自定義
對Resquest和Response過濾,并建立HttpTrace對象:Filter
1public class CustomerHttpTraceFilter extends OncePerRequestFilter implements Ordered {
2//存儲HttpTrace的repository,預設是居于記憶體的,可擴充該類跟換存儲資料的方式
3 private HttpTraceRepository httpTraceRepository;
4//該類建立HttpTrace對象,Set<Include>包含的内容是我們需要展示那些内容的容器(request-headers,response-headers,remote-address,time-taken)
5 private HttpExchangeTracer httpExchangeTracer;
6
7 public CustomerHttpTraceFilter(HttpTraceRepository httpTraceRepository, HttpExchangeTracer httpExchangeTracer) {
8 this.httpTraceRepository = httpTraceRepository;
9 this.httpExchangeTracer = httpExchangeTracer;
10 }
11
12 @Override
13 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
14//校驗URI是否有效
15 if (!isRequestValid(request)) {
16 filterChain.doFilter(request, response);
17 return;
18 }
19//将HttpServletRequest包裝成我們自己的
20 RequestWrapper wrapper = new RequestWrapper(request);
21//将HttpServletResponse包裝成我們的自己的
22 ResponseWrapper responseWrapper = new ResponseWrapper(response);
23
24//建立我們的自己的TraceRequest對象
25 CustomerTraceableRequest traceableRequest = new CustomerTraceableRequest(wrapper);
26//建立HttpTrace對象(FilteredTraceableRequest 是内部類,通過Set<Include>篩選那些資訊需要展示就儲存那些資訊),重點設定HttpTrace#Request對象的各種參數
27 HttpTrace httpTrace = httpExchangeTracer.receivedRequest(traceableRequest);
28 try {
29 filterChain.doFilter(wrapper, responseWrapper);
30 } finally {
31//自定義的TraceableResponse 儲存需要的response資訊
32 CustomerTraceableResponse traceableResponse = new CustomerTraceableResponse(responseWrapper,request);
33//根據Set<Include>設定HttpTrace中session、principal、timeTaken資訊以及Response内部類資訊
34 this.httpExchangeTracer.sendingResponse(httpTrace, traceableResponse, null, null);
35//将HttpTrace對象儲存在Respository中存儲起來
36 this.httpTraceRepository.add(httpTrace);
37 }
38 }
39
40 private boolean isRequestValid(HttpServletRequest request) {
41 try {
42 new URI(request.getRequestURL().toString());
43 return true;
44 } catch (URISyntaxException ex) {
45 return false;
46 }
47 }
48
49 @Override
50 public int getOrder() {
51 return Ordered.LOWEST_PRECEDENCE - 10;
52 }
53}
- 第六步:通過
禁用@SpringBootApplication(exclude)
自動配置,自定義自動配置更換Filter過濾器:HttpTraceAutoConfiguration
[email protected]
[email protected]
[email protected](prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
[email protected](HttpTraceProperties.class)
5public class TraceFilterConfig {
6
7//存儲HttpTrace資訊的對象
8 @Bean
9 @ConditionalOnMissingBean(HttpTraceRepository.class)
10 public InMemoryHttpTraceRepository traceRepository() {
11 return new InMemoryHttpTraceRepository();
12 }
13//建立HttpTrace對象Exchange
14 @Bean
15 @ConditionalOnMissingBean
16 public HttpExchangeTracer httpExchangeTracer(HttpTraceProperties traceProperties) {
17 return new HttpExchangeTracer(traceProperties.getInclude());
18 }
19
20 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
21 static class ServletTraceFilterConfiguration {
22//将我們自定義的Filter已Bean的方式注冊,才能生效
23 @Bean
24 @ConditionalOnMissingBean
25 public CustomerHttpTraceFilter httpTraceFilter(HttpTraceRepository repository,
26 HttpExchangeTracer tracer) {
27 return new CustomerHttpTraceFilter(repository,tracer);
28 }
29 }
30
31 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
32 static class ReactiveTraceFilterConfiguration {
33
34 @Bean
35 @ConditionalOnMissingBean
36 public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository,
37 HttpExchangeTracer tracer, HttpTraceProperties traceProperties) {
38 return new HttpTraceWebFilter(repository, tracer,
39 traceProperties.getInclude());
40 }
41 }
42}
內建Redisson健康狀态監控
如果有引入
spring-boot-starter-redis
,SBA預設同過
RedisConnectionFactory
監控Redis的健康狀态,無奈Redisson還沒有,自己東收豐衣足食。通過
HealthIndicator
和
ReactiveHealthIndicator
使用政策模式實作不同元件的健康監控,後者是使用Rective模式下的。我是通過JavaBean的方式配置Redisson,是以順便實作
ReactiveHealthIndicator
再添加該名額即可:
[email protected]
[email protected](value = RedissonProperties.class)
3public class RedissonConfig implements ReactiveHealthIndicator {
4//自己的RedissonProperties檔案
5 @Autowired
6 private RedissonProperties redissonProperties;
7//暴露 redissonClient句柄
8 @Bean
9 @ConditionalOnMissingBean
10 public RedissonClient redisClient() {
11 return Redisson.create(config());
12 }
13//通過Bean的方式配置RedissonConfig相關資訊
14 @Bean
15 public Config config() {
16 Config config = new Config();
17 config.useSingleServer() //單實列模式
18 .setAddress(redissonProperties.getAddress() + ":" + redissonProperties.getPort())
19 .setPassword(redissonProperties.getPassword())
20 .setDatabase(redissonProperties.getDatabase())
21 .setConnectionPoolSize(redissonProperties.getConnectionPoolSize())
22 .setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize())
23 .setIdleConnectionTimeout(redissonProperties.getIdleConnectionTimeout())
24 .setSubscriptionConnectionPoolSize(redissonProperties.getSubscriptionConnectionPoolSize())
25 .setSubscriptionConnectionMinimumIdleSize(redissonProperties.getSubscriptionConnectionMinimumIdleSize())
26 .setTimeout(redissonProperties.getTimeout())
27 .setRetryAttempts(redissonProperties.getRetryAttempts())
28 .setRetryInterval(redissonProperties.getRetryInterval())
29 .setConnectTimeout(redissonProperties.getConnectTimeout())
30 .setReconnectionTimeout(redissonProperties.getReconnectionTimeout());
31 config
32 .setCodecProvider(new DefaultCodecProvider())
33 .setEventLoopGroup(new NioEventLoopGroup())
34 .setThreads(Runtime.getRuntime().availableProcessors() * 2)
35 .setNettyThreads(Runtime.getRuntime().availableProcessors() * 2);
36 return config;
37 }
38//實作ReactiveHealthIndicator 重寫health方法
39 @Override
40 public Mono<Health> health() {
41 return checkRedissonHealth().onErrorResume(ex -> Mono.just(new Health.Builder().down(ex).build()));
42 }
43//我是通過ping 的方式判斷redis伺服器是否up的狀态,并增加加Netty和Threads的監控
44 private Mono<Health> checkRedissonHealth() {
45 Health.Builder builder = new Health.Builder();
46 builder.withDetail("address", redissonProperties.getAddress());
47 //檢測健康狀态
48 if (this.redisClient().getNodesGroup().pingAll()) {
49 builder.status(Status.UP);
50 builder.withDetail("dataBase", redissonProperties.getDatabase());
51 builder.withDetail("redisNodeThreads", this.redisClient().getConfig().getThreads());
52 builder.withDetail("nettyThreads", this.redisClient().getConfig().getNettyThreads());
53
54 }else{
55 builder.status(Status.DOWN);
56 }
57 return Mono.just(builder.build());
58 }
59}
在頁面上看就是:
Ok!圓滿完成!
如有錯誤,不吝賜教!
歡迎關注Java技術公衆号: