spring components actuator (2): code analyst

如何使用不再赘述,解释几个感兴趣的关键实现吧:

1 Endpoint如何实现的?

先看下如何实现一个自定义的Endpoint:

@Component
@Endpoint(id = "myendpoint")
public class MyEndpoint {

public final static class Student {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

@ReadOperation
public Student getData() {
Student student = new Student();
student.setName("fujian");
return student;
}
}

然后修改配置application.yml:

[Java]
management:
metrics:
enable:
kafka: true

endpoints:
web:
exposure:
include: myendpoint
[/Java]
然后就可以访问了:http://localhost:8080/actuator/myendpoint

步骤1: 扫描Endpoint,组装成EndpointBean集合

执行的是:org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer#discoverEndpoints

private Collection discoverEndpoints() {
Collection endpointBeans = createEndpointBeans();
addExtensionBeans(endpointBeans);
return convertToEndpoints(endpointBeans);
}

private Collection createEndpointBeans() {
Map byId = new LinkedHashMap<>();
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext,
Endpoint.class);
//最终调用的是:org.springframework.beans.factory.ListableBeanFactory#getBeanNamesForAnnotation
for (String beanName : beanNames) {
if (!ScopedProxyUtils.isScopedTarget(beanName)) {
EndpointBean endpointBean = createEndpointBean(beanName);
EndpointBean previous = byId.putIfAbsent(endpointBean.getId(), endpointBean);
Assert.state(previous == null, () -> “Found two endpoints with the id ‘” + endpointBean.getId() + “‘: ‘”
+ endpointBean.getBeanName() + “‘ and ‘” + previous.getBeanName() + “‘”);
}
}
return byId.values();
}

步骤2: 封装EndpointBean到ServletEndpointRegistrar:这里注意下,它是一种ServletContextInitializer:

执行的是:org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration.WebMvcServletEndpointManagementContextConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DispatcherServlet.class)
public static class WebMvcServletEndpointManagementContextConfiguration {
//注意这个返回类型的定义:public class ServletEndpointRegistrar implements ServletContextInitializer
@Bean
public ServletEndpointRegistrar servletEndpointRegistrar(WebEndpointProperties properties,
ServletEndpointsSupplier servletEndpointsSupplier, DispatcherServletPath dispatcherServletPath) {
return new ServletEndpointRegistrar(dispatcherServletPath.getRelativePath(properties.getBasePath()),
servletEndpointsSupplier.getEndpoints());
}

}

步骤3:ServletContextInitializer就是启动(Tomcat等容器)成功之后会回调它的onStartup方法。

执行的是:org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar#onStartup

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
this.servletEndpoints.forEach((servletEndpoint) -> register(servletContext, servletEndpoint));
}

private void register(ServletContext servletContext, ExposableServletEndpoint endpoint) {
String name = endpoint.getEndpointId().toLowerCaseString() + “-actuator-endpoint”;
String path = this.basePath + “/” + endpoint.getRootPath();
String urlMapping = path.endsWith(“/”) ? path + “*” : path + “/*”;
EndpointServlet endpointServlet = endpoint.getEndpointServlet();
//关键点
Dynamic registration = servletContext.addServlet(name, endpointServlet.getServlet());
registration.addMapping(urlMapping);
registration.setInitParameters(endpointServlet.getInitParameters());
registration.setLoadOnStartup(endpointServlet.getLoadOnStartup());
logger.info(“Registered ‘” + path + “‘ to ” + name);
}

记录到此,基本应该清楚了,最核心点就是在于扫描功能+ServletContextInitializer的作用。

这里可以自己写一个测试下:
@Component
public class DemoServletContextInitializer implements ServletContextInitializer {

public void onStartup(ServletContext servletContext) throws ServletException{
System.out.println(“this is my demo servlet context initializer” );
}

}

启动程序后,就有输出了。

2 某项服务的Health check是如何开启和关闭的?

先看下如何开启或者关闭?
management:
health:
db:
enabled: true
redis:
enabled: true
cassandra:
enabled: true

以redis为例:不是enable了,就意味着一定会检查,原因参考:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@ConditionalOnBean(RedisConnectionFactory.class) //必须有Redis的Jar依赖
@ConditionalOnEnabledHealthIndicator(“redis”)
@AutoConfigureAfter({ RedisAutoConfiguration.class, RedisReactiveHealthContributorAutoConfiguration.class })
public class RedisHealthContributorAutoConfiguration
extends CompositeHealthContributorConfiguration {

@Bean
@ConditionalOnMissingBean(name = { “redisHealthIndicator”, “redisHealthContributor” })
public HealthContributor redisHealthContributor(Map redisConnectionFactories) {
return createContributor(redisConnectionFactories);
}

}

再来看下,management.health.redis怎么开启和关闭的?关键点在于上面的:@ConditionalOnEnabledHealthIndicator(“redis”)

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnEnabledHealthIndicatorCondition.class)
public @interface ConditionalOnEnabledHealthIndicator {

/**
* The name of the health indicator.
* @return the name of the health indicator
*/
String value();

}

其中Conditional表明了条件OnEnabledHealthIndicatorCondition,看下这个条件:

class OnEnabledHealthIndicatorCondition extends OnEndpointElementCondition {

OnEnabledHealthIndicatorCondition() {
super(“management.health.”, ConditionalOnEnabledHealthIndicator.class);
}

}

继续看父类的一段实现:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
//annotationType就是ConditionalOnEnabledHealthIndicator
AnnotationAttributes annotationAttributes = AnnotationAttributes
.fromMap(metadata.getAnnotationAttributes(this.annotationType.getName()));
String endpointName = annotationAttributes.getString(“value”);
//value就是redis
ConditionOutcome outcome = getEndpointOutcome(context, endpointName);
if (outcome != null) {
return outcome;
}
return getDefaultEndpointsOutcome(context);
}

OnEnabledHealthIndicatorCondition指明了判断条件时,可以依据注解”ConditionalOnEnabledHealthIndicator.class”来获取开关项(value),那么上述的metadata是什么?其实就是基类SpringBootCondition中的AnnotatedTypeMetadata:

public abstract class SpringBootCondition implements Condition {

@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)
//……

}

顾名思义,其实就是Conditional标记的类上标记的所有注解信息,所以RedisHealthContributorAutoConfiguration上标记的ConditionalOnEnabledHealthIndicator,自然也是其中一个,所以能拿到它的value:redis,结合前缀”management.health.”,就这样对应上了。

这里也写一个简化版案例:

@Configuration
public class BeanConfig {

private final static class DemoCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// style one:
MergedAnnotations annotations = metadata.getAnnotations();
MergedAnnotation annotation = annotations.get(MyAnnotation.class);
System.out.println(annotation.getString(“value”));

// style two:
//MyAnnotation.class.getName(): com.example.actuatorlearning.BeanConfig$MyAnnotation
Map myAnnotation = metadata.getAnnotationAttributes(MyAnnotation.class.getName());
System.out.println(myAnnotation.get(“value”));
return false;
}
}

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
String value();
}

@Conditional(DemoCondition.class)
@MyAnnotation(“my flag key”)
@Bean
public MyEndpoint.Student student(){
return new MyEndpoint.Student();
}

}

上面的AnnotatedTypeMetadata包含了三个注解:其中就有MyAnnotation

其他一些知识点:

解析泛型的类型:
ResolvableType type = ResolvableType.forClass(AbstractCompositeHealthContributorConfiguration.class,
getClass());
this.indicatorType = type.resolveGeneric(1);
this.beanType = type.resolveGeneric(2);

spring components actuator (1): usages

查看可以看哪些信息:

http://localhost:8080/actuator

{
“_links”: {
“self”: {
“href”: “http://localhost:8080/actuator”,
“templated”: false
},
“health”: {
“href”: “http://localhost:8080/actuator/health”,
“templated”: false
},
“health-path”: {
“href”: “http://localhost:8080/actuator/health/{*path}”,
“templated”: true
}
}
}

完整的可以参考:
https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints

提供哪些,需要配置:
management:
endpoints:
web:
exposure:
include: info,health,prometheus

配置了include,则需要显式显示配置所有的。例如,之前提到的默认的health也需要配置下,否则访问404

Health check的返回和常见配置:

http://localhost:8080/actuator/health

{“status”:”UP”}

Metrics:

查看可以看哪些:

http://localhost:8080/actuator/metrics/http.server.requests

{
“names”: [
“http.server.requests”,
“jvm.buffer.count”,
“jvm.buffer.memory.used”,
“jvm.buffer.total.capacity”,
“jvm.classes.loaded”,
“jvm.classes.unloaded”,
“jvm.gc.live.data.size”,
“jvm.gc.max.data.size”,
“jvm.gc.memory.allocated”,
“jvm.gc.memory.promoted”,
“jvm.gc.pause”,
“jvm.memory.committed”,
“jvm.memory.max”,
“jvm.memory.used”,
“jvm.threads.daemon”,
“jvm.threads.live”,
“jvm.threads.peak”,
“jvm.threads.states”,
“logback.events”,
“process.cpu.usage”,
“process.start.time”,
“process.uptime”,
“system.cpu.count”,
“system.cpu.usage”,
“tomcat.sessions.active.current”,
“tomcat.sessions.active.max”,
“tomcat.sessions.alive.max”,
“tomcat.sessions.created”,
“tomcat.sessions.expired”,
“tomcat.sessions.rejected”
]
}

具体某项查看:

http://localhost:8080/actuator/metrics/http.server.requests

{
“name”: “http.server.requests”,
“description”: null,
“baseUnit”: “seconds”,
“measurements”: [
{
“statistic”: “COUNT”,
“value”: 84.0
},
{
“statistic”: “TOTAL_TIME”,
“value”: 0.33151280000000005
},
{
“statistic”: “MAX”,
“value”: 0.024693
}
],
“availableTags”: [
{
“tag”: “exception”,
“values”: [
“None”
]
},
{
“tag”: “method”,
“values”: [
“GET”
]
},
{
“tag”: “uri”,
“values”: [
“/actuator/health”,
“/actuator”,
“/actuator/metrics”,
“/**”
]
},
{
“tag”: “outcome”,
“values”: [
“CLIENT_ERROR”,
“SUCCESS”
]
},
{
“tag”: “status”,
“values”: [
“404”,
“200”
]
}
]
}

输出metric:

management.metrics.export.influx.uri=https://influx.example.com:8086

参考https://emacsist.github.io/2018/08/07/springboot%E7%BB%93%E5%90%88influxdb%E6%94%B6%E9%9B%86%E7%9B%91%E6%8E%A7%E7%BB%9F%E8%AE%A1%E4%BF%A1%E6%81%AF/

logger:

http://localhost:8080/actuator/loggers 获取所有log配置
http://localhost:8080/actuator/loggers/{name} 获取某个logger,例如root

发送一个POST请求到http://localhost:8080/actuator/loggers/root,加入如下参数

{
“configuredLevel”: “DEBUG”
}

这个功能对于线上问题的排查非常有用。

一些有用的文档:https://docs.spring.io/spring-boot/docs/2.5.3/actuator-api/pdf/spring-boot-actuator-web-api.pdf