Instrumentação personalizada com OpenTelemetry custom-instrumentation

Quando a OpenTelemetry está habilitada para o seu ambiente, o AEM as a Cloud Service rastreia todas as solicitações e exporta métricas JVM, HTTP e AEM automaticamente. Essa telemetria descreve o AEM. Ela mostra que uma página levou 900 milissegundos para ser renderizada e quais componentes foram renderizados. Ele não pode mostrar o que seu próprio código estava fazendo, como para qual item de lista uma consulta de repositório era e se encontrou algo ou quais páginas retornam a uma pesquisa cara porque um autor deixou de fora um campo.

Você mesmo pode adicionar esse conhecimento com a API OpenTelemetry. As extensões e os atributos que seu código cria com a telemetria automática: mesmo nome de serviço, mesmo processamento, mesmo provedor de APM. Você não configura um endpoint, uma credencial ou um exportador em seu código.

Se você é novo no OpenTelemetry no AEM as a Cloud Service, comece com o tutorial Observabilidade via OpenTelemetry para saber como ele funciona. Siga Configurar OpenTelemetry para insights sobre aplicativos AEM para habilitá-lo para o seu ambiente.

Os exemplos nesta página instrumentam o site de referência WKND. Cada exemplo descreve o que você deseja alcançar, o código a ser adicionado e o resultado que você pode esperar em seu provedor de APM. O código instrumentado completo, incluindo testes de unidade que declaram nos spans, está na ramificação tutorial/otel-custom-instrumentation do repositório WKND.

NOTE
A instrumentação personalizada requer que o OpenTelemetry esteja habilitado para o seu ambiente. Consulte Configurar a integração.

Como a instrumentação personalizada funciona how-it-works

  • O agente Java OpenTelemetry que o Adobe executa no ambiente AEM contém o SDK OpenTelemetry e os exportadores. Seu código depende apenas da OpenTelemetry API.
  • Quando seu código chama GlobalOpenTelemetry, o agente o conecta à sua própria implementação. Suas extensões se tornam parte do rastreamento da solicitação que as executa e sua telemetria é exportada com o restante da telemetria do ambiente.
  • Quando o agente não está presente (o AEM SDK local, testes de unidade ou um ambiente sem OpenTelemetry ativado), a API não faz nada, a um custo insignificante. Você pode implantar o mesmo código em cada ambiente.
O que você adiciona
Como
Enviado para seu provedor de APM
Extensões
Uma Tracer ou a anotação @WithSpan
Sim
Atributos, eventos e status de erro em extensões
A API Span
Sim
Entradas de log correlacionadas a rastreamentos
Seu registro SLF4J existente
Sim, no nível WARN e superior, quando os logs estiverem habilitados
NOTE
Métricas personalizadas, como contadores e histogramas criados por seu código, estão planejadas para uma versão futura.

Configurar um projeto set-up-project

Etapa 1: adicionar a API OpenTelemetry add-api

Adicione a API OpenTelemetry e as anotações de instrumentação à seção dependencyManagement do pai do projeto pom.xml, com o escopo 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>

Em seguida, adicione ambas as dependências, sem versões, ao pom.xml de cada pacote que as utiliza, como 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>

Use o opentelemetry-api 1.61.0 e o opentelemetry-instrumentation-annotations 2.27.0 ou versões anteriores. Eles correspondem ao agente Java OpenTelemetry que o AEM as a Cloud Service executa e as versões posteriores do agente continuam a oferecer suporte a eles. Os recursos de API adicionados após a versão que o agente aceita não têm efeito.

Etapa 2: incorporar a API ao seu pacote embed-api

O AEM as a Cloud Service não exporta os pacotes da API OpenTelemetry, portanto, cada pacote que usa a API deve conter sua própria cópia. Adicione as instruções a seguir à configuração de ligação do seu pacote. Com o Arquétipo de Projeto do AEM, essa é a configuração bnd-maven-plugin em core/pom.xml. Mantenha todas as entradas que a instrução Import-Package já tenha.

Import-Package: !io.opentelemetry.*,javax.annotation;version=0.0.0,*
-includepackage: io.opentelemetry.*
Instrução
Por que é necessário
!io.opentelemetry.* no início de Import-Package
A API se refere aos pacotes OpenTelemetry opcionais que a AEM não fornece e os carrega somente se estiverem presentes. Sem essa exclusão, o pacote importa esses pacotes, não pode ser iniciado e a build do Cloud Manager falha. Consulte Solução de problemas.
-includepackage: io.opentelemetry.*
Copia a API complete para o seu pacote. O agente conecta a cópia da API do seu pacote à sua própria implementação somente quando toda a API está presente. Se você incorporar apenas os pacotes aos quais o código se refere, por exemplo, com -conditionalpackage, suas extensões e atributos serão descartados sem erros.

Cada pacote que usa a API incorpora sua própria cópia, e o agente conecta cada cópia separadamente.

CAUTION
Não adicione o SDK OpenTelemetry, um exportador ou o módulo de configuração automática do SDK ao seu projeto e não crie sua própria instância do OpenTelemetrySdk ou chame GlobalOpenTelemetry.set(). A telemetria enviada dessa maneira ignora o agente, a remoção dos detalhes da infraestrutura do Cloud Service descrita em Quais dados são enviados e o ponto de extremidade configurado.

Etapa 3: criar um rastreador create-tracer

Crie um Tracer para sua base de código e reutilize-o em qualquer lugar. Na WKND, uma pequena classe a mantém:

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() {
    }
}

O nome do escopo aparece em cada span criado por meio desse rastreador, como o escopo de instrumentação (ou nome da biblioteca). Use um nome que identifique o código, como o pacote Java básico do projeto.

Onde as extensões aparecem where-spans-appear

No AEM as a Cloud Service, uma solicitação de página já produz extensões do próprio AEM. As extensões criadas pelo código são adicionadas dentro delas. Por exemplo, este é o caminho para o componente Lista de imagens em um rastro da página da 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

Enquanto um componente é renderizado, a extensão de seu script HTL é a extensão atual. Um Modelo Sling que o script carrega com data-sly-use é executado dentro desse intervalo, de modo que os intervalos que o modelo cria se tornam seus filhos.

Cada intervalo de solicitação é nomeado como GET /* (ou POST /* e assim por diante). Para localizar os rastreamentos de uma página específica, pesquise no atributo url.path do intervalo da solicitação ou adicione seus próprios atributos, conforme mostrado no Exemplo 3.

Exemplos examples

Exemplo 1: Rastrear uma operação cara trace-operation

O que você deseja alcançar trace-operation-goal

O componente Lista de Imagens WKND (wknd/components/image-list) exibe uma lista de páginas, cada uma com sua primeira imagem. Para localizar essa imagem, seu Modelo Sling, ImageListImpl, executa uma consulta QueryBuilder para cada item na lista. As próprias extensões do AEM já mostram essas consultas: na página da WKND Magazine, que lista cinco artigos, o rastreamento contém cinco extensões JCR query no intervalo HTL da Lista de imagens. Eles não mostram para qual item de lista cada consulta era, se encontrou uma imagem ou o tempo gasto processando os resultados. O código original cronometrava a consulta com System.currentTimeMillis() e gravava o resultado em uma entrada de log DEBUG, que não está disponível na produção. Você deseja que cada consulta apareça no rastreamento como uma operação nomeada, com esse contexto.

O código trace-operation-code

Em ImageListImpl.findPageComponentResources(), crie uma extensão em torno da consulta:

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() torna seu span o pai de tudo o que é executado dentro do bloco, incluindo os limites que o AEM cria para a consulta do repositório.
  • Sempre chame end() em um bloco finally. Um trecho que não foi finalizado nunca é exportado.
  • Dê à extensão um nome fixo que descreva a operação e coloque valores que variam, como caminhos, em atributos.

O resultado trace-operation-outcome

Na página da Revista WKND, que lista cinco artigos, o rastreamento agora contém cinco ImageList.findPageComponentResources extensões dentro da Lista de Imagens, uma para cada item da lista. Cada um é o pai do span JCR query que o AEM cria para sua 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

Cada uma das extensões tem o escopo de instrumentação com.adobe.aem.guides.wknd e os seguintes atributos:

Atributo
Exemplo de valor
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

Agora é possível ver, para cada item de lista, qual página a consulta pesquisou, se encontrou uma imagem (wknd.query.result_count é 0 quando não encontrou) e quanto tempo demorou a pesquisa inteira, incluindo o processamento dos resultados. Na página da WKND Magazine, cada pesquisa leva cerca de 6,5 milissegundos, dos quais a própria consulta do repositório leva cerca de 3,5: metade do tempo entra no processamento dos resultados, que as próprias extensões do AEM não mostram. Como a contagem de consultas cresce com o comprimento da lista, você também pode ver quanto do tempo de renderização do componente as pesquisas levam em conta.

Exemplo 2: Rastrear um método com uma anotação with-span

O que você deseja alcançar with-span-goal

O modelo SocialImageImpl da WKND é executado em cada página para preencher a meta tag og:image. Se um autor não tiver definido uma imagem em destaque, o modelo percorrerá a árvore de conteúdo da página para encontrar a primeira imagem, visitando até 500 recursos. Você quer saber com que frequência essa caminhada é realizada e quanto da árvore ele visita, sem reestruturar o método.

O código with-span-code

Anotar o método com @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
}
  • O agente inicia um intervalo cada vez que o método é executado e o encerra quando o método retorna ou lança uma exceção.
  • @WithSpan funciona em métodos públicos, protegidos e privados.
  • Para registrar um parâmetro de método como um atributo, anotar o parâmetro com @SpanAttribute, por exemplo void applyTemplate(@SpanAttribute("wknd.page.template") String template).
  • A anotação tem efeito somente onde o agente é executado. No AEM SDK local e em testes de unidade, o método é executado inalterado.

O resultado with-span-outcome

Um span SocialImage.resolveFromFirstImageOnPage aparece somente nos rastreamentos de páginas que não têm imagem em destaque:

Atributo
Exemplo de valor
wknd.social_image.nodes_visited
21
wknd.social_image.walk_truncated
false
code.namespace
com.adobe.aem.guides.wknd.core.models.impl.SocialImageImpl (adicionado pelo agente)
code.function
resolveFromFirstImageOnPage (adicionado pelo agente)

Procure por esse nome de span para listar as páginas que precisam de uma imagem em destaque. Se wknd.social_image.walk_truncated for true, a caminhada parou em 500 recursos e a página pode não ter nenhuma imagem social.

As extensões criadas por @WithSpan carregam o escopo de instrumentação do agente (io.opentelemetry.opentelemetry-instrumentation-annotations-1.16), não o seu.

Exemplo 3: Adicionar contexto de negócios ao intervalo atual current-span

O que você deseja alcançar current-span-goal

Você deseja filtrar os rastreamentos pelo fato de que seu código sabe, aqui, de onde veio a imagem social da página, sem criar outro intervalo.

O código current-span-code

Em SocialImageImpl, adicione um atributo ao span que já está ativo:

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() retorna o span mais ativo, seja o AEM ou seu código o tenha criado. Para um Modelo do Sling carregado do HTL, é o span do script HTL que carregou o modelo.
  • Se nenhum span estiver ativo, a chamada não fará nada.
  • Definir constantes AttributeKey uma vez mantém os nomes de atributos consistentes em todo o código.

O resultado current-span-outcome

A extensão do script customheaderlibs.html da página, onde o WKND carrega esse modelo, carrega wknd.social_image.source com o valor featured_image, first_image_on_page ou none. Procure rastreamentos onde wknd.social_image.source é none para localizar as páginas publicadas sem uma imagem social.

No site WKND, cada página relata first_image_on_page ou none: nenhuma página define uma imagem em destaque, portanto, cada página paga pela caminhada da árvore de conteúdo de Exemplo 2. Configurar uma imagem em destaque nas páginas mais visitadas remove esse trabalho.

Exemplo 4: Registrar um erro record-errors

O que você deseja alcançar record-errors-goal

O modelo ContentAIAvailabilityImpl da WKND verifica se a Pesquisa com IA de conteúdo está configurada ao ler uma configuração OSGi. Se a pesquisa falhar, o modelo tratará a IA de conteúdo como não configurada e registrará um aviso. A página ainda é renderizada e a solicitação é bem-sucedida, portanto, a falha é fácil de perder. Você deseja que a falha apareça nas visualizações de erro do provedor de APM, anexadas à operação que falhou.

O código 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() adiciona um evento exception ao span, com o tipo, mensagem e rastreamento de pilha da exceção.
  • setStatus(StatusCode.ERROR, ...) marca a extensão como com falha. Defina-o no span que representa a operação que falhou. A própria solicitação não é marcada como com falha, pois o componente foi recuperado e a página foi renderizada.

O resultado record-errors-outcome

O rastreamento contém um ContentAIAvailability.resolveConfigured span com o status Erro e a descrição Content AI configuration lookup failed. O span tem um evento exception:

Atributo de evento
Exemplo de valor
exception.type
java.io.IOException
exception.message
Configuration store unavailable
exception.stacktrace
O rastreamento de pilha completa

O span conta como um erro no provedor de APM, portanto, é exibido sempre que o provedor mostra operações com falha. O intervalo da solicitação mantém seu status, pois a solicitação retornou HTTP 200.

Exemplo 5: correlacionar logs com rastreamentos correlate-logs

O que você deseja alcançar correlate-logs-goal

Quando a pesquisa da IA de Conteúdo no Exemplo 4 falhar, você desejará ir do aviso em seus logs para o rastreamento da solicitação que o produziu e voltar.

O código correlate-logs-code

Você não precisa de nenhum código OpenTelemetry. Faça logon com SLF4J como de costume e habilite os logs (logs: true) na sua configuração de OpenTelemetry:

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

O agente adiciona as IDs de rastreamento e span da extensão atual a cada entrada de log. Como ContentAIAvailabilityImpl faz logon enquanto seu span de Exemplo 4 é atual, o aviso é vinculado a esse span.

O resultado correlate-logs-outcome

Seu provedor APM recebe uma entrada de log como a seguinte e pode mostrá-la com seu rastreamento:

Texto
Exemplo de valor
Severidade
WARN
Corpo
Unable to determine whether Content AI is configured (PID com.adobe.cq.wcm.core.components.internal.services.contentai.ContentAIClientImpl)
ID de rastreamento e ID de span
As IDs do span ContentAIAvailability.resolveConfigured
exception.type, exception.message, exception.stacktrace
A exceção que foi registrada
Escopo da instrumentação
O nome do agente, com.adobe.aem.guides.wknd.core.models.impl.contentai.ContentAIAvailabilityImpl
NOTE
Somente entradas de log no nível WARN e superior são enviadas. INFO e DEBUG entradas não estão. Não confie neles para diagnósticos de produção: registre as informações como um atributo span.

Práticas recomendadas best-practices

  • O nome se estende após as operações, não pelos dados. Use nomes como ImageList.findPageComponentResources e coloque valores que variam, como caminhos e IDs, em atributos.
  • Prefixe seus próprios nomes de atributos com um namespace para o seu projeto, como wknd.. Isso evita conflitos com as convenções semânticas de OpenTelemetry e com nomes que a AEM usa. Não use o prefixo aem., que é reservado para o AEM.
  • Não registre dados pessoais ou segredos em nomes de extensão, atributos, eventos ou mensagens de log. Isso inclui nomes, endereços de email, endereços IP, tokens e cadeias de caracteres de consulta que podem contê-los.
  • Adicionar intervalos para operações, não iterações. Por exemplo, o Exemplo 2 cria um span para a movimentação da árvore de conteúdo e registra o número de recursos visitados como um atributo, em vez de criar um span para cada recurso. O AEM já cria muitas extensões para uma única página (cerca de 260 para a página da WKND Magazine) e cada extensão adiciona ao volume que o seu provedor de APM assimila.
  • Crie rastreadores uma vez, e reutilize-os.
  • Encerre cada span em um bloco finally e feche a cada Scope no thread que o abriu.
  • Passe o contexto para as threads que você mesmo inicia, por exemplo, com Context.current().wrap(runnable), para que o trabalho concluído em outra thread se junte ao mesmo rastreamento.
  • Espere um novo rastreamento para trabalhar fora de uma solicitação. O código que é executado em um trabalho do Sling, uma tarefa agendada ou um ouvinte de eventos não tem um intervalo de solicitação. As extensões criadas lá iniciam seus próprios rastreamentos, um para cada execução.

Testar sua instrumentação test

  • Testes de unidade: sem o agente, a API não faz nada, portanto, seus testes de unidade existentes continuam passando inalterados.
  • Build: Execute mvn clean install localmente. O analisador do AEM, que o Cloud Manager também executa, verifica se as importações do seu pacote podem ser resolvidas.
  • Conteúdo do pacote: verifique se o seu pacote contém a API completa, conforme descrito em Etapa 2. Por exemplo, unzip -l core/target/<your-core-bundle>.jar | grep "io/opentelemetry/api/baggage/" deve listar classes e META-INF/MANIFEST.MF do pacote não deve importar nenhum pacote io.opentelemetry.
  • Seu provedor de APM: implante em um ambiente de desenvolvimento que tenha OpenTelemetry habilitado, solicite uma página que execute seu código e pesquise seu provedor de APM quanto aos nomes de extensão ou quanto ao escopo de instrumentação escolhido em Etapa 3.

Resolução de problemas troubleshooting

Sintoma
Causa e solução
A compilação do Cloud Manager falha com [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.
Seu pacote importa pacotes OpenTelemetry opcionais. Adicionar !io.opentelemetry.* no início de Import-Package. Consulte Etapa 2.
Seu pacote está instalado, mas não ativo, com missing requirement ... osgi.wiring.package=io.opentelemetry.api.incubator.
Mesma causa e solução da linha anterior.
@WithSpan extensões são exibidas, mas suas outras extensões e os atributos definidos com Span.current() não.
Seu pacote contém apenas parte da API, de modo que o agente não conecta sua cópia da API. Adicionar -includepackage: io.opentelemetry.*. Consulte Etapa 2.
Nenhuma das extensões é exibida.
Verifique se OpenTelemetry está habilitado para o ambiente e se uma implantação foi executada desde que você implantou seu código. Verifique se o caminho do código foi executado e se cada span foi encerrado.
Suas extensões aparecem como rastreamentos separados em vez de dentro da solicitação.
O span foi criado em outro thread sem o contexto da solicitação. Passar o contexto para o thread, por exemplo, com Context.current().wrap(runnable).
Suas entradas de log não aparecem.
Somente as entradas no nível WARN e superior são enviadas, e somente quando os logs estão habilitados na sua configuração.
recommendation-more-help
experience-manager-cloud-service-help-main-toc