自定义Metrics埋点方案「建议收藏」

自定义Metrics埋点方案「建议收藏」一、背景1.1什么是埋点?有别于前端的埋点,我们这里主要讨论的是后端的代码埋点,埋的是一些预定义或者自定义的关于系统业务、性能方面的Metrics。Metrics,就是度量的意思,主要是为了给某个系统某个服务做监控、做统计。1.2为什么需要埋点?不同的IT系统有各自关注的业务场景和关键细节,需要通过埋点来获知当前系统运行的状况,埋点数据采集起来也可以用于事后统计分析。以服务注册中心为例…

大家好,欢迎来到IT知识分享网。

一、背景

1.1 什么是埋点?

有别于前端的埋点,我们这里主要讨论的是后端的代码埋点,埋的是一些预定义或者自定义的关于系统业务、性能方面的Metrics。Metrics,就是度量的意思,主要是为了给某个系统某个服务做监控、做统计。

1.2 为什么需要埋点?

不同的IT系统有各自关注的业务场景和关键细节,需要通过埋点来获知当前系统运行的状况,埋点数据采集起来也可以用于事后统计分析。以服务注册中心为例,当前cache大小、watch数、client数、节点数、有多少独立IP等等Zabbix对ZooKeeper做的事情,需要通过自定义埋点去统计实现。在压测时候,想要知道新生代、老生代的内存使用情况,GC时长等也可以通过Spring Boot内置的MicroMeter埋点去观察。

1.3 Metric的种类

对于Metrics埋点工具的选择,大公司会有内部的自研框架,开源的也有Coda Hale的DropWizard,Spring Boot的MicroMeter,甚至还有淘宝前端团队封装的前端Metrics框架Pandora.js等。

而纵观以上各种Metrics埋点框架,所提供的Metrics种类主要有以下几种类型:

  • Counter:累加型度量,对指标的数据进行累加,反映的是数据随着时间单调递增的关系,应用接受到的 HTTP 请求的总次数;
  • Gauge:瞬态型度量,表示指标在当前时间点的瞬时情况,反映的是数据随着时间上下波动的关系,如系统的load,内存使用率,堆信息等;
  • Timer:变化速率度量,表示指标在某个时间段内变化的速率,反映的是数据随时间的增长快慢关系,如某个接口的耗时;
  • Histogram:数据分布度量,表示某一些指标在某个时间段内的分布情况,反映的是数据随时间的统计学分布关系,如某段时间内,某个接口的 RT 的最大,最小,平均值,方差,95% 分位数等;

其中最常用的是 Gauge 瞬态值,Counter 累加值,以及Timer计时器,基本上可以覆盖90% 以上的场景。

1.4 Metric的命名

MetricName 大体分为两部分,key 和 tags。
metric naming
key 代表着一个具体的项,比如:register.counter,尽量做到 key 可描述,可扩展。

tags 代表着一个指标的不同分类,它和 key 加起来唯一指定了一个 Metric。tags 是一个对象 {},通过不同的 kv 对来描述详情,比如区分不同请求的来源,以 HTTP 接口来举例,{“source”:“shanghai”} 和 {“source”: “hangzhou”} 这样就是不同的 tags,结合 key,就用来表示不同的 Metric 了。

这样的好处就是 tags 可以无限扩展,不会影响到 key,同时,在后续的存储中,同一个 key 可以进行查询筛选,保证数据一致性和连贯性。

1.5 对于埋点框架的选择

1.5.1 内部埋点框架

敝司内部有自研的埋点框架,除了提供客户端埋点的API,还对Metrics的计算做了优化,即先在客户端进行进程级别的轻量级统计计算,再把计算后的结果输出到日志,通过flume agent采集上报到kafka集群,后端系统可按需使用这些计算结果,再做二次聚合计算然后保存到大数据存储引擎,这样便可大大降低后端计算的压力和资源。

1.5.2 Spring Boot Actuator Metrics

Spring Boot使用的是MicroMeter Metrics,与Spring集成度高,内置了JVM、GC等预定义的埋点,也支持自定义埋点,提供的Metrics API也方便易用,而且可以与Prometheus和Grafana对接,可以很方便地打通数据的采集与展现。

1.5.3 为什么需要隔离并使用多套埋点框架?

公司内部的埋点框架足够强大易用,而且有着Client端预计算的性能加成,为何我们还需要再用一套外面的埋点框架呢?原因有三:

首先是由于内部埋点框架的体系架构本身比较重的原因(基于日志的采集上报再做实时和离线的统计运算),开发测试环境的机器资源不足,测试系统响应较慢,而且需要用户在测试机上自行安装日志采集的Agent,再联系开发团队打开测试环境的采集开关。在这样的情况下,实在难以满足在开发测试尤其是压测遇到问题的时候,需要支持快速便利地添加、修改并查看埋点的要求。而Spring Boot Actuator + Prometheus + Grafana这样开源的埋点展现解决方案目前已经比较成熟,网上资源丰富,可以利用项目组本身的测试资源快速地搭建实施。

还有就是考虑到目前在研发的系统日后假如开源的话,所使用的内部埋点框架必然需要剥离并替换为开源的组件。

而从系统架构本身的灵活性去考虑,对于埋点框架这种非核心业务功能,应该保留尽可能多的可选项,以便日后在需要的时候能够很方便地作出必要的变更。譬如说,出现了其他更好的埋点框架被Spring采用了,正如MicroMeter Metrics逐步蚕食Coda Hale DropWizard的地位一样。

因此,基于以上几点的考虑,我们采用接口隔离的方式同时使用了两套埋点框架,在生产环境使用内部埋点框架,在开发测试环境使用MicroMeter Metrics。

二、如何同时使用两套埋点框架

如何同时使用两套埋点框架呢?最简单的做法,当然是不做隔离,同时用两套框架在代码里埋两遍了。不过这样做的话,除了代码重复冗长之外,还把埋点实现强耦合到系统本身的业务层代码,以后如果需要更新、更换埋点框架,需要修改代码,都要修改代码,非常笨拙。

参考接口隔离(ISP)和依赖倒置(DIP)原则,我们采取的做法是根据不同埋点实现框架API的共性抽象出一套埋点接口层(metrics-api),然后针对每个采用的具体框架写一个Spring Boot Starter,使用Maven引入依赖,通过Spring IOC注入到使用埋点的地方,并利用Spring的@Profile注解根据不同的运行时环境激活对应的埋点实现框架。

2.1 通用埋点接口层

按照网上的经验,结合我们目前项目中的所需,目前抽取出来的埋点接口层API暂时只包含了Gauge、Counter和Timer三种类型的埋点:

import java.util.SortedMap;
import java.util.concurrent.Callable;

/** * 通用埋点接口,为了隔离不同的metrics埋点实现 */
public interface MetricsClient { 
   
	/** * 注册并返回一个MetricCounter对象,累加器。 * 针对同种类型的埋点只需调用本方法一次,保存返回的counter重复使用即可。 * * @param metricsName 埋点名 * @param description 埋点描述 * @param tagMap 定义埋点的相关标签,一般会添加到tsdb中用于区分记录 * @return */
	MetricCounter counter(String metricsName, String description, SortedMap<String, String> tagMap);

	/** * 注册并返回一个MetricTimer,计时器。用法建议可参考counter。 * * @param metricsName 埋点名 * @param description 埋点描述 * @param tagMap 定义埋点的相关标签,一般会添加到tsdb中用于区分记录 * @return */
	MetricTimer timer(String metricsName, String description, SortedMap<String, String> tagMap);

	/** * 注册一个gauge类型的埋点,瞬时值 * * @param metricsName 埋点名 * @param description 埋点描述 * @param tagMap 定义埋点的相关标签,一般会添加到tsdb中用于区分记录 * @param callable 封装如何计算值的逻辑闭包 */
	void gauge(String metricsName, String description, SortedMap<String, String> tagMap, Callable<Double> callable);
}

其中所返回的MetricCounter提供了类似Redis的incr和incrby的操作:

/** * 累加器埋点类型 */
public interface MetricCounter { 
   
    /** * 累加器加1 */
    void increment();
    /** * 累加器加delta * * @param delta */
    void incrementBy(long delta);
}

而Timer计时器接口主要提供了记录时间的接口:

import java.util.concurrent.TimeUnit;

/** * 计时器埋点类型 */
public interface MetricTimer { 
   
	/** * 记录消耗的时间(毫秒) * * @param millis */
	void record(long millis);

	/** * 记录消耗的时间(指定时间单位) * * @param time * @param unit */
	void record(long time, TimeUnit unit);
}

另外,在接口包中还提供了一个默认的实现——DefaultMetricsClient,把gauge和timer数据打印到控制台,使用AtomicLong实现counter:

import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/** * 默认埋点实现,打印数据到控制台,仅作参考 */
public class DefaultMetricsClient implements MetricsClient { 
   
	private Timer timer;
	private int period;
	private boolean mute;

	public DefaultMetricsClient() { 
   
		this(10000);
	}

	public DefaultMetricsClient(int period) { 
   
		this(period, false);
	}

	public DefaultMetricsClient(int period, boolean mute) { 
   
		System.out.println("default metrics client created"); // NOSONAR
		timer = new Timer("default_metrics_client_timer", true);
		this.period = period;
		this.mute = mute;
	}

	@Override
	public MetricCounter counter(String metricsName, String description, SortedMap<String, String> tagMap) { 
   
		return new MetricCounter() { 
   
			private AtomicLong c = new AtomicLong(0);
			private String mn = metricsName;
			private String d = description;
			private SortedMap<String, String> t = tagMap;

			@Override
			public void increment() { 
   
				if (!mute) { 
   
					System.out.println(// NOSONAR
							"metric name: " + mn + ", description: " + d + (t != null && !t.isEmpty() ?
									", tags: " + t.toString() :
									"") + ", counter: " + c.incrementAndGet());
				}
			}

			@Override
			public void incrementBy(long delta) { 
   
				if (!mute) { 
   
					System.out.println(// NOSONAR
							"metric name: " + mn + ", description: " + d + (t != null && !t.isEmpty() ?
									", tags: " + t.toString() :
									"") + ", counter: " + c.addAndGet(delta));
				}
			}
		};
	}

	@Override
	public MetricTimer timer(String metricsName, String description, SortedMap<String, String> tagMap) { 
   
		return new MetricTimer() { 
   
			private int limit = 1_000_000;
			private SortedMap<Long, Long> storage = buildStorage();

			private SortedMap<Long, Long> buildStorage() { 
   
				return Collections.synchronizedSortedMap(new TreeMap<>());
			}

			@Override
			public void record(long millis) { 
   
				if (storage.size() >= limit) { 
   
					storage = buildStorage();
				}
				storage.put(System.currentTimeMillis(), millis);
				if (!mute) { 
   
					System.out.println("time consumed: " + millis + " ms."); // NOSONAR
				}
			}

			@Override
			public void record(long time, TimeUnit unit) { 
   
				this.record(TimeUnit.MILLISECONDS.convert(time, unit));
			}
		};
	}

	@Override
	public void gauge(String metricsName, String description, SortedMap<String, String> tagMap,
			Callable<Double> callable) { 
   
		try { 
   
			if (!mute) { 
   
				timer.scheduleAtFixedRate(new TimerTask() { 
   
					@Override
					public void run() { 
   
						try { 
   
							System.out.println(// NOSONAR
									"metric name: " + metricsName + ", description: " + description + (
											tagMap != null && !tagMap.isEmpty() ? ", tags: " + tagMap.toString() : "")
											+ ", value: " + callable.call());
						} catch (Exception e) { 
   
							e.printStackTrace(); // NOSONAR
						}
					}
				}, 0, period);
			}
		} catch (Exception e) { 
   
			e.printStackTrace(); // NOSONAR
		}
	}
}

2.2 Spring Boot Actuator Metrics封装

正如前面所述,针对Spring的Metrics的封装,我们提供了一个Spring Boot Starter:metrics-api-spring

作为一个starter,首先提供了依赖集中管理的功能,在pom中统一声明了所需的依赖项:

		<dependency>
           <groupId>org.springframework</groupId>
           <artifactId>spring-context</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework</groupId>
           <artifactId>spring-core</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-actuator</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-configuration-processor</artifactId>
       </dependency>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-api</artifactId>
       </dependency>

然后是AutoConfiguration相关的配置。SpringMetricsAutoConfiguration是整个starter的入口,@Profile(“!prod”)指定非prod环境时候激活,@EnableConfigurationProperties(SpringMetricsProperties.class)声明相关的properties,并创建了SpringMetricsClient和SpringMetricsRegistry两个bean。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@ConditionalOnClass(MetricsClient.class)
@EnableConfigurationProperties(SpringMetricsProperties.class)
@Profile("!prod")
public class SpringMetricsAutoConfiguration { 
   
    @Autowired
    private SpringMetricsProperties properties;
    @Bean("springMetricsClient")
    @ConditionalOnMissingBean
    public MetricsClient springMetricsClient() { 
   
        return new SpringMetricsClient();
    }
    @Bean
    public SpringMetricsRegistry springMetricsRegister() { 
   
        return new SpringMetricsRegistry();
    }
}

按照Spring Boot Starter标准要求,需要把这个类在META-INF/spring.factories中声明一下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.abc.metrics.SpringMetricsAutoConfiguration

SpringMetricsRegistry实现了MicroMeter的MeterBinder,在重载的bindTo方法中记下了传入的MeterRegistry,registerGauge和registerCounter两个方法创建micrometer中对应的埋点类型并注册到bindTo时候记住的MeterRegistry中。这样就不用把每个埋点都声明为一个Spring Bean,而用户也可以在Spring的@PostConstruct的时候去埋点了。另外,由于MicroMeter是通过BeanPostProcessor去Spring容器中检查有哪些Bean实现了MeterBinder接口然后逐个处理并添加到注册表,如果在一些生命周期早于BeanPostProcessor的地方埋点的话可能会有空指针问题,这种情况可以使用TimerTask延迟实际埋点时机的方式解决。而我们在SpringMetricsRegistry中如果发现MeterRegistry尚未设置,会抛出IllegalStateException提示用户。

import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PreDestroy;
import java.util.List;
import java.util.SortedMap;
import java.util.stream.Collectors;

public class SpringMetricsRegistry implements MeterBinder { 
   
	private static Logger log = LoggerFactory.getLogger(SpringMetricsRegistry.class);

	private MeterRegistry registry;

	@PreDestroy
	public void destroy() { 
   
		if (registry != null && !registry.isClosed()) { 
   
			registry.close();
		}
	}

	@Override
	public void bindTo(MeterRegistry registry) { 
   
		if (this.registry == null) { 
   
			this.registry = registry;
			log.info("MeterRegistry is set...");
		}
	}

	public void registerGauge(SpringMetricGuage g) { 
   
		checkState();
		List<Tag> tags = getTags(g.getTagMap());
		Gauge.builder(g.getMetricsName(), g.getCallable(), SpringMetricGuage.metricFunc).tags(tags)
				.description(g.getDescription()).register(this.registry);
	}

	public Counter registerCounter(String metricsName, String description, SortedMap<String, String> tagMap) { 
   
		checkState();
		List<Tag> tags = getTags(tagMap);
		return Counter.builder(metricsName).tags(tags).description(description).register(this.registry);
	}

	public Timer registerTimer(String metricsName, String description, SortedMap<String, String> tagMap) { 
   
		checkState();
		List<Tag> tags = getTags(tagMap);
		return Timer.builder(metricsName).tags(tags).description(description).register(this.registry);
	}

	private List<Tag> getTags(SortedMap<String, String> tagMap) { 
   
		return tagMap.entrySet().stream().map(entry -> Tag.of(entry.getKey(), entry.getValue()))
				.collect(Collectors.toList());
	}

	private void checkState() { 
   
		if (this.registry == null) { 
   
			throw new IllegalStateException("Metrics registry is not initialized yet!");
		}
	}
}

SpringMetricsClient实现了MetricsClient中的gauge和counter方法,委托给SpringMetricsRegistry执行相应的埋点动作。使用set和map记住已经埋过的点避免重复埋点。SpringMetricsProperties.isEnabled()是一个默认打开的开关,用户可以选择在application.yml中关闭,这样的话就会退化为使用DefaultMetricsClient埋点(打印到控制台)。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class SpringMetricsClient implements MetricsClient { 
   
	private static Logger log = LoggerFactory.getLogger(SpringMetricsClient.class);
	@Autowired
	private SpringMetricsRegistry register;
	@Autowired
	private SpringMetricsProperties springMetricsProperties;
	private Set<SpringMetricGuage> gauges = ConcurrentHashMap.newKeySet();
	private ConcurrentMap<String, MetricCounter> counters = new ConcurrentHashMap<>();
	private ConcurrentMap<String, MetricTimer> timers = new ConcurrentHashMap<>();
	private MetricsClient defaultClient;

	@PostConstruct
	public void init() { 
   
		if (!springMetricsProperties.isEnabled()) { 
   
			defaultClient = new DefaultMetricsClient(springMetricsProperties.getDefaultClientPeriod(),
					springMetricsProperties.isMuteDefaultClient());
		}
	}

	@Override
	public void gauge(String metricName, String description, SortedMap<String, String> tagMap,
			Callable<Double> callable) { 
   
		SpringMetricGuage g = new SpringMetricGuage(metricName, description, tagMap, callable);
		if (gauges.add(g)) { 
   
			if (springMetricsProperties.isEnabled()) { 
   
				register.registerGauge(g);
				log.info("gauge metric added for: {}", g);
			} else { 
   
				defaultClient.gauge(metricName, description, tagMap, callable);
			}
		} else { 
   
			log.warn("duplicated gauge: {}", g);
		}
	}

	@Override
	public MetricCounter counter(String metricName, String description, SortedMap<String, String> tagMap) { 
   
		MetricCounter c;
		String key = getKey(metricName, tagMap);
		if ((c = counters.get(key)) == null) { 
   
			c = counters.computeIfAbsent(key, k -> getSpringMetricCounter(metricName, description, tagMap));
		}
		return c;
	}

	@Override
	public MetricTimer timer(String metricName, String description, SortedMap<String, String> tagMap) { 
   
		MetricTimer t;
		String key = getKey(metricName, tagMap);
		if ((t = timers.get(key)) == null) { 
   
			t = timers.computeIfAbsent(key, k -> getSpringMetricTimer(metricName, description, tagMap));
		}
		return t;
	}

	private MetricTimer getSpringMetricTimer(String metricName, String description, SortedMap<String, String> tagMap) { 
   
		if (springMetricsProperties.isEnabled()) { 
   
			return new SpringMetricTimer(metricName, tagMap, register.registerTimer(metricName, description, tagMap));
		} else { 
   
			return defaultClient.timer(metricName, description, tagMap);
		}
	}

	private MetricCounter getSpringMetricCounter(String metricName, String description,
			SortedMap<String, String> tagMap) { 
   
		if (springMetricsProperties.isEnabled()) { 
   
			return new SpringMetricCounter(metricName, tagMap,
					register.registerCounter(metricName, description, tagMap));
		} else { 
   
			return defaultClient.counter(metricName, description, tagMap);
		}
	}

	private String getKey(String metricName, SortedMap<String, String> tagMap) { 
   
		return metricName + (tagMap != null ? tagMap.toString() : "");
	}

	// getters
}

SpringMetricGuage、SpringMetricCounter和SpringMetricTimer主要是委托给Micrometer的实现:

import java.util.Objects;
import java.util.SortedMap;
import java.util.concurrent.Callable;
import java.util.function.ToDoubleFunction;

public class SpringMetricGuage { 
   
	public static final ToDoubleFunction<Callable<Double>> metricFunc = doubleCallable -> { 
   
		try { 
   
			return doubleCallable.call();
		} catch (Exception e) { 
   
			e.printStackTrace(); // NOSONAR
			return 0L;
		}
	};
	private String metricsName;
	private String description;
	private SortedMap<String, String> tagMap;
	private Callable<Double> callable;

	// constructor

	// getters & setters

	@Override
	public String toString() { 
   
		return "SpringMetricGuage{" + "metricsName='" + metricsName + '\'' + ", description='" + description + '\''
				+ ", tagMap=" + (tagMap != null && !tagMap.isEmpty() ? tagMap.toString() : "{}") + '}';
	}

	@Override
	public boolean equals(Object o) { 
   
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		SpringMetricGuage myGuage = (SpringMetricGuage) o;
		return Objects.equals(metricsName, myGuage.metricsName) && Objects.equals(description, myGuage.description)
				&& Objects.equals(tagMap, myGuage.tagMap);
	}

	@Override
	public int hashCode() { 
   
		return Objects.hash(metricsName, description, tagMap);
	}
}
import io.micrometer.core.instrument.Counter;

import java.util.Objects;
import java.util.SortedMap;

public class SpringMetricCounter implements MetricCounter { 
   
	private String metricName;
	private SortedMap<String, String> tagMap;
	private Counter counter;

	// constructor
	
	@Override
	public void increment() { 
   
		this.counter.increment();
	}

	@Override
	public void incrementBy(long delta) { 
   
		this.counter.increment(delta);
	}

	// getters & setters

	// toString, hashCode & equals
}
import io.micrometer.core.instrument.Timer;

import java.util.Objects;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;

public class SpringMetricTimer implements MetricTimer { 
   
	private String metricName;
	private SortedMap<String, String> tagMap;
	private Timer timer;

	// constructor

	// getters & setters
	
	@Override
	public void record(long millis) { 
   
		this.record(millis, TimeUnit.MILLISECONDS);
	}

	@Override
	public void record(long time, TimeUnit unit) { 
   
		timer.record(time, unit);
	}

	// toString, hashCode & equals
}

2.3 内部埋点框架的封装

公司内部的埋点框架封装与Spring的类似,其中的MetricsAutoConfiguration声明了@Profile(“prod”),即生产环境启用。其他细节由于对外部用户参考意义不大,这里就不表了。

2.4 使用说明

上面介绍了这么多,我们看下在系统中具体是如何使用的。

首先在pom.xml中引入依赖:

<dependency>
    <groupId>com.abc</groupId>
    <artifactId>metrics-api</artifactId>
    <version>${project.parent.version}</version>
</dependency>
<dependency>
    <groupId>com.abc</groupId>
    <artifactId>metrics-api-spring</artifactId>
    <version>${project.parent.version}</version>
</dependency>

Gauge类型的埋点:

	@PostConstruct
	public void init() { 
   
		metricsClient
				.gauge(CONFIG_CACHE_SIZE, "cached history configs of config server", tags, new Callable<Double>() { 
   
					@Override
					public Double call() throws Exception { 
   
						return (double) configHistoryService.getCacheSize();
					}
				});
		logger.info("metric added: {}", CONFIG_CACHE_SIZE);
		// ...
	}

Counter类型和Timer类型的埋点可以用AOP的方式去进行埋点:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
@Component
public class MetricsAspect { 
   
	private static final Logger log = LoggerFactory.getLogger(MetricsAspect.class);
	@Autowired
	private MetricsClient metricsClient;
	private volatile MetricTimer publishTimer;
	private volatile MetricCounter publishCounter;

	@Around("execution(* com.abc.PublishService.doXXXPublish(..))")
	public void logAround(ProceedingJoinPoint joinPoint) throws Throwable { 
   
		long start = System.currentTimeMillis();
		joinPoint.proceed();
		getPublishCounter().increment();
		getPublishTimer().record(System.currentTimeMillis() - start);
	}
	private MetricCounter getPublishCounter() { 
   
		if (this.publishCounter == null) { 
   
			synchronized (this) { 
   
				if (this.publishCounter == null) { 
   
					this.publishCounter = metricsClient
							.counter(MetricsService.CONFIG_PUBLISH_COUNTER, "full publish counter", new TreeMap<>());
				}
			}
		}
		return this.publishCounter;
	}	
	private MetricTimer getPublishTimer() { 
   
		if (this.publishTimer == null) { 
   
			synchronized (this) { 
   
				if (this.publishTimer == null) { 
   
					this.publishTimer = metricsClient
							.timer(MetricsService.CONFIG_PUBLISH_TIMER, "full publish timer", new TreeMap<>());
				}
			}
		}
		return this.publishTimer;
	}
}

三、总结与反思

在本文中,我们介绍了如何同时隔离并使用不同的埋点框架。主要是通过抽取一个抽象的埋点接入层,封装了目前系统中常用的Gauge、Counter和Timer埋点类型,隔离了具体的埋点框架实现;再通过不同的Spring Boot Starter引入依赖,提供配置,根据运行时Profile激活对应的埋点框架。至于Spring Metrics的埋点如何采集展现属于另外一个话题了,感兴趣的用户可以留意小弟后续的博客文章。

使用Spring Metrics埋点的时候,由于MicroMeter是通过BeanPostProcessor扫描context中实现了MeterBinder的Bean然后放入其自身的regitry,所以如果在生命周期早于其执行的地方埋点的话会遇到空指针的问题,这种时候可以考虑使用TimerTask延迟注册时机解决。在JUnit中也可能会遇到类似问题,可以考虑设置enabled属性为false或者mock掉埋点client。我们也会考虑是否有必要在后续的开发迭代中在client进行容错处理,如果registry尚未被注入就被用户用作埋点的话就把用户埋点的Metric放入延迟队列中,初始化完成后再从队列中取出并执行埋点,这样就不需要在用户代码中去纠结这个埋点时机的问题。

四、参考资料

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/24792.html

(0)

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信