如何使用Spring Cloud Sleuth

时间:2022-12-02 13:55:00

如何使用Spring Cloud Sleuth

本节将更详细地介绍如何使用Spring Cloud Sleuth。 它涵盖了诸如使用 Spring Cloud Sleuth API 或通过注释控制跨度生命周期等主题。 我们还介绍了一些Spring Cloud Sleuth最佳实践。

如果您从Spring Cloud Sleuth开始,则在进入本节之前,您可能应该阅读入门指南。

1. 使用春云侦探的 API 跨越生命周期

其模块中的春云侦探核心包含由跟踪器实现的所有必要接口。 该项目带有OpenZipkin Brave实现。 您可以通过查看来检查跟踪器如何桥接到侦探的 API。​​api​​​​org.springframework.cloud.sleuth.brave.bridge​

最常用的接口是:

  • ​org.springframework.cloud.sleuth.Tracer​​- 使用跟踪器,可以创建捕获请求关键路径的根跨度。
  • ​org.springframework.cloud.sleuth.Span​​- 跨度是需要启动和停止的单个工作单元。 包含计时信息以及事件和标记。

您也可以直接使用跟踪器实现的 API。

让我们看一下以下 Span 生命周期操作。

  • 开始:启动跨度时,将分配其名称并记录开始时间戳。
  • end:跨度完成(记录跨度的结束时间),如果跨度被采样,则有资格收集(例如到 Zipkin)。
  • 继续:跨度继续,例如在另一个线程中。
  • 使用显式父项创建:您可以创建新的范围并为其设置显式父项。

春云侦探为您创建一个实例。 为了使用它,您可以自动连线它。​​Tracer​

1.1. 创建和结束跨度

您可以使用 手动创建跨度,如以下示例所示:​​Tracer​

// Start a span. If there was a span present in this thread it will become
// the `newSpan`'s parent.
Span newSpan = this.tracer.nextSpan().name("calculateTax");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
// ...
// You can tag a span
newSpan.tag("taxValue", taxValue);
// ...
// You can log an event on a span
newSpan.event("taxCalculated");
}
finally {
// Once done remember to end the span. This will allow collecting
// the span to send it to a distributed tracing system e.g. Zipkin
newSpan.end();
}

在前面的示例中,我们可以看到如何创建跨度的新实例。 如果此线程中已存在跨度,则它将成为新跨度的父级。

创建范围后始终清洁。

如果范围包含大于 50 个字符的名称,则该名称将被截断为 50 个字符。 你的名字必须明确和具体。 大牌会导致延迟问题,有时甚至会导致异常。

1.2. 连续跨度

有时,您不想创建一个新的跨度,但想要继续一个。 这种情况的示例可能如下所示:

  • AOP:如果在达到一个方面之前已经创建了一个跨度,您可能不希望创建新跨度。

要继续跨度,您可以将跨度存储在一个线程中,然后将其传递给另一个线程,如下例所示。

Span spanFromThreadX = this.tracer.nextSpan().name("calculateTax");
try (Tracer.SpanInScope ws = this.tracer.withSpan(spanFromThreadX.start())) {
executorService.submit(() -> {
// Pass the span from thread X
Span continuedSpan = spanFromThreadX;
// ...
// You can tag a span
continuedSpan.tag("taxValue", taxValue);
// ...
// You can log an event on a span
continuedSpan.event("taxCalculated");
}).get();
}
finally {
spanFromThreadX.end();
}

1.3. 使用显式父级创建范围

您可能希望开始一个新的跨度,并提供该跨度的显式父级。 假设跨度的父级位于一个线程中,并且您希望在另一个线程中启动新的跨度。 每当您调用时,它都会创建一个范围来引用当前范围内的范围。 可以将范围放在作用域中,然后调用,如以下示例所示:​​Tracer.nextSpan()​​​​Tracer.nextSpan()​

// let's assume that we're in a thread Y and we've received
// the `initialSpan` from thread X. `initialSpan` will be the parent
// of the `newSpan`
Span newSpan = null;
try (Tracer.SpanInScope ws = this.tracer.withSpan(initialSpan)) {
newSpan = this.tracer.nextSpan().name("calculateCommission");
// ...
// You can tag a span
newSpan.tag("commissionValue", commissionValue);
// ...
// You can log an event on a span
newSpan.event("commissionCalculated");
}
finally {
// Once done remember to end the span. This will allow collecting
// the span to send it to e.g. Zipkin. The tags and events set on the
// newSpan will not be present on the parent
if (newSpan != null) {
newSpan.end();
}
}

创建这样的跨度后,您必须完成它。 否则不报告(例如向Zipkin报告)。

您还可以使用版本显式提供父跨度。​​Tracer.nextSpan(Span parentSpan)​

2. 命名跨度

选择范围名称并非易事。 范围名称应描述操作名称。 名称应为低基数,因此不应包含标识符。

由于正在进行大量检测,因此某些跨度名称是人为的:

  • ​controller-method-name​​当由方法名称为controllerMethodName
  • ​async​​对于使用包装和接口完成的异步操作。CallableRunnable
  • 方法批注返回类的简单名称。@Scheduled

幸运的是,对于异步处理,您可以提供显式命名。

2.1.@SpanName注解

您可以使用注释显式命名范围,如以下示例所示:​​@SpanName​

@SpanName("calculateTax")
class TaxCountingRunnable implements Runnable {

@Override
public void run() {
// perform logic
}

}

在这种情况下,当以下列方式处理时,范围被命名为:​​calculateTax​

Runnable runnable = new TraceRunnable(this.tracer, spanNamer, new TaxCountingRunnable());
Future<?> future = executorService.submit(runnable);
// ... some additional logic ...
future.get();

2.2.toString()Method

创建单独的类是非常罕见的。 通常,创建这些类的匿名实例。 不能批注此类类。 为了克服这个限制,如果存在noannotation,我们检查类是否具有该方法的自定义实现。​​Runnable​​​​Callable​​​​@SpanName​​​​toString()​

运行此类代码会导致创建名为 span 的范围,如以下示例所示:​​calculateTax​

Runnable runnable = new TraceRunnable(this.tracer, spanNamer, new Runnable() {
@Override
public void run() {
// perform logic
}

@Override
public String toString() {
return "calculateTax";
}
});
Future<?> future = executorService.submit(runnable);
// ... some additional logic ...
future.get();

3. 使用注释管理跨度

使用批注管理跨度有很多很好的理由,包括:

  • 与 API 无关意味着跨度协作。 使用批注允许用户添加到跨度,而不需要库依赖跨度 API。 这样做可以让 Sleuth 更改其核心 API,以减少对用户代码的影响。
  • 减少了基本跨度操作的表面积。 如果没有此功能,则必须使用 span api,该 API 具有可能不正确使用的生命周期命令。 通过仅公开范围、标记和日志功能,您可以在不意外中断跨度生命周期的情况下进行协作。
  • 与运行时生成的代码协作。 使用Spring Data和Feign等库,接口的实现是在运行时生成的。 因此,对象的跨度包装很乏味。 现在,您可以提供接口注释和这些接口的参数。

3.1. 创建新跨度

如果不想手动创建局部范围,可以使用注释。 此外,我们还提供注释以自动方式添加标签。​​@NewSpan​​​​@SpanTag​

现在我们可以考虑一些用法示例。

@NewSpan
void testMethod();

在没有任何参数的情况下批注方法会导致创建一个新范围,其名称等于批注的方法名称。

@NewSpan("customNameOnTestMethod4")
void testMethod4();

如果在批注中提供值(直接或通过设置参数),则创建的范围会将提供的值作为名称。​​name​

// method declaration
@NewSpan(name = "customNameOnTestMethod5")
void testMethod5(@SpanTag("testTag") String param);

// and method execution
this.testBean.testMethod5("test");

您可以组合名称和标签。 让我们关注后者。 在这种情况下,批注方法的参数运行时值的值将成为标记的值。 在我们的示例中,标签键是,标签值是。​​testTag​​​​test​

@NewSpan(name = "customNameOnTestMethod3")
@Override
public void testMethod3() {
}

您可以在类和接口上放置注释。 如果您覆盖接口的方法并为注释提供不同的值,则最具体的值获胜(在本例中为 set)。​​@NewSpan​​​​@NewSpan​​​​customNameOnTestMethod3​

3.2. 连续跨度

如果要向现有范围添加标记和注释,可以使用注释,如以下示例所示:​​@ContinueSpan​

// method declaration
@ContinueSpan(log = "testMethod11")
void testMethod11(@SpanTag("testTag11") String param);

// method execution
this.testBean.testMethod11("test");
this.testBean.testMethod13();

(请注意,与注释相比,您还可以使用参数添加日志。​​@NewSpan​​​​log​

这样,跨度就会继续,并且:

  • 已创建命名的日志条目。testMethod11.beforetestMethod11.after
  • 如果引发异常,则还会创建日志条目 namedis。testMethod11.afterFailure
  • 创建具有键 of 和值 of 的标记。testTag11test

3.3. 高级标签设置

有 3 种不同的方法可以将标签添加到范围。 所有这些都由注释控制。 优先级如下:​​SpanTag​

  1. 尝试使用类型的 bean 和提供的名称。TagValueResolver
  2. 如果尚未提供 Bean 名称,请尝试计算表达式。 我们寻找阿豆。 默认实现使用 SPEL 表达式解析。重要只能引用 SPEL 表达式中的属性。 由于安全约束,不允许方法执行。TagValueExpressionResolver
  3. 如果我们找不到任何要计算的表达式,则返回参数的值。toString()

3.3.1. 自定义提取器

以下方法的标记值由接口的实现计算。 它的类名必须作为属性的值传递。​​TagValueResolver​​​​resolver​

请考虑以下带批注的方法:

@NewSpan
public void getAnnotationForTagValueResolver(
@SpanTag(key = "test", resolver = TagValueResolver.class) String test) {
}

现在进一步考虑以下 Bean 实现:​​TagValueResolver​

@Bean(name = "myCustomTagValueResolver")
public TagValueResolver tagValueResolver() {
return parameter -> "Value from myCustomTagValueResolver";
}

前面的两个示例导致将标记值设置为等于。​​Value from myCustomTagValueResolver​

3.3.2. 解析值的表达式

请考虑以下带批注的方法:

@NewSpan
public void getAnnotationForTagValueExpression(
@SpanTag(key = "test", expression = "'hello' + ' characters'") String test) {
}

没有自定义实现会导致计算 SPEL 表达式,并且在跨度上设置了值为 of 的标记。 如果要使用其他表达式解析机制,可以创建自己的 Bean 实现。​​TagValueExpressionResolver​​​​hello characters​

3.3.3. 使用toString() 方法

请考虑以下带批注的方法:

@NewSpan
public void getAnnotationForArgumentToString(@SpanTag("test") Long param) {
}

使用值 of 运行上述方法,以设置具有 String 值为 的标记。​​15​​​​"15"​

4. 接下来要读什么

您现在应该了解如何使用Spring Cloud Sleuth以及应遵循的一些最佳实践。 您现在可以继续了解特定的Spring CloudSleuth功能,或者您可以跳过并阅读Spring Cloud Sleuth中可用的集成。