使用OpenTelemetry进行自定义检测 custom-instrumentation

为您的环境启用OpenTelemetry后,AEM as a Cloud Service会跟踪每个请求并自动导出JVM、HTTP和AEM指标。 该遥测技术描述了AEM。 它显示页面渲染耗时900毫秒以及渲染的组件。 它无法显示您自己的代码正在执行什么操作,例如存储库查询用于哪个列表项,是否找到任何内容,或者哪些页面因作者省略字段而回退到成本高昂的搜索。

您可以使用OpenTelemetry API自行添加该知识。 代码创建的范围和属性使用自动遥测传输:相同的服务名称、相同的处理、相同的APM提供程序。 您未在代码中配置端点、凭据或导出程序。

如果您不熟悉AEM as a Cloud Service上的OpenTelemetry,请从教程Observability via OpenTelemetry开始,了解它的工作方式。 然后按照配置OpenTelemetry以深入了解AEM应用程序为您的环境启用它。

此页面上的示例检测WKND引用站点。 每个示例都描述了要达到的目标、要添加的代码以及预期在APM提供程序中产生的结果。 完整的检测代码(包括在跨域上声明的单元测试)位于WKND存储库的tutorial/otel-custom-instrumentation分支上。

NOTE
自定义检测要求为您的环境启用OpenTelemetry。 请参阅设置集成。

自定义检测的工作原理 how-it-works

  • Adobe在您的AEM环境中运行的OpenTelemetry Java代理包含OpenTelemetry SDK和导出程序。 您的代码仅依赖于OpenTelemetry API。
  • 当您的代码调用GlobalOpenTelemetry时,代理会将其连接到其自己的实现。 范围将成为运行这些范围的请求跟踪的一部分,并且遥测将与环境的其余遥测一起导出。
  • 如果代理不存在(本地AEM SDK、单元测试或未启用OpenTelemetry的环境),则API不会执行任何操作,开销极小。 您可以将相同的代码部署到每个环境。
您添加的内容
如何
已发送给您的APM提供商
范围
Tracer或@WithSpan注释
是
span上的属性、事件和错误状态
Span API
是
与跟踪关联的日志条目
您现有的SLF4J日志记录
是,在WARN级别及以上启用日志时
NOTE
您的代码创建的自定义量度(如计数器和直方图)计划于即将发布的版本中推出。

设置您的项目 set-up-project

步骤1:添加OpenTelemetry API add-api

将OpenTelemetry API和检测注释添加到项目父pom.xml的dependencyManagement部分,范围为provided:

<properties>
    <opentelemetry.version>1.61.0</opentelemetry.version>
    <opentelemetry-instrumentation.version>2.27.0</opentelemetry-instrumentation.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-api</artifactId>
            <version>${opentelemetry.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry.instrumentation</groupId>
            <artifactId>opentelemetry-instrumentation-annotations</artifactId>
            <version>${opentelemetry-instrumentation.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

然后将两个依赖项(不带版本)添加到使用它们的每个包的pom.xml中,如core/pom.xml:

<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-instrumentation-annotations</artifactId>
</dependency>

使用opentelemetry-api 1.61.0和opentelemetry-instrumentation-annotations 2.27.0或更早版本。 它们与AEM as a Cloud Service运行的OpenTelemetry Java代理匹配,并且代理的更高版本继续支持它们。 在代理支持的版本之后添加的API功能不起作用。

步骤2:将API嵌入捆绑包中 embed-api

AEM as a Cloud Service不导出OpenTelemetry API包,因此使用该API的每个包都必须包含其自己的副本。 将以下说明添加到捆绑包的捆绑配置中。 对于AEM项目原型,它是core/pom.xml中的bnd-maven-plugin配置。 保留Import-Package指令已有的任何条目。

Import-Package: !io.opentelemetry.*,javax.annotation;version=0.0.0,*
-includepackage: io.opentelemetry.*
说明
为什么需要它
Import-Package开头的!io.opentelemetry.*
API引用AEM未提供的可选OpenTelemetry包,并且仅在存在这些包时才加载它们。 如果没有此排除项,则您的捆绑包将导入这些包,无法启动它,并且Cloud Manager构建失败。 请参阅疑难解答。
-includepackage: io.opentelemetry.*
将​complete API复制到包中。 代理仅在存在整个API时才将捆绑包的API副本连接到其自身的实施。 如果仅嵌入代码引用的包(例如使用-conditionalpackage),则将丢弃范围和属性,而不会出现错误。

每个使用该API的包都嵌入其自身的副本,并且代理会单独连接每个副本。

CAUTION
请勿将OpenTelemetry SDK、导出程序或SDK自动配置模块添加到您的项目中,也不要创建自己的OpenTelemetrySdk实例或调用GlobalOpenTelemetry.set()。 通过此方式发送的遥测绕过了代理,删除了发送了哪些数据中描述的Cloud Service基础结构详细信息,以及您配置的端点。

第3步:创建跟踪器 create-tracer

为代码库创建一个Tracer并在所有位置重复使用。 在WKND中,有一个小类包含它:

package com.adobe.aem.guides.wknd.core.telemetry;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Tracer;

public final class WkndTelemetry {

    /** Instrumentation scope: identifies telemetry produced by WKND code in your APM provider. */
    public static final String SCOPE = "com.adobe.aem.guides.wknd";

    public static final Tracer TRACER = GlobalOpenTelemetry.getTracer(SCOPE);

    private WkndTelemetry() {
    }
}

范围名称显示在您通过此跟踪器创建的每个跨度上,作为检测范围(或库名称)。 使用可标识代码的名称,如项目的基本Java包。

显示跨距的位置 where-spans-appear

在AEM as a Cloud Service上,页面请求已从AEM本身生成跨区。 您的代码创建的范围将添加到其中。 例如,下面是WKND Magazine页面跟踪中的图像列表组件的路径:

GET /*                                                     the request (AEM)
└─ SlingMainServlet.service                                Sling request processing (AEM)
   └─ AEM component: cq:Page                               (AEM)
      └─ AEM component: wknd/components/page               the page component (AEM)
         └─ HTL: libs.core.wcm.components.page.v3.page.page__002e__html
            └─ ...                                         containers and layout grids (AEM)
               └─ AEM component: wknd/components/image-list
                  └─ HTL: apps.wknd.components.image__002d__list.image__002d__list__002e__html
                     └─ ...                                spans that the Image List's Sling Model creates

在组件呈现时,其HTL脚本的跨度是当前跨度。 脚本加载且包含data-sly-use的Sling模型在该范围内运行,因此该模型创建的跨度将成为其子项。

每个请求范围都名为GET /*(或POST /*,以此类推)。 要查找特定页面的跟踪,请搜索请求范围的url.path属性,或添加您自己的属性,如示例3所示。

示例 examples

示例1:跟踪昂贵操作 trace-operation

您希望实现的目标 trace-operation-goal

WKND图像列表组件(wknd/components/image-list)显示一个页面列表,每个页面都具有其第一个图像。 为了查找该图像,其Sling模型ImageListImpl对列表中的每个项目运行一个QueryBuilder查询。 AEM自己的范围已显示这些查询:在列出五篇文章的WKND Magazine页面上,跟踪在图像列表的HTL范围下包含五个JCR query范围。 它们不会显示每个查询针对的列表项,无论它找到的是图像,还是处理结果所花费的时间。 原始代码使用System.currentTimeMillis()对查询进行计时,并将结果写入到DEBUG日志条目,该日志条目在生产环境中不可用。 您希望每个查询都以命名操作的形式显示在跟踪中,并包含该上下文。

代码 trace-operation-code

在ImageListImpl.findPageComponentResources()中,围绕查询创建一个范围:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Scope;

// ... build the QueryBuilder parameters, as before

final Span span = WkndTelemetry.TRACER.spanBuilder("ImageList.findPageComponentResources")
        .setAttribute("wknd.query.path", page.getContentResource().getPath())
        .setAttribute("wknd.query.resource_type", slingResourceType)
        .setAttribute("wknd.query.limit", limit)
        .startSpan();

try (Scope ignored = span.makeCurrent()) {
    // ... run the query and collect the resources, as before

    span.setAttribute("wknd.query.result_count", componentResources.size());
} catch (RuntimeException e) {
    span.recordException(e);
    span.setStatus(StatusCode.ERROR, "Image List query failed");
    throw e;
} finally {
    span.end();
}

return componentResources;
  • makeCurrent()将您的范围设置为块内运行的所有内容的父级,包括AEM为存储库查询创建的范围。
  • 始终在finally块中调用end()。 从不导出未结束的跨度。
  • 为跨度提供一个描述操作的固定名称,并将更改的值(如路径)置于属性中。

结果 trace-operation-outcome

在列出五篇文章的WKND杂志页面上,跟踪现在包含图像列表内的五个ImageList.findPageComponentResources跨区,每个列表项各一个。 每个对象都是AEM为其查询创建的JCR query范围的父级:

AEM component: wknd/components/image-list
└─ HTL: apps.wknd.components.image__002d__list.image__002d__list__002e__html
   ├─ ImageList.findPageComponentResources        your span
   │  └─ JCR query                                AEM
   ├─ ImageList.findPageComponentResources
   │  └─ JCR query
   └─ ...                                         three more

您的每个跨域都具有检测范围com.adobe.aem.guides.wknd和以下属性:

属性
示例值
wknd.query.path
/content/wknd/us/en/magazine/arctic-surfing/jcr:content
wknd.query.resource_type
wknd/components/image
wknd.query.limit
1
wknd.query.result_count
1

现在,您可以看到对于每个列表项,查询搜索了哪个页面,它是否找到了图像(wknd.query.result_count是0,而它没有),以及整个查找耗时(包括处理结果)。 在WKND Magazine页面上,每个查找大约需要6.5毫秒,其中存储库查询本身大约需要3.5毫秒:一半的时间用于处理结果,而AEM自己的跨度不显示这些结果。 由于查询计数会随着列表的长度而增加,因此您还可以看到查找占用的组件渲染时间。

示例2:使用注释跟踪方法 with-span

您希望实现的目标 with-span-goal

WKND的SocialImageImpl模型在每个页面上运行以填充og:image Meta标记。 如果作者未设置精选图像,则模型将遍历页面的内容树以查找第一个图像,并访问多达500个资源。 您希望了解该行走运行的频率,以及它访问树的频率,而不重新构建方法。

代码 with-span-code

使用@WithSpan注释方法:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.instrumentation.annotations.WithSpan;

@WithSpan("SocialImage.resolveFromFirstImageOnPage")
protected boolean resolveFromFirstImageOnPage() {
    Resource contentResource = currentPage.getContentResource();
    if (contentResource == null) {
        return false;
    }

    int[] visitedCount = new int[]{0};
    Resource firstImage = findFirstImage(contentResource, 0, visitedCount);

    // Span.current() is the span that @WithSpan started for this method.
    Span.current().setAttribute("wknd.social_image.nodes_visited", visitedCount[0]);
    Span.current().setAttribute("wknd.social_image.walk_truncated", visitedCount[0] > MAX_NODES_TO_VISIT);

    // ... use firstImage, as before
}
  • 代理在每次运行方法时都会启动一个范围,并在方法返回或引发异常时结束该范围。
  • @WithSpan使用公共、受保护以及私有方法。
  • 要将方法参数记录为属性,请使用@SpanAttribute注释该参数,例如void applyTemplate(@SpanAttribute("wknd.page.template") String template)。
  • 注释仅在代理运行时生效。 在本地AEM SDK和单元测试中,方法未更改地运行。

结果 with-span-outcome

SocialImage.resolveFromFirstImageOnPage范围仅出现在没有精选图像的页面的跟踪中:

属性
示例值
wknd.social_image.nodes_visited
21
wknd.social_image.walk_truncated
false
code.namespace
com.adobe.aem.guides.wknd.core.models.impl.SocialImageImpl (由代理添加)
code.function
resolveFromFirstImageOnPage (由代理添加)

搜索此范围名称可列出需要精选图像的页面。 如果wknd.social_image.walk_truncated为true,则漫游停止在500个资源,并且页面可能根本没有社交图像。

由@WithSpan创建的跨域承载代理的检测范围(io.opentelemetry.opentelemetry-instrumentation-annotations-1.16),而不是您的检测范围。

示例3:将业务上下文添加到当前范围 current-span

您希望实现的目标 current-span-goal

您希望通过代码知道页面社交图像来源的事实来过滤跟踪,而不是创建另一个范围。

代码 current-span-code

在SocialImageImpl中,向已处于活动状态的范围中添加一个属性:

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;

private static final AttributeKey<String> IMAGE_SOURCE = AttributeKey.stringKey("wknd.social_image.source");

@PostConstruct
protected void init() {
    if (currentPage == null) {
        return;
    }

    final String source;
    if (resolveFromFeaturedImage()) {
        source = "featured_image";
    } else if (resolveFromFirstImageOnPage()) {
        source = "first_image_on_page";
    } else {
        source = "none";
    }

    Span.current().setAttribute(IMAGE_SOURCE, source);
}
  • Span.current()返回最内层的活动范围,无论AEM还是您的代码创建了该范围。 对于从HTL加载的Sling模型,它是加载该模型的HTL脚本的范围。
  • 如果没有活动span,则调用不会执行任何操作。
  • 定义AttributeKey常量后,将使所有代码中的属性名称保持一致。

结果 current-span-outcome

页面customheaderlibs.html脚本的跨度(其中WKND加载此模型)携带值为featured_image、first_image_on_page或none的wknd.social_image.source。 搜索wknd.social_image.source为none的跟踪以查找在没有社交图像的情况下发布的页面。

在WKND网站上,每个页面都会报告first_image_on_page或none:没有页面设置特色图像,因此每个页面都会支付从示例2开始的内容树演练的费用。 在访问次数最多的页面上设置精选图像会移除该效果。

示例4:记录错误 record-errors

您希望实现的目标 record-errors-goal

WKND的ContentAIAvailabilityImpl模型检查是否通过读取OSGi配置配置了ContentAI 搜索。 如果查找失败,则模型会将内容人工智能视为未配置并记录警告。 页面仍会呈现,并且请求成功,因此很容易错过失败。 您希望失败显示在APM提供程序的错误视图中,附加到失败的操作。

代码 record-errors-code

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Scope;

private boolean resolveConfigured() {
    if (configurationAdmin == null) {
        return false;
    }
    final Span span = WkndTelemetry.TRACER.spanBuilder("ContentAIAvailability.resolveConfigured").startSpan();
    // Close the scope in finally, not with try-with-resources: try-with-resources closes it before
    // the catch block runs, and the warning logged there must carry this span's trace and span IDs.
    final Scope scope = span.makeCurrent();
    try {
        Configuration configuration = lookupConfiguration();
        // ... read the API key, as before
        span.setAttribute("wknd.content_ai.configured", isConfigured);
        return isConfigured;
    } catch (IOException e) {
        span.recordException(e);
        span.setStatus(StatusCode.ERROR, "Content AI configuration lookup failed");
        LOGGER.warn("Unable to determine whether Content AI is configured (PID {})", CONTENT_AI_CLIENT_PID, e);
        return false;
    } finally {
        scope.close();
        span.end();
    }
}
  • recordException()向范围中添加一个exception事件,该事件具有异常的类型、消息和栈栈跟踪。
  • setStatus(StatusCode.ERROR, ...)将范围标记为失败。 在表示失败的操作的范围上设置它。 请求本身不会标记为失败,因为组件已恢复并且页面已渲染。

结果 record-errors-outcome

跟踪包含状态为​ 错误 ​和描述为Content AI configuration lookup failed的ContentAIAvailability.resolveConfigured范围。 范围具有exception事件:

事件属性
示例值
exception.type
java.io.IOException
exception.message
Configuration store unavailable
exception.stacktrace
完整栈栈跟踪

span在APM提供程序中计为错误,因此无论您的提供程序显示失败的操作如何,它都会显示。 请求范围会保持其状态,因为请求返回了HTTP 200。

示例5:将日志与跟踪关联 correlate-logs

您希望实现的目标 correlate-logs-goal

当示例4中的内容人工智能查找失败时,您想要从日志中的警告转到生成该警告的请求的跟踪,然后再返回。

代码 correlate-logs-code

您不需要任何OpenTelemetry代码。 照常使用SLF4J进行记录,并在OpenTelemetry配置中启用日志(logs: true):

LOGGER.warn("Unable to determine whether Content AI is configured (PID {})", CONTENT_AI_CLIENT_PID, e);

代理将当前范围的跟踪和范围ID添加到每个日志条目。 由于ContentAIAvailabilityImpl在其从示例4起的范围为当前时进行记录,因此该警告链接到该范围。

结果 correlate-logs-outcome

您的APM提供商会收到如下所示的日志条目,并可显示其跟踪信息:

字段
示例值
严重性
WARN
主体
Unable to determine whether Content AI is configured (PID com.adobe.cq.wcm.core.components.internal.services.contentai.ContentAIClientImpl)
跟踪ID和span ID
ContentAIAvailability.resolveConfigured范围的ID
exception.type, exception.message, exception.stacktrace
记录的异常
检测范围
记录器名称,com.adobe.aem.guides.wknd.core.models.impl.contentai.ContentAIAvailabilityImpl
NOTE
仅发送WARN级别及以上的日志条目。 INFO和DEBUG条目不是。 不要依赖它们进行生产诊断:请将该信息记录为span属性。

最佳做法 best-practices

  • 名称在操作后跨越,而不是跨数据。 使用名称(如ImageList.findPageComponentResources),并将更改的值(如路径和ID)放入属性中。
  • 为您自己的属性名称​添加项目的命名空间,如wknd.。 这样可避免与OpenTelemetry语义约定和AEM使用的名称冲突。 请勿使用为AEM保留的aem.前缀。
  • 不要在span名称、属性、事件或日志消息中记录个人数据或密钥。 这包括名称、电子邮件地址、IP地址、令牌和可包含这些地址的查询字符串。
  • 为操作添加跨区,而不是为迭代添加跨区。 例如,示例2为内容树演练创建一个范围,并将访问的资源数记录为一个属性,而不是为每个资源创建一个范围。 AEM已经为单个页面创建了多个跨区(WKND Magazine页面大约创建了260个),每个跨区都会添加到APM提供商摄取的卷中。
  • 创建跟踪器一次,​并重用它们。
  • 在finally块中结束每个span,并在打开它的线程上每隔Scope关闭一次。
  • 将上下文传递给您自己启动的线程,例如 Context.current().wrap(runnable),以便在另一个线程上完成的工作将加入相同的跟踪。
  • 需要新的工作跟踪而不是请求。 在Sling作业、计划任务或事件侦听器中运行的代码没有请求范围。 您在此处创建的跨度会启动它们自己的跟踪,每次运行一个。

测试您的工具 test

  • 单元测试:​如果没有代理,API将不执行任何操作,因此您现有的单元测试将保持不变。
  • 生成:​在本地运行mvn clean install。 AEM分析器(Cloud Manager也运行)会检查是否能够解析捆绑包的导入。
  • 包内容:​检查您的包是否包含完整的API,如步骤2中所述。 例如,unzip -l core/target/<your-core-bundle>.jar | grep "io/opentelemetry/api/baggage/"必须列出类,捆绑包的META-INF/MANIFEST.MF不得导入任何io.opentelemetry包。
  • 您的APM提供程序:​部署到启用了OpenTelemetry的开发环境,请求运行代码的页面,然后搜索您的APM提供程序以查找您的跨域名称或您在步骤3中选择的检测范围。

疑难解答 troubleshooting

症状
原因和解决方案
Cloud Manager生成失败,出现[api-regions-exportsimports] ... is importing package(s) [io.opentelemetry.api.incubator, io.opentelemetry.api.incubator.internal, io.opentelemetry.sdk.autoconfigure] in start level 20 but no bundle is exporting these for that start level.
您的捆绑包导入可选的OpenTelemetry包。 在Import-Package的开头添加!io.opentelemetry.*。 请参阅步骤2。
您的捆绑包已安装,但未处于活动状态,带有missing requirement ... osgi.wiring.package=io.opentelemetry.api.incubator。
与上一行相同的原因和解决方案。
@WithSpan范围出现,但您的其他范围以及您使用Span.current()设置的属性未出现。
您的包仅包含API的一部分,因此代理不会连接您的副本。 添加-includepackage: io.opentelemetry.*。 请参阅步骤2。
您的范围都不显示。
检查是否为该环境启用了OpenTelemetry,以及部署代码后是否已运行部署。 检查代码路径是否正在运行,以及每个跨度是否都已结束。
范围显示为单独的跟踪,而不是显示在请求中。
跨度是在另一个线程上创建的,没有请求的上下文。 将上下文传递到线程,例如Context.current().wrap(runnable)。
您的日志条目不会显示。
仅发送WARN级别及更高级别的条目,并且仅在配置中启用日志时发送。
recommendation-more-help
experience-manager-cloud-service-help-main-toc