观察组件
在本节中,我们将描述与 Micrometer Observation 相关的主要组件。
Micrometer 观察基础流程
通过 ObservationRegistry
创建的 Observation
会附带一个可变的 Observation.Context
。在每次 Micrometer Observation 生命周期操作(例如 start()
)时,会调用相应的 ObservationHandler
方法(例如 onStart
),并将可变的 Observation.Context
作为参数传递。
┌───────────────────┐┌───────┐
│ObservationRegistry││Context│
└┬──────────────────┘└┬──────┘
┌▽────────────────────▽┐
│Observation │
└┬─────────────────────┘
┌▽──────┐
│Handler│
└───────┘
Micrometer 观测详细流程
┌───────────────────┐┌───────┐┌─────────────────────┐┌────────────────────┐
│ObservationRegistry││Context││ObservationConvention││ObservationPredicate│
└┬──────────────────┘└┬──────┘└┬────────────────────┘└┬───────────────────┘
┌▽────────────────────▽────────▽──────────────────────▽┐
│Observation │
└┬─────────────────────────────────────────────────────┘
┌▽──────┐
│Handler│
└┬──────┘
┌▽────────────────┐
│ObservationFilter│
└─────────────────┘
通过 ObservationRegistry
创建的 Observation
带有一个可变的 Observation.Context
。为了允许名称和键值的自定义,可以使用 ObservationConvention
而不是直接设置名称。运行 ObservationPredicate
列表以验证是否应创建 Observation
而不是无操作版本。在每次 Micrometer Observation 生命周期 操作(例如 start()
)时,会调用相应的 ObservationHandler
方法(例如 onStart
),并将可变的 Observation.Context
作为参数传递。在 Observation
停止时,在调用 ObservationHandler
的 onStop
方法之前,会调用 ObservationFilter
列表,以选择性地进一步修改 Observation.Context
。
Observation.Context
为了在插桩代码和处理程序之间(或在处理程序方法之间,例如 onStart
和 onStop
)传递信息,你可以使用 Observation.Context
。Observation.Context
是一个类似于 Map
的容器,可以在处理程序访问上下文中的数据时为你存储值。
观察处理器
观察处理器(Observation Handler)允许为现有的仪表(instrumentations)添加功能(即你只需对代码进行一次仪表化,根据观察处理器的设置,将执行不同的操作,如创建跨度、指标、日志等)。换句话说,如果你已经对代码进行了仪表化,并希望在其周围添加指标,你只需在观察注册表(Observation Registry)中注册一个观察处理器即可添加该行为。
让我们来看一个为现有插装添加计时器行为的示例。
记录观测结果的一种流行方式是将开始状态存储在 Timer.Sample
实例中,并在事件结束时停止它。记录此类测量的代码可能如下所示:
MeterRegistry registry = new SimpleMeterRegistry();
Timer.Sample sample = Timer.start(registry);
try {
// do some work here
}
finally {
sample.stop(Timer.builder("my.timer").register(registry));
}
如果你想要有更多的观测选项(比如指标和追踪——这些已经在 Micrometer 中包含——以及其他你可能会插入的内容),你需要重写代码以使用 Observation
API。
ObservationRegistry registry = ObservationRegistry.create();
Observation.createNotStarted("my.operation", registry).observe(this::doSomeWorkHere);
从 Micrometer 1.10 开始,你可以注册“处理器”(ObservationHandler
实例),这些处理器会在观测的生命周期事件(例如,当观测开始或停止时,你可以运行自定义代码)时收到通知。使用此功能可以让你在现有的指标检测中添加追踪功能(参见:DefaultTracingObservationHandler
)。这些处理器的实现不需要与追踪相关。如何实现它们完全取决于你(例如,你可以添加日志记录功能)。
ObservationHandler 示例
基于此,我们可以实现一个简单的处理程序,通过将调用信息打印到 stdout
来让用户知道其调用情况:
static class SimpleHandler implements ObservationHandler<Observation.Context> {
@Override
public void onStart(Observation.Context context) {
System.out.println("START " + "data: " + context.get(String.class));
}
@Override
public void onError(Observation.Context context) {
System.out.println("ERROR " + "data: " + context.get(String.class) + ", error: " + context.getError());
}
@Override
public void onEvent(Observation.Event event, Observation.Context context) {
System.out.println("EVENT " + "event: " + event + " data: " + context.get(String.class));
}
@Override
public void onStop(Observation.Context context) {
System.out.println("STOP " + "data: " + context.get(String.class));
}
@Override
public boolean supportsContext(Observation.Context handlerContext) {
// you can decide if your handler should be invoked for this context object or
// not
return true;
}
}
你需要将处理器注册到 ObservationRegistry
中:
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(new SimpleHandler());
你可以使用 observe
方法来对你的代码库进行监控:
ObservationRegistry registry = ObservationRegistry.create();
Observation.Context context = new Observation.Context().put(String.class, "test");
// using a context is optional, so you can call createNotStarted without it:
// Observation.createNotStarted(name, registry)
Observation.createNotStarted("my.operation", () -> context, registry).observe(this::doSomeWorkHere);
你也可以完全控制作用域机制:
ObservationRegistry registry = ObservationRegistry.create();
Observation.Context context = new Observation.Context().put(String.class, "test");
// using a context is optional, so you can call start without it:
// Observation.start(name, registry)
Observation observation = Observation.start("my.operation", () -> context, registry);
try (Observation.Scope scope = observation.openScope()) {
doSomeWorkHere();
}
catch (Exception ex) {
observation.error(ex); // and don't forget to handle exceptions
throw ex;
}
finally {
observation.stop();
}
信号错误和任意事件
在插桩代码时,我们可能希望发出信号表示发生了错误,或者表示发生了任意事件。观察 API 允许我们通过其 error
和 event
方法来实现这一点。
一个用于通知任意事件的用例可以是将注释附加到 Span
以进行分布式跟踪,但你也可以在自己的处理程序中以任何方式处理它们,例如基于它们发出日志事件:
ObservationRegistry registry = ObservationRegistry.create();
Observation observation = Observation.start("my.operation", registry);
try (Observation.Scope scope = observation.openScope()) {
observation.event(Observation.Event.of("my.event", "look what happened"));
doSomeWorkHere();
}
catch (Exception exception) {
observation.error(exception);
throw exception;
}
finally {
observation.stop();
}
Observation.ObservationConvention 示例
在插桩代码时,我们希望为标签提供合理的默认值,同时也希望让用户能够轻松地更改这些默认值。ObservationConvention
接口描述了我们应该为 Observation.Context
创建哪些标签和名称:
/**
* A dedicated {@link Observation.Context} used for taxing.
*/
class TaxContext extends Observation.Context {
private final String taxType;
private final String userId;
TaxContext(String taxType, String userId) {
this.taxType = taxType;
this.userId = userId;
}
String getTaxType() {
return taxType;
}
String getUserId() {
return userId;
}
}
/**
* An example of an {@link ObservationFilter} that will add the key-values to all
* observations.
*/
class CloudObservationFilter implements ObservationFilter {
@Override
public Observation.Context map(Observation.Context context) {
return context.addLowCardinalityKeyValue(KeyValue.of("cloud.zone", CloudUtils.getZone()))
.addHighCardinalityKeyValue(KeyValue.of("cloud.instance.id", CloudUtils.getCloudInstanceId()));
}
}
/**
* An example of an {@link ObservationConvention} that renames the tax related
* observations and adds cloud related tags to all contexts. When registered via the
* `ObservationRegistry#observationConfig#observationConvention` will override the
* default {@link TaxObservationConvention}. If the user provides a custom
* implementation of the {@link TaxObservationConvention} and passes it to the
* instrumentation, the custom implementation wins.
*
* In other words
*
* 1) Custom {@link ObservationConvention} has precedence 2) If no custom convention
* was passed and there's a matching {@link GlobalObservationConvention} it will be
* picked 3) If there's no custom, nor matching global convention, the default
* {@link ObservationConvention} will be used
*
* If you need to add some key-values regardless of the used
* {@link ObservationConvention} you should use an {@link ObservationFilter}.
*/
class GlobalTaxObservationConvention implements GlobalObservationConvention<TaxContext> {
// this will be applicable for all tax contexts - it will rename all the tax
// contexts
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof TaxContext;
}
@Override
public String getName() {
return "global.tax.calculate";
}
}
// Interface for an ObservationConvention related to calculating Tax
interface TaxObservationConvention extends ObservationConvention<TaxContext> {
@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof TaxContext;
}
}
/**
* Default convention of tags related to calculating tax. If no user one or global
* convention will be provided then this one will be picked.
*/
class DefaultTaxObservationConvention implements TaxObservationConvention {
@Override
public KeyValues getLowCardinalityKeyValues(TaxContext context) {
return KeyValues.of(TAX_TYPE.withValue(context.getTaxType()));
}
@Override
public KeyValues getHighCardinalityKeyValues(TaxContext context) {
return KeyValues.of(USER_ID.withValue(context.getUserId()));
}
@Override
public String getName() {
return "default.tax.name";
}
}
/**
* If micrometer-docs-generator is used, we will automatically generate documentation
* for your observations. Check this URL
* https://github.com/micrometer-metrics/micrometer-docs-generator#documentation for
* setup example and read the {@link ObservationDocumentation} javadocs.
*/
enum TaxObservationDocumentation implements ObservationDocumentation {
CALCULATE {
@Override
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
return DefaultTaxObservationConvention.class;
}
@Override
public String getContextualName() {
return "calculate tax";
}
@Override
public String getPrefix() {
return "tax";
}
@Override
public KeyName[] getLowCardinalityKeyNames() {
return TaxLowCardinalityKeyNames.values();
}
@Override
public KeyName[] getHighCardinalityKeyNames() {
return TaxHighCardinalityKeyNames.values();
}
};
enum TaxLowCardinalityKeyNames implements KeyName {
TAX_TYPE {
@Override
public String asString() {
return "tax.type";
}
}
}
enum TaxHighCardinalityKeyNames implements KeyName {
USER_ID {
@Override
public String asString() {
return "tax.user.id";
}
}
}
}
/**
* Our business logic that we want to observe.
*/
class TaxCalculator {
private final ObservationRegistry observationRegistry;
// If the user wants to override the default they can override this. Otherwise,
// it will be {@code null}.
@Nullable
private final TaxObservationConvention observationConvention;
TaxCalculator(ObservationRegistry observationRegistry,
@Nullable TaxObservationConvention observationConvention) {
this.observationRegistry = observationRegistry;
this.observationConvention = observationConvention;
}
void calculateTax(String taxType, String userId) {
// Create a new context
TaxContext taxContext = new TaxContext(taxType, userId);
// Create a new observation
TaxObservationDocumentation.CALCULATE
.observation(this.observationConvention, new DefaultTaxObservationConvention(), () -> taxContext,
this.observationRegistry)
// Run the actual logic you want to observe
.observe(this::calculateInterest);
}
private void calculateInterest() {
// do some work
}
}
/**
* Example of user changing the default conventions.
*/
class CustomTaxObservationConvention extends DefaultTaxObservationConvention {
@Override
public KeyValues getLowCardinalityKeyValues(TaxContext context) {
return super.getLowCardinalityKeyValues(context)
.and(KeyValue.of("additional.low.cardinality.tag", "value"));
}
@Override
public KeyValues getHighCardinalityKeyValues(TaxContext context) {
return KeyValues.of("this.would.override.the.default.high.cardinality.tags", "value");
}
@Override
public String getName() {
return "tax.calculate";
}
}
/**
* A utility class to set cloud related arguments.
*/
static class CloudUtils {
static String getZone() {
return "...";
}
static String getCloudInstanceId() {
return "...";
}
}
有关更详细的示例,请参阅完整的仪表化示例,以及如何覆盖默认标签。
以下示例将整个代码整合在一起:
// Registry setup
ObservationRegistry observationRegistry = ObservationRegistry.create();
// add metrics
SimpleMeterRegistry registry = new SimpleMeterRegistry();
observationRegistry.observationConfig().observationHandler(new DefaultMeterObservationHandler(registry));
observationRegistry.observationConfig().observationConvention(new GlobalTaxObservationConvention());
// This will be applied to all observations
observationRegistry.observationConfig().observationFilter(new CloudObservationFilter());
// In this case we're overriding the default convention by passing the custom one
TaxCalculator taxCalculator = new TaxCalculator(observationRegistry, new CustomTaxObservationConvention());
// run the logic you want to observe
taxCalculator.calculateTax("INCOME_TAX", "1234567890");
观察谓词和过滤器
要在给定条件下全局禁用观察,可以使用 ObservationPredicate
。要修改 Observation.Context
,可以使用 ObservationFilter
。
要设置这些,请分别调用 ObservationRegistry#observationConfig()#observationPredicate()
和 ObservationRegistry#observationConfig()#observationFilter()
方法。
以下示例使用了谓词和过滤器:
// Example using a metrics handler - we need a MeterRegistry
MeterRegistry meterRegistry = new SimpleMeterRegistry();
// Create an ObservationRegistry
ObservationRegistry registry = ObservationRegistry.create();
// Add predicates and filter to the registry
registry.observationConfig()
// ObservationPredicate can decide whether an observation should be
// ignored or not
.observationPredicate((observationName, context) -> {
// Creates a noop observation if observation name is of given name
if ("to.ignore".equals(observationName)) {
// Will be ignored
return false;
}
if (context instanceof MyContext) {
// For the custom context will ignore a user with a given name
return !"user to ignore".equals(((MyContext) context).getUsername());
}
// Will proceed for all other types of context
return true;
})
// ObservationFilter can modify a context
.observationFilter(context -> {
// We're adding a low cardinality key to all contexts
context.addLowCardinalityKeyValue(KeyValue.of("low.cardinality.key", "low cardinality value"));
if (context instanceof MyContext) {
// We're mutating a specific type of a context
MyContext myContext = (MyContext) context;
myContext.setUsername("some username");
// We want to remove a high cardinality key value
return myContext.removeHighCardinalityKeyValue("high.cardinality.key.to.ignore");
}
return context;
})
// Example of using metrics
.observationHandler(new DefaultMeterObservationHandler(meterRegistry));
// Observation will be ignored because of the name
then(Observation.start("to.ignore", () -> new MyContext("don't ignore"), registry)).isSameAs(Observation.NOOP);
// Observation will be ignored because of the entries in MyContext
then(Observation.start("not.to.ignore", () -> new MyContext("user to ignore"), registry))
.isSameAs(Observation.NOOP);
// Observation will not be ignored...
MyContext myContext = new MyContext("user not to ignore");
myContext.addHighCardinalityKeyValue(KeyValue.of("high.cardinality.key.to.ignore", "some value"));
Observation.createNotStarted("not.to.ignore", () -> myContext, registry).observe(this::yourCodeToMeasure);
// ...and will have the context mutated
then(myContext.getLowCardinalityKeyValue("low.cardinality.key").getValue()).isEqualTo("low cardinality value");
then(myContext.getUsername()).isEqualTo("some username");
then(myContext.getHighCardinalityKeyValues())
.doesNotContain(KeyValue.of("high.cardinality.key.to.ignore", "some value"));
使用 @Observed 注解
如果你已经启用了面向切面编程(例如,通过使用 org.aspectj:aspectjweaver
),你可以使用 @Observed
注解来创建观察点。你可以将这个注解放在方法上以观察该方法,或者放在类上以观察该类中的所有方法。
以下示例展示了一个 ObservedService
,其中在方法上有一个注解:
static class ObservedService {
@Observed(name = "test.call", contextualName = "test#call",
lowCardinalityKeyValues = { "abc", "123", "test", "42" })
void call() {
System.out.println("call");
}
}
以下测试断言当调用代理的 ObservedService
实例时,是否创建了正确的观察结果:
// create a test registry
TestObservationRegistry registry = TestObservationRegistry.create();
// add a system out printing handler
registry.observationConfig().observationHandler(new ObservationTextPublisher());
// create a proxy around the observed service
AspectJProxyFactory pf = new AspectJProxyFactory(new ObservedService());
pf.addAspect(new ObservedAspect(registry));
// make a call
ObservedService service = pf.getProxy();
service.call();
// assert that observation has been properly created
assertThat(registry)
.hasSingleObservationThat()
.hasBeenStopped()
.hasNameEqualTo("test.call")
.hasContextualNameEqualTo("test#call")
.hasLowCardinalityKeyValue("abc", "123")
.hasLowCardinalityKeyValue("test", "42")
.hasLowCardinalityKeyValue("class", ObservedService.class.getName())
.hasLowCardinalityKeyValue("method", "call").doesNotHaveError();