什么是Actuator
spring boot actuator模块提供了生产级别的功能,比如健康检查、审计、指标收集、http跟踪等,帮助我们监控和管理spring boot应用。这个模块是一个采集应用内部信息暴露给外部的模块,上述的功能都可以通过HTTP和JMX访问。
因为暴露内部信息的特征,Actuator也可以和一些外部的应用监控系统整合(prometheus、graphite、apollo、nacos等)
集成Actuator
只要加上了下列包,springboot 在运行时会自动开启/actuator/health和/actuator/info这两个endpoint
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
访问http://localhost:8081/actuator/health
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "MySQL",
"validationQuery": "select 1",
"result": 1
}
},
"discoveryComposite": {
"status": "UP",
"components": {
"discoveryClient": {
"status": "UP",
"details": {
"services": [
"service-member"
}
}
}
}
},
"hystrix": {
"status": "UP"
},
"livenessState": {
"status": "UP"
},
"readinessState": {
"status": "UP"
},
"tomcat": {
"status": "UP",
"details": {
"threadPool": {
"currentThreadsBusy": 0,
"currentThreadCount": 10,
"maxThreads": 200
}
}
}
},
"groups": [
"liveness",
"readiness"
]
}
访问http://localhost:8081/actuator暴露出来的端点
{
"_links": {
"self": {
"href": "http://localhost:8081/actuator",
"templated": false
},
"archaius": {
"href": "http://localhost:8081/actuator/archaius",
"templated": false
},
"start": {
"href": "http://localhost:8081/actuator/start",
"templated": false
},
"beans": {
"href": "http://localhost:8081/actuator/beans",
"templated": false
},
"caches": {
"href": "http://localhost:8081/actuator/caches",
"templated": false
},
"caches-cache": {
"href": "http://localhost:8081/actuator/caches/{cache}",
"templated": true
},
"health": {
"href": "http://localhost:8081/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8081/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:8081/actuator/info",
"templated": false
},
"conditions": {
"href": "http://localhost:8081/actuator/conditions",
"templated": false
},
"shutdown": {
"href": "http://localhost:8081/actuator/shutdown",
"templated": false
},
"configprops": {
"href": "http://localhost:8081/actuator/configprops",
"templated": false
},
"env-toMatch": {
"href": "http://localhost:8081/actuator/env/{toMatch}",
"templated": true
},
"env": {
"href": "http://localhost:8081/actuator/env",
"templated": false
},
"loggers": {
"href": "http://localhost:8081/actuator/loggers",
"templated": false
},
"loggers-name": {
"href": "http://localhost:8081/actuator/loggers/{name}",
"templated": true
},
"heapdump": {
"href": "http://localhost:8081/actuator/heapdump",
"templated": false
},
"threaddump": {
"href": "http://localhost:8081/actuator/threaddump",
"templated": false
},
"prometheus": {
"href": "http://localhost:8081/actuator/prometheus",
"templated": false
},
"metrics-requiredMetricName": {
"href": "http://localhost:8081/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://localhost:8081/actuator/metrics",
"templated": false
},
"scheduledtasks": {
"href": "http://localhost:8081/actuator/scheduledtasks",
"templated": false
},
"mappings": {
"href": "http://localhost:8081/actuator/mappings",
"templated": false
},
"refresh": {
"href": "http://localhost:8081/actuator/refresh",
"templated": false
},
"features": {
"href": "http://localhost:8081/actuator/features",
"templated": false
},
"service-registry": {
"href": "http://localhost:8081/actuator/service-registry",
"templated": false
}
}
}
Endpoints介绍
spring boot提供了所谓的endpoints(下文翻译为端点)给外部来与应用程序进行访问和交互
eg:/health端点提供了关于应用健康情况的一些基础信息。metrics端点提供了一些有用的应用程序指标(JVM内存使用、系统cpu使用等)
这些Actuator 模块本来就有的端点我们称之为原生端点。根据端点的作用
- 应用配置类:获取应用程序中加载的应用配置、环境变量、自动化配置报告等与Spring Boot应用密切相关的配置类信息。
- 度量指标类:获取应用程序运行过程中用于监控的度量指标,比如:内存信息、线程池信息、HTTP请求统计等。
- 操作控制类:提供了对应用的关闭等操作类功能。
端点配置
#指定端口号
management.server.port=8081
#同时可以将health修改成healthcheck
management.endpoints.web.path-mapping.health=healthcheck
#這樣寫的話,原本內建的/actuator/xxx路徑,都會變成/manage/xxx,可以用來防止被其他人猜到
management.endpoints.web.base-path=/manage
# 可以這樣寫,就會開啟所有endpoints(不包含shutdown)
management.endpoints.web.exposure.include=*
# 也可以這樣寫,就只會開啟指定的endpoint,因此此處只會再額外開啟/actuator/beans和/actuator/mappings
management.endpoints.web.exposure.include=beans,mappings
# exclude可以用來關閉某些endpoints
# exclude通常會跟include一起用,就是先include了全部,然後再exclude /actuator/beans這個endpoint
management.endpoints.web.exposure.exclude=beans
management.endpoints.web.exposure.include=*
# 如果要開啟/actuator/shutdown,要額外再加這一行
management.endpoint.shutdown.enabled=true
自定义端口号
package com.xxxxxx.starter.autoconfigure.config;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.xxxxxx.starter.autoconfigure.health.ApolloHealthIndicator;
import com.xxxxxx.starter.autoconfigure.health.NacosHealthIndicator;
import com.xxxxxx.starter.autoconfigure.health.StartEndpoint;
import com.xxxxxx.starter.gray.core.config.NacosAutoGrayRuleConfigUpdater;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import javax.annotation.PostConstruct;
@Slf4j
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ConfigUtil.class)
@ConditionalOnProperty(name = "xxxxxx.starter.health.enabled", havingValue = "true",
matchIfMissing = true)
public class HealthIndicatorConfiguration {
private final ApplicationContext applicationContext;
@Autowired
private ConfigurableEnvironment environment;
public HealthIndicatorConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@PostConstruct
public void init() {
if (log.isInfoEnabled()) {
log.info("ApolloHealthIndicatorConfiguration init completed");
}
}
// @Bean
// @ConditionalOnMissingBean(ApolloHealthIndicator.class)
// public ApolloHealthIndicator apolloHealthIndicator() {
// return new ApolloHealthIndicator();
// }
@Value("${gray.config.nacos-namespace.date-id:xxxxxx-go}")
private String dateId;
@Value("${gray.config.nacos-namespace.group:gray-dev}")
private String group;
@Value("${gray.config.nacos-namespace:01269cd6-7f0e-41a2-a492-18783d0075ee}")
private String nacosNameSpace;
@Value("${gray.config.nacos-user-name:nacos}")
private String nacosUser;
@Value("${gray.config.nacos-password:nacos}")
private String nacosPassword;
@Value("${gray.config.nacos-server:10.6.4.124:8848}")
private String nacosServer;
@Bean
@ConditionalOnMissingBean(ApolloHealthIndicator.class)
public StartEndpoint startEndpoint() {
ApolloHealthIndicator apolloHealthIndicator = new ApolloHealthIndicator(environment);
String nameSpace = environment.getProperty("gray.config.namespace");
nameSpace = StringUtils.isEmpty(nameSpace) ? "01269cd6-7f0e-41a2-a492-18783d0075ee" : nameSpace ;
NacosHealthIndicator nacosHealthIndicator = new NacosHealthIndicator(dateId, group, nacosNameSpace, nacosUser, nacosPassword, nacosServer);
return new StartEndpoint(applicationContext, apolloHealthIndicator, nacosHealthIndicator, environment);
}
}
package com.xxxxxx.starter.autoconfigure.health;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthComponent;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Endpoint(id = "start")
@Slf4j
public class StartEndpoint {
private final ApplicationContext applicationContext;
private final ApolloHealthIndicator apolloHealthIndicator;
private final NacosHealthIndicator nacosHealthIndicator;
private final ConfigurableEnvironment environment;
public StartEndpoint(ApplicationContext applicationContext,ApolloHealthIndicator apolloHealthIndicator
,NacosHealthIndicator nacosHealthIndicator,ConfigurableEnvironment environment) {
this.applicationContext = applicationContext;
this.apolloHealthIndicator = apolloHealthIndicator;
this.nacosHealthIndicator = nacosHealthIndicator;
this.environment = environment;
}
@ReadOperation
public WebEndpointResponse<HealthComponent> getStartHealth() {
return checkHealth();
}
public WebEndpointResponse<HealthComponent> checkHealth() {
List<HealthIndicator> allHealthIndicators = getAllHealthIndicators();
for (HealthIndicator healthIndicator : allHealthIndicators) {
Health health = healthIndicator.health();
log.info("health is {},class is {}",health.getDetails(), healthIndicator.getClass());
boolean healthBoolean = health.getStatus().equals(Status.DOWN);
log.info("healthStatus is {},healthBoolean is {}",health.getStatus(),healthBoolean);
if (healthBoolean) {
return new WebEndpointResponse(health, 503);
}
}
String nacosProperty = environment.getProperty("xxxxxx.starter.nacos.health.enabled");
if (StringUtils.isEmpty(nacosProperty) || "true".equals(nacosProperty)) {
Health nacosHealth = nacosHealthIndicator.health();
log.info("nacosHealth is {},nacosProperty is {}", nacosHealth ,nacosProperty);
boolean nacosHealthBoolean = nacosHealth.getStatus().equals(Status.DOWN);
log.info("nacosHealthStatus is {},nacosHealthBoolean is {}", nacosHealth.getStatus(), nacosHealthBoolean);
if (nacosHealthBoolean) {
return new WebEndpointResponse(nacosHealthBoolean, 503);
}
}
String apolloProperty = environment.getProperty("xxxxxx.starter.apollo.health.enabled");
if (StringUtils.isEmpty(apolloProperty) || "true".equals(apolloProperty)) {
Health apolloHealth = apolloHealthIndicator.health();
log.info("apolloHealth is {},apolloProperty is {}", apolloHealth, apolloProperty);
boolean apolloHealthBoolean = apolloHealth.getStatus().equals(Status.DOWN);
log.info("apolloHealthStatus is {},apolloHealthBoolean is {}", apolloHealth.getStatus(), apolloHealthBoolean);
if (apolloHealthBoolean) {
return new WebEndpointResponse(apolloHealthBoolean, 503);
}
}
return new WebEndpointResponse(Health.up().build(), 200);
}
public List<HealthIndicator> getAllHealthIndicators() {
List<HealthIndicator> healthIndicators = new ArrayList<>();
Map<String, HealthIndicator> beansOfType = applicationContext.getBeansOfType(HealthIndicator.class);
for (HealthIndicator healthIndicator : beansOfType.values()) {
// 排除自定义的 HealthIndicatorProvider 类本身
healthIndicators.add(healthIndicator);
}
return healthIndicators;
}
}
apollo健康检查
package com.xxxxxx.starter.autoconfigure.health;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.internals.DefaultConfig;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.escape.Escaper;
import com.google.common.net.UrlEscapers;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Slf4j
public class ApolloHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate = new RestTemplate();
private final ConfigUtil m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
private static final Escaper queryParamEscaper = UrlEscapers.urlFormParameterEscaper();
private static final Joiner.MapJoiner MAP_JOINER = Joiner.on("&").withKeyValueSeparator("=");
private static final Splitter NAMESPACE_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults();
private ConfigurableEnvironment environment;
public ApolloHealthIndicator(ConfigurableEnvironment environment) {
this.environment = environment;
}
// @Autowired
// public void setEnvironment(ConfigurableEnvironment environment) {
// this.environment = environment;
// }
@Override
public Health health() {
try {
restTemplate.getForObject(assembleMetaServiceUrl(), String.class);
String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
for (String namespace : namespaceList) {
DefaultConfig config = (DefaultConfig) ConfigService.getConfig(namespace);
Set<String> propertyNames = config.getPropertyNames();
if (propertyNames.isEmpty()) {
log.error("ApolloHealthIndicator namespace is {} empty",namespace);
continue;
//return Health.down().build();
} else {
return Health.up().build();
}
}
return Health.down().build();
} catch (Exception e) {
log.error("health error",e);
return Health.down(e).build();
}
}
private String assembleMetaServiceUrl() {
String domainName = m_configUtil.getMetaServerDomainName();
String appId = m_configUtil.getAppId();
String localIp = m_configUtil.getLocalIp();
Map<String, String> queryParams = Maps.newHashMap();
queryParams.put("appId", queryParamEscaper.escape(appId));
if (!Strings.isNullOrEmpty(localIp)) {
queryParams.put("ip", queryParamEscaper.escape(localIp));
}
return domainName + "/services/config?" + MAP_JOINER.join(queryParams);
}
}
nacos健康检查
package com.xxxxxx.starter.autoconfigure.health;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import java.util.Properties;
@Slf4j
public class NacosHealthIndicator implements HealthIndicator {
private final String dateId;
private final String group;
private final String nacosNameSpace;
private final String nacosUser;
private final String nacosPassword;
private final String nacosServer;
public NacosHealthIndicator(String dateId,String group,String nacosNameSpace,String nacosUser,String nacosPassword,String nacosServer) {
this.dateId = dateId;
this.group = group;
this.nacosNameSpace = nacosNameSpace;
this.nacosUser = nacosUser;
this.nacosPassword = nacosPassword;
this.nacosServer = nacosServer;
}
@Override
public Health health() {
ConfigService configService = getConfig();
try {
String content = configService.getConfig(dateId, group, 3000L);
if (!StringUtils.isEmpty(content)) {
return Health.up().build();
}
} catch (NacosException e) {
log.error("NacosHealthIndicator error is ",e);
return Health.down().build();
}
return Health.down().build();
}
public ConfigService getConfig() {
Properties properties = new Properties();
properties.put(PropertyKeyConst.USERNAME, nacosUser);
properties.put(PropertyKeyConst.PASSWORD, nacosPassword);
properties.put(PropertyKeyConst.SERVER_ADDR, nacosServer);
properties.put(PropertyKeyConst.NAMESPACE, nacosNameSpace);
log.info("灰度配置信息: data-id:{}, group:{}, namespace:{}", dateId, group, nacosNameSpace);
try {
return NacosFactory.createConfigService(properties);
} catch (NacosException e) {
log.error("getConfig error",e);
}
return null;
}
}