跳到主要内容

观察组件

DeepSeek V3 中英对照 Components Observation Components

在本节中,我们将描述与 Micrometer Observation 相关的主要组件。

Micrometer 观察基础流程

通过 ObservationRegistry 创建的 Observation 会附带一个可变的 Observation.Context。在每次 Micrometer Observation 生命周期操作(例如 start())时,会调用相应的 ObservationHandler 方法(例如 onStart),并将可变的 Observation.Context 作为参数传递。

┌───────────────────┐┌───────┐
│ObservationRegistry││Context│
└┬──────────────────┘└┬──────┘
┌▽────────────────────▽┐
│Observation │
└┬─────────────────────┘
┌▽──────┐
│Handler│
└───────┘
none

Micrometer 观测详细流程

┌───────────────────┐┌───────┐┌─────────────────────┐┌────────────────────┐
│ObservationRegistry││Context││ObservationConvention││ObservationPredicate│
└┬──────────────────┘└┬──────┘└┬────────────────────┘└┬───────────────────┘
┌▽────────────────────▽────────▽──────────────────────▽┐
│Observation │
└┬─────────────────────────────────────────────────────┘
┌▽──────┐
│Handler│
└┬──────┘
┌▽────────────────┐
│ObservationFilter│
└─────────────────┘
none

通过 ObservationRegistry 创建的 Observation 带有一个可变的 Observation.Context。为了允许名称和键值的自定义,可以使用 ObservationConvention 而不是直接设置名称。运行 ObservationPredicate 列表以验证是否应创建 Observation 而不是无操作版本。在每次 Micrometer Observation 生命周期 操作(例如 start())时,会调用相应的 ObservationHandler 方法(例如 onStart),并将可变的 Observation.Context 作为参数传递。在 Observation 停止时,在调用 ObservationHandleronStop 方法之前,会调用 ObservationFilter 列表,以选择性地进一步修改 Observation.Context

Observation.Context

为了在插桩代码和处理程序之间(或在处理程序方法之间,例如 onStartonStop)传递信息,你可以使用 Observation.ContextObservation.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));
}
java

如果你想要有更多的观测选项(比如指标和追踪——这些已经在 Micrometer 中包含——以及其他你可能会插入的内容),你需要重写代码以使用 Observation API。

ObservationRegistry registry = ObservationRegistry.create();
Observation.createNotStarted("my.operation", registry).observe(this::doSomeWorkHere);
java

从 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;
}

}
java

你需要将处理器注册到 ObservationRegistry 中:

ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(new SimpleHandler());
java

你可以使用 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);
java

你也可以完全控制作用域机制:

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();
}
java

信号错误和任意事件

插桩代码时,我们可能希望发出信号表示发生了错误,或者表示发生了任意事件。观察 API 允许我们通过其 errorevent 方法来实现这一点。

一个用于通知任意事件的用例可以是将注释附加到 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();
}
java

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 "...";
}

}
java

有关更详细的示例,请参阅完整的仪表化示例,以及如何覆盖默认标签。

以下示例将整个代码整合在一起:

// 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");
java

观察谓词和过滤器

要在给定条件下全局禁用观察,可以使用 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"));
java

使用 @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");
}

}
java

以下测试断言当调用代理的 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();
java