草庐IT

Spring Boot Admin2 实例状态监控详解

阿提说说 2024-06-27 原文

其他相关文章:

  1. Spring Boot Admin 参考指南
  2. SpringBoot Admin服务离线、不显示健康信息的问题
  3. Spring Boot Admin2 @EnableAdminServer的加载
  4. Spring Boot Admin2 AdminServerAutoConfiguration详解

在微服务中集成Spring Boot Admin 的主要作用之一就是用来监控服务的实例状态,并且最好是当服务DOWN或者OFFLINE的时候发消息提醒,SBA2 提供了很多提醒方式,并且SBA2 已经集成了钉钉,只要进行少量配置即可将状态变更发送到钉钉,详见我的另外一篇文章《Spring Boot Admin 参考指南》。

SBA2 接入飞书

这里我要说明如何进行自定义提醒,将飞书提醒集成到SBA2中,顺便看看SBA2的状态监控具体是如何实现的。

  1. 定义配置类

FeiShuNotifierConfiguration

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnProperty(prefix = "spring.boot.admin.notify.feishu",  name = "enabled", havingValue = "true")
	@AutoConfigureBefore({ AdminServerNotifierAutoConfiguration.NotifierTriggerConfiguration.class, AdminServerNotifierAutoConfiguration.CompositeNotifierConfiguration.class })
	@Lazy(false)
	public static class FeiShuNotifierConfiguration {

		@Bean
		@ConditionalOnMissingBean
		@ConfigurationProperties("spring.boot.admin.notify.feishu")
		public FeiShuNotifier feiShuNotifier(InstanceRepository repository,
											 NotifierProxyProperties proxyProperties) {
			return new FeiShuNotifier(repository, createNotifierRestTemplate(proxyProperties));
		}

	}

这里是模仿SBA2 定义其他通知的方式,InstanceRepository 用于实例持久化操作,NotifierProxyProperties 是通知的HTTP代理配置

  1. 定义消息提醒实现
public class FeiShuNotifier extends AbstractStatusChangeNotifier implements AlarmMessage {

	private static final String DEFAULT_MESSAGE = " 服务名称:#{instance.registration.name} \n 服务实例:#{instance.id} \n 服务URL:#{instance.registration.serviceUrl} \n 服务状态:【#{event.statusInfo.status}】 \n 发送时间:#{time}";

	private final SpelExpressionParser parser = new SpelExpressionParser();

	private RestTemplate restTemplate;

	private String webhookUrl;

	private String secret;

	private Expression message;


	public FeiShuNotifier(InstanceRepository repository, RestTemplate restTemplate) {
		super(repository);
		this.restTemplate = restTemplate;
		this.message = parser.parseExpression(DEFAULT_MESSAGE, ParserContext.TEMPLATE_EXPRESSION);
	}

	@Override
	protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
		return Mono.fromRunnable(() -> sendNotify(event, instance));
	}

	@Override
	protected void updateLastStatus(InstanceEvent event) {
		//原有的更新最后实例状态,在重启后会删除最后状态,导致实例重启后会过滤掉UNKNOWN:UP通知,这里在重启注册后,将最后的状态重新更新会实例中
		//如此实例的变化状态为OFFLINE:UP
		//还有一种办法是:重写shouldNotify(),去掉UNKNOWN:UP,不过滤该通知,也能够收到UP通知,但如此会在Admin重启的时候,所有服务的通知都会发一遍
		if (event instanceof InstanceDeregisteredEvent) {
			String lastStatus = getLastStatus(event.getInstance());
			StatusInfo statusInfo = StatusInfo.valueOf(lastStatus);
			InstanceStatusChangedEvent instanceStatusChangedEvent = new InstanceStatusChangedEvent(event.getInstance(), event.getVersion(), statusInfo);
			super.updateLastStatus(instanceStatusChangedEvent);
		}
		if (event instanceof InstanceStatusChangedEvent) {
			super.updateLastStatus(event);
		}
	}

	private void sendNotify(InstanceEvent event, Instance instance) {
		sendData(getText(event, instance));
	}

	@Override
	public void sendData(String content) {
		if (!isEnabled()) {
			return;
		}
		Map<String, Object> message = createMessage(content);
		doSendData(JSONObject.toJSONString(message));
	}

	private void doSendData(String message) {
		sendWebData(message);
	}

	private void sendWebData(String message) {
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_JSON);
		restTemplate.postForEntity(webhookUrl, new HttpEntity<>(message, headers), Void.class);
	}

	protected Map<String, Object> createMessage(String content) {
		Map<String, Object> messageJson = new HashMap<>();
		messageJson.put("msg_type", "text");

		Map<String, Object> text = new HashMap<>();
		text.put("text", content);
		messageJson.put("content", text);
		Long timestamp = System.currentTimeMillis() / 1000;
		messageJson.put("timestamp", timestamp);
		messageJson.put("sign", getSign(timestamp));

		return messageJson;
	}

	private String getText(InstanceEvent event, Instance instance) {
		Map<String, Object> root = new HashMap<>();
		root.put("event", event);
		root.put("instance", instance);
		root.put("lastStatus", getLastStatus(event.getInstance()));
		root.put("time", DateUtil.now());
		StandardEvaluationContext context = new StandardEvaluationContext(root);
		context.addPropertyAccessor(new MapAccessor());
		return message.getValue(context, String.class);
	}

	private String getSign(Long timestamp) {
		try {
			String stringToSign = timestamp + "\n" + secret;
			Mac mac = Mac.getInstance("HmacSHA256");
			mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
			byte[] signData = mac.doFinal(new byte[]{});
			return new String(Base64.encodeBase64(signData));
		}
		catch (Exception ex) {
			ex.printStackTrace();
		}
		return "";
	}

	public void setRestTemplate(RestTemplate restTemplate) {
		this.restTemplate = restTemplate;
	}

	public String getWebhookUrl() {
		return webhookUrl;
	}

	public void setWebhookUrl(String webhookUrl) {
		this.webhookUrl = webhookUrl;
	}

	public String getSecret() {
		return secret;
	}

	public void setSecret(String secret) {
		this.secret = secret;
	}

	public String getMessage() {
		return message.getExpressionString();
	}

	public void setMessage(String message) {
		this.message = parser.parseExpression(message, ParserContext.TEMPLATE_EXPRESSION);
	}
}

这里继承了AbstractStatusChangeNotifier 用来处理实例状态变更通知,AlarmMessage 是我自己将消息发送给抽象了出来,以便其他告警也能调用。其他都比较简单,飞书的群提醒请参考飞书文档

另外,这里重写了updateLastStatus方法,在取消注册的时候将实例的最后一次状态重新更新到实例中,因为在测试中,实例如果重启,实例状态变为OFFLINE,但重启完成后,却没有收到UP的消息,查看源码后,SBA2在实例取消注册的时候,删除实例的最后一次状态,导致实例的状态变成UNKNOWN,而SBA2里面shouldNotify方法又会过滤UNKNOWN:UP的状态变更。后面会详细看下这部分的源码。

通过如上两步即可接入飞书,看效果图:

状态监控源码分析

从《Spring Boot Admin2 AdminServerAutoConfiguration详解》这篇文章我们可以知道,在SBA2启动的时候,会加载StatusUpdaterStatusUpdateTrigger,前者用于更新实例状态,后者用来触发状态更新,这两个类我将从头至下分部说明。

StatusUpdateTrigger

	private static final Logger log = LoggerFactory.getLogger(StatusUpdateTrigger.class);

	private final StatusUpdater statusUpdater;

	private final IntervalCheck intervalCheck;

	public StatusUpdateTrigger(StatusUpdater statusUpdater, Publisher<InstanceEvent> publisher) {
		super(publisher, InstanceEvent.class);
		this.statusUpdater = statusUpdater;
		this.intervalCheck = new IntervalCheck("status", this::updateStatus);
	}

StatusUpdateTrigger 继承自AbstractEventHandler类,通过构造函数,传入StatusUpdater 更新状态实例,Publisher 接收一个InstanceEvent事件。
super(publisher, InstanceEvent.class) 调用父类构造方法,将Publisher和要关注的事件InstanceEvent传入。
this.intervalCheck = new IntervalCheck(“status”, this::updateStatus) 创建了一个定时任务用来检查实例状态

接下来看下StatusUpdateTrigger 的父类AbstractEventHandler

AbstractEventHandler.start

	public void start() {
		this.scheduler = this.createScheduler();
		this.subscription = Flux.from(this.publisher).subscribeOn(this.scheduler).log(this.log.getName(), Level.FINEST)
				.doOnSubscribe((s) -> this.log.debug("Subscribed to {} events", this.eventType)).ofType(this.eventType)
				.cast(this.eventType).transform(this::handle)
				.retryWhen(Retry.indefinitely().doBeforeRetry((s) -> this.log.warn("Unexpected error", s.failure())))
				.subscribe();
	}

AbstractEventHandler 的start 方法会在StatusUpdateTrigger 初始化@Bean(initMethod = "start", destroyMethod = "stop") 中被调用,这里其创建了一个定时任务,并订阅了指定的事件类型eventType,如果监听到了感兴趣的事件,会调用handle方法,该方法由子类实现

StatusUpdateTrigger.handle

	@Override
	protected Publisher<Void> handle(Flux<InstanceEvent> publisher) {
		return publisher
				.filter((event) -> event instanceof InstanceRegisteredEvent
						|| event instanceof InstanceRegistrationUpdatedEvent)
				.flatMap((event) -> updateStatus(event.getInstance()));
	}

在StatusUpdateTrigger 中 如果事件类型是InstanceRegisteredEvent(实例注册事件)或者InstanceRegistrationUpdatedEvent(实例更新事件),会调用更新实例状态方法
updateStatus

StatusUpdateTrigger.updateStatus

	protected Mono<Void> updateStatus(InstanceId instanceId) {
		return this.statusUpdater.updateStatus(instanceId).onErrorResume((e) -> {
			log.warn("Unexpected error while updating status for {}", instanceId, e);
			return Mono.empty();
		}).doFinally((s) -> this.intervalCheck.markAsChecked(instanceId));
	}

StatusUpdateTrigger.updateStatus 调用了构造函数传入的StatusUpdater Bean 的updateStatus,执行具体的查询实例状态、更新实例状态操作,最后更新该实例的最后检查时间。

StatusUpdateTrigger.start/stop

	@Override
	public void start() {
		super.start();
		this.intervalCheck.start();
	}

	@Override
	public void stop() {
		super.stop();
		this.intervalCheck.stop();
	}

StatusUpdateTrigger 最后是Bean初始化调用方法start和销毁时调用的stop方法,分别用于启动其父类AbstractEventHandler的事件监听,和 IntervalCheck 的定时状态检查任务

StatusUpdater

StatusUpdater 是真正去查询实例状态,并更新实例的类,我们在StatusUpdateTrigger.updateStatus中已经看到其会请求StatusUpdater.updateStatus

	public Mono<Void> updateStatus(InstanceId id) {
		return this.repository.computeIfPresent(id, (key, instance) -> this.doUpdateStatus(instance)).then();

	}

repository.computeIfPresent 会调用EventsourcingInstanceRepository.computeIfPresent,表示实例id存在的话,执行doUpdateStatus并更新状态,doUpdateStatus 会查询实例最新状态,并通过Instance.withStatusInfo包装成一个新的Instance 对象。

EventsourcingInstanceRepository.computeIfPresent

	@Override
	public Mono<Instance> computeIfPresent(InstanceId id,
			BiFunction<InstanceId, Instance, Mono<Instance>> remappingFunction) {
		return this.find(id).flatMap((application) -> remappingFunction.apply(id, application)).flatMap(this::save)
				.retryWhen(this.retryOptimisticLockException);
	}

其中this::save 用来保存实例事件,此处为状态变更事件

EventsourcingInstanceRepository.save

	public Mono<Instance> save(Instance instance) {
		return this.eventStore.append(instance.getUnsavedEvents()).then(Mono.just(instance.clearUnsavedEvents()));
	}

eventStore 实际调用的是在AdminServerAutoConfiguration中加载的InMemoryEventStore

InMemoryEventStore.append

	public Mono<Void> append(List<InstanceEvent> events) {
		return super.append(events).then(Mono.fromRunnable(() -> this.publish(events)));
	}

该方法将在事件保存后,发送一个Publish,这样实现了AbstractEventHandler<InstanceEvent>的类就能监听到该变更事件。

AdminServerNotifierAutoConfiguration

当SBA2中存在通知相关的Notifier Bean时,会开启NotificationTrigger,用来发送变更事件通知

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnBean(Notifier.class)
	@Lazy(false)
	public static class NotifierTriggerConfiguration {

		@Bean(initMethod = "start", destroyMethod = "stop")
		@ConditionalOnMissingBean(NotificationTrigger.class)
		public NotificationTrigger notificationTrigger(Notifier notifier, Publisher<InstanceEvent> events) {
			return new NotificationTrigger(notifier, events);
		}

	}

NotificationTrigger 道理同 StatusUpdateTrigger 。

NotificationTrigger.sendNotifications

	protected Mono<Void> sendNotifications(InstanceEvent event) {
		return this.notifier.notify(event).doOnError((e) -> log.warn("Couldn't notify for event {} ", event, e))
				.onErrorResume((e) -> Mono.empty());
	}

this.notifier.notify(event) 表示会调用对应通知类的notify方法,这里已飞书为例,由于飞书继承了AbstractStatusChangeNotifier类,该处会调用AbstractStatusChangeNotifier.notifyAbstractStatusChangeNotifier.notify又会调用其父类AbstractEventNotifier的notify方法。

AbstractStatusChangeNotifier.notify

	public Mono<Void> notify(InstanceEvent event) {
		return super.notify(event).then(Mono.fromRunnable(() -> updateLastStatus(event)));
	}

AbstractEventNotifier.notify

	public Mono<Void> notify(InstanceEvent event) {
		if (!enabled) {
			return Mono.empty();
		}

		return repository.find(event.getInstance()).filter((instance) -> shouldNotify(event, instance))
				.flatMap((instance) -> doNotify(event, instance))
				.doOnError((ex) -> getLogger().error("Couldn't notify for event {} ", event, ex)).then();
	}

AbstractEventNotifier.notify中会通过shouldNotify判断该事件是否应该通知,该方法由子类实现,因此这里父类又调用了子类AbstractStatusChangeNotifier的实现,如果需要通知,则执行具体的doNotify方法。

AbstractStatusChangeNotifier.shouldNotify

	protected boolean shouldNotify(InstanceEvent event, Instance instance) {
		if (event instanceof InstanceStatusChangedEvent) {
			InstanceStatusChangedEvent statusChange = (InstanceStatusChangedEvent) event;
			String from = getLastStatus(event.getInstance());
			String to = statusChange.getStatusInfo().getStatus();
			return Arrays.binarySearch(ignoreChanges, from + ":" + to) < 0
					&& Arrays.binarySearch(ignoreChanges, "*:" + to) < 0
					&& Arrays.binarySearch(ignoreChanges, from + ":*") < 0;
		}
		return false;
	}

AbstractStatusChangeNotifier.shouldNotify 采用了二分查找法来判断当前变更状态是否在忽略状态类,<0表示不在忽略状态内,需要通知。

使用二分查找,必须先对元素进行排序

最后这么弯弯圈圈下来,实例的状态变更事件就到了FeiShuNotifier.doNotify中,到此我们对SBA2的实例状态监控的分析就结束了。

有关Spring Boot Admin2 实例状态监控详解的更多相关文章

  1. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  2. ruby-on-rails - 如何使用 instance_variable_set 正确设置实例变量? - 2

    我正在查看instance_variable_set的文档并看到给出的示例代码是这样做的:obj.instance_variable_set(:@instnc_var,"valuefortheinstancevariable")然后允许您在类的任何实例方法中以@instnc_var的形式访问该变量。我想知道为什么在@instnc_var之前需要一个冒号:。冒号有什么作用? 最佳答案 我的第一直觉是告诉你不要使用instance_variable_set除非你真的知道你用它做什么。它本质上是一种元编程工具或绕过实例变量可见性的黑客攻击

  3. ruby 正则表达式 - 如何替换字符串中匹配项的第 n 个实例 - 2

    在我的应用程序中,我需要能够找到所有数字子字符串,然后扫描每个子字符串,找到第一个匹配范围(例如5到15之间)的子字符串,并将该实例替换为另一个字符串“X”。我的测试字符串s="1foo100bar10gee1"我的初始模式是1个或多个数字的任何字符串,例如,re=Regexp.new(/\d+/)matches=s.scan(re)给出["1","100","10","1"]如果我想用“X”替换第N个匹配项,并且只替换第N个匹配项,我该怎么做?例如,如果我想替换第三个匹配项“10”(匹配项[2]),我不能只说s[matches[2]]="X"因为它做了两次替换“1fooX0barXg

  4. ruby-on-rails - 跳过状态机方法的所有验证 - 2

    当我的预订模型通过rake任务在状态机上转换时,我试图找出如何跳过对ActiveRecord对象的特定实例的验证。我想在reservation.close时跳过所有验证!叫做。希望调用reservation.close!(:validate=>false)之类的东西。仅供引用,我们正在使用https://github.com/pluginaweek/state_machine用于状态机。这是我的预订模型的示例。classReservation["requested","negotiating","approved"])}state_machine:initial=>'requested

  5. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  6. ruby-on-rails - RSpec:避免使用允许接收的任何实例 - 2

    我正在处理旧代码的一部分。beforedoallow_any_instance_of(SportRateManager).toreceive(:create).and_return(true)endRubocop错误如下:Avoidstubbingusing'allow_any_instance_of'我读到了RuboCop::RSpec:AnyInstance我试着像下面那样改变它。由此beforedoallow_any_instance_of(SportRateManager).toreceive(:create).and_return(true)end对此:let(:sport_

  7. ruby - 字符串文字中的转义状态作为 `String#tr` 的参数 - 2

    对于作为String#tr参数的单引号字符串文字中反斜杠的转义状态,我觉得有些神秘。你能解释一下下面三个例子之间的对比吗?我特别不明白第二个。为了避免复杂化,我在这里使用了'd',在双引号中转义时不会改变含义("\d"="d")。'\\'.tr('\\','x')#=>"x"'\\'.tr('\\d','x')#=>"\\"'\\'.tr('\\\d','x')#=>"x" 最佳答案 在tr中转义tr的第一个参数非常类似于正则表达式中的括号字符分组。您可以在表达式的开头使用^来否定匹配(替换任何不匹配的内容)并使用例如a-f来匹配一

  8. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  9. ruby-on-rails - 使用 ruby​​ 将多个实例变量转换为散列的更好方法? - 2

    我收到格式为的回复#我需要将其转换为哈希值(针对活跃商家)。目前我正在遍历变量并执行此操作:response.instance_variables.eachdo|r|my_hash.merge!(r.to_s.delete("@").intern=>response.instance_eval(r.to_s.delete("@")))end这有效,它将生成{:first="charlie",:last=>"kelly"},但它似乎有点hacky和不稳定。有更好的方法吗?编辑:我刚刚意识到我可以使用instance_variable_get作为该等式的第二部分,但这仍然是主要问题。

  10. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

随机推荐