Engenharia

Como fazer instrumentação do seu aplicativo Go com o agente Go do Elastic APM

O Elastic APM (Monitoramento de performance de aplicação) fornece informações sofisticadas do desempenho e da visibilidade de aplicativos para cargas de trabalho distribuídas, com suporte oficial para inúmeras linguagens, incluindo Go, Java, Ruby, Python e JavaScript (Node.js e RUM (Monitoramento real de usuário) para o navegador).

Para obter essas informações sobre desempenho, você deve fazer a instrumentação da aplicação. Instrumentação é o ato de alterar o código da aplicação para medir seu comportamento. Para algumas das linguagens com suporte, basta instalar um agente. Por exemplo, os aplicativos Java podem ser instrumentados automaticamente usando um simples flag ‑javaagent, que usa instrumentação de bytecode, ou seja, o processo de manipular o bytecode Java compilado, normalmente quando uma classe Java é carregada ao iniciar o programa. Além disso, é comum um único thread assumir o controle de uma operação do início ao fim, por isso o armazenamento local de thread pode ser usado para correlacionar operações.

Falando em termos gerais, os programas em Go são compilados em código de máquina nativo, que não é tão receptivo à instrumentação automatizada. Além disso, o modelo de threading dos programas em Go é diferente da maioria das outras linguagens. Em um programa em Go, um “goroutine” que executa código pode se mover entre threads de sistema operacional, e as operações lógicas geralmente abrangem vários goroutines. Sendo assim, como fazer a instrumentação de um aplicativo em Go?

Neste artigo, vamos analisar como fazer a instrumentação de um aplicativo em Go com o Elastic APM, para capturar dados detalhados do desempenho de tempo de resposta (rastreamento), capturar metrics de infraestrutura e aplicativo e integrar-se aos logs — a trifecta da observabilidade. Vamos desenvolver um aplicativo e sua instrumentação ao longo do artigo, abordando os seguintes tópicos em sequência:

Rastreamento de solicitações Web

O agente Go do Elastic APM fornece uma API para operações de “rastreamento”, como solicitações de entrada feitas a um servidor. O rastreamento de uma operação envolve a gravação de eventos que descrevem a operação, por exemplo, nome da operação, tipo/categoria e alguns atributos como IP de origem, usuário autenticado etc. O evento também gravará quando a operação foi iniciada, quanto tempo ela durou e os identificadores que descrevem a linhagem da operação.

O agente Go do Elastic APM fornece inúmeros módulos para fazer a instrumentação de várias estruturas da Web, estruturas de RPC, drivers de banco de dados; e também para fazer a integração com várias estruturas de logs. Confira a lista completa das tecnologias com suporte.

Vamos adicionar a instrumentação do Elastic APM a um serviço Web simples usando o roteador gorilla/mux e mostrar como seria a captura do desempenho por meio do Elastic APM.

Este é o código original, não instrumentado:

package main                                                 
import (                                                     
        "fmt"                                                
        "log"                                                
        "net/http"                                           
        "github.com/gorilla/mux"                             
)                                                            
func helloHandler(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintf(w, "Hello, %s!\n", mux.Vars(req)["name"])
}                                                            
func main() {                                                
        r := mux.NewRouter()                                 
        r.HandleFunc("/hello/{name}", helloHandler)          
        log.Fatal(http.ListenAndServe(":8000", r))           
}

Para fazer a instrumentação das solicitações servidas pelo roteador gorilla/mux, você precisará de uma versão recente do gorilla/mux (v1.6.1 ou mais recente) com suporte a middleware. Depois, bastará importar go.elastic.co/apm/module/apmgorilla e adicionar a seguinte linha de código:

r.Use(apmgorilla.Middleware())

O middleware apmgorilla relatará cada solicitação ao APM Server como uma transação. Vamos dar uma pequena pausa na instrumentação e ver sua aparência na UI do APM.

Visualização do desempenho

Fizemos a instrumentação do nosso serviço Web, mas ele não tem nenhum local para enviar os dados. Por padrão, os agentes do APM tentarão enviar os dados a um APM Server em http://localhost:8200. Vamos configurar um stack novo, usando a recém-lançada versão Elastic Stack 7.0.0. Você pode fazer download da implantação padrão do stack gratuitamente ou pode iniciar uma avaliação grátis por 14 dias do Elasticsearch Service no Elastic Cloud. Se preferir executar por conta própria, você poderá encontrar o exemplo da configuração do Docker Compose em https://github.com/elastic/stack-docker.

Depois de configurar o stack, você poderá configurar o aplicativo para enviar dados ao APM Server. Você precisará saber a URL e o token secreto do APM Server. Quando você usa o Elastic Cloud, eles podem ser encontrados na página “Activity” (Atividade) durante a implantação e na página “APM” depois que a implantação é concluída. Durante a implantação, você também deve anotar a senha do Elasticsearch e do Kibana, porque não poderá vê-la novamente depois (apesar de poder redefini-la se necessário).

Elastic APM no Cloud Console do Elasticsearch Service

O agente Go do APM é configurado por meio de variáveis de ambiente. Para configurar a URL e o token secreto do APM Server, exporte as seguintes variáveis de ambiente para que sejam escolhidas pelo seu aplicativo:

export ELASTIC_APM_SERVER_URL=https://bdf8658ddda74d47af1875242c3ef203.apm.europe-west1.gcp.cloud.es.io:443
export ELASTIC_APM_SECRET_TOKEN=H9U312SRGzbG7M0Yp6

Agora, se executarmos o programa instrumentado, deveremos ver dentro de instantes alguns dados na UI do APM. O agente periodicamente enviará metrics: CPU e utilização de memória, além de estatísticas de tempo de execução de Go. Sempre que uma solicitação for atendida, o agente também gravará uma transação; essas serão armazenadas em buffer e enviadas em lotes, por padrão, a cada 10 segundos. Assim, vamos executar o serviço, enviar algumas solicitações e ver o que acontece.

Para verificar se esses eventos estão sendo enviados ao APM Server com êxito, podemos definir algumas variáveis de ambiente adicionais:

export ELASTIC_APM_LOG_FILE=stderr
export ELASTIC_APM_LOG_LEVEL=debug

Agora, inicie o aplicativo (hello.go contém o programa instrumentado de antes):

go run hello.go

Depois, usaremos github.com/rakyll/hey para enviar algumas solicitações ao servidor:

go get -u github.com/rakyll/hey
hey http://localhost:8000/hello/world

Na saída do aplicativo, você deverá ver algo como:

{"level":"debug","time":"2019-03-28T20:33:56+08:00","message":"sent request with 200 transactions, 0 spans, 0 errors, 0 metricsets"}

E na saída de hey, você deverá ver diversas estatísticas, incluindo um histograma das latências de tempo de resposta. Se agora abrirmos o Kibana e navegarmos até a UI do APM, deveremos ver um serviço chamado “hello”, com um grupo de transações chamado “/hello/{name}”. Vamos ver:

Uma lista de serviços disponíveis no Elastic APM

Detalhamento do serviço 'hello' no Elastic APM

Talvez você esteja se perguntando: como o agente sabe qual nome atribuir ao serviço; e por que o padrão de rotas é usado em vez do URI de solicitação? A primeira pergunta é fácil de responder: se você não especificar o nome do serviço (por meio de uma variável de ambiente), o nome binário do programa será usado. Nesse caso, o programa será compilado para um binário chamado “hello”.

Já o motivo de usarmos o padrão de rotas é permitir agregações. Se agora clicarmos na transação, poderemos ver um histograma das latências de tempo de resposta.

Latências de tempo de resposta no Elastic APM

Observe que, mesmo que estejamos agregando no padrão de rotas, a URL solicitada completa está disponível nas propriedades da transação.

Rastreamento de consultas SQL

Em um aplicativo normal, haverá uma lógica mais complexa envolvendo serviços externos como bancos de dados, caches e assim por diante. Quando você está tentando diagnosticar problemas de desempenho em seu aplicativo, é crucial ser capaz de ver as interações com esses serviços é crucial.

Vamos analisar como podemos observar as consultas SQL feitas pelo aplicativo em Go.

Para fins de demonstração, usaremos um banco de dados SQLite incorporado, mas, desde que estejamos usando database/sql, não importará qual driver usemos.

Para rastrear as operações database/sql, fornecemos o módulo de instrumentação go.elastic.co/apm/module/apmsql. O módulo apmsql faz instrumentação de drivers de database/sql para relatar as operações de banco de dados como durações em uma transação. Para usar esse módulo, você precisará fazer alterações na maneira de registrar e abrir o driver do banco de dados.

Normalmente em um aplicativo você importará um pacote de driver de database/sql para registrar o driver, por exemplo:

import _ “github.com/mattn/go-sqlite3” // registers the “sqlite3” driver

Fornecemos vários pacotes de conveniência para fazer a mesma coisa, mas que registram versões instrumentadas dos mesmos drivers, por exemplo, para o SQLite3, você importaria da seguinte maneira:

import _ "go.elastic.co/apm/module/apmsql/sqlite3"

Em segundo plano, esse usa o método apmsql.Register, que equivale a chamar sql.Register, mas faz instrumentação do driver registrado. Sempre que você usar apmsql.Register, deverá usar pmsql.Open para abrir uma conexão, em vez de usar sql.Open:

import (
        “go.elastic.co/apm/module/apmsql”
        _ "go.elastic.co/apm/module/apmsql/sqlite3"
)
var db *sql.DB
func main() {
        var err error
        db, err = apmsql.Open("sqlite3", ":memory:")
        if err != nil {
                log.Fatal(err)
        }
        if _, err := db.Exec("CREATE TABLE stats (name TEXT PRIMARY KEY, count INTEGER);"); err != nil {
                log.Fatal(err)
        }
        ...
}

Anteriormente mencionamos que, diferentemente de muitas outras linguagens, no Go não há uma estrutura de armazenamento local de thread para vincular as operações relacionadas. Em vez disso, você deve propagar explicitamente o contexto. Quando iniciamos a gravação de uma transação para uma solicitação Web, armazenamos uma referência à transação em andamento no contexto da solicitação, disponível através do método net/http.Request.Context. Vamos ver como é a aparência disso gravando quantas vezes cada nome foi visto, relatando as consultas ao banco de dados ao Elastic APM.

var db *sql.DB
func helloHandler(w http.ResponseWriter, req *http.Request) {       
        vars := mux.Vars(req)
        name := vars[“name”]
        requestCount, err := updateRequestCount(req.Context(), name)
        if err != nil {                                             
                panic(err)                                          
        }                                                           
        fmt.Fprintf(w, "Hello, %s! (#%d)\n", name, requestCount)    
}                                                                   
// updateRequestCount incrementa uma contagem para o nome no banco de dados, retornando a nova contagem.
func updateRequestCount(ctx context.Context, name string) (int, error) {                                                    
        tx, err := db.BeginTx(ctx, nil)                                                                                     
        if err != nil {                                                                                                     
                return -1, err                                                                                              
        }                                                                                                                   
        row := tx.QueryRowContext(ctx, "SELECT count FROM stats WHERE name=?", name)                                        
        var count int                                                                                                       
        switch err := row.Scan(&count); err {                                                                               
        case nil:                                                                                                           
                count++                                                                                                     
                if _, err := tx.ExecContext(ctx, "UPDATE stats SET count=? WHERE name=?", count, name); err != nil {        
                        return -1, err                                                                                      
                }                                                                                                           
        case sql.ErrNoRows:
                count = 1                                                                                                 
                if _, err := tx.ExecContext(ctx, "INSERT INTO stats (name, count) VALUES (?, ?)", name, count); err != nil {
                        return -1, err                                                                                      
                }                                                                                                           
        default:                                                                                                            
                return -1, err                                                                                              
        }                                                                                                                   
        return count, tx.Commit()                                                                                           
}

Há dois detalhes cruciais a ressaltar sobre esse código:

  • Estamos usando os métodos *Context de database/sql (ExecContext, QueryRowContext)
  • Passamos o contexto da solicitação circundante para essas chamadas de método.

O driver do banco de dados instrumentado por apmsql espera encontrar uma referência à transação em andamento no contexto fornecido; é assim que a operação de banco de dados relatada está associada ao manipulador de solicitação que a está chamando. Vamos enviar algumas solicitações a esse serviço restruturado e ver como ele funciona:

Busca por latência em transações SQL com o Elastic APM

Observe que os nomes de duração do banco de dados não retêm a instrução SQL completa, mas apenas uma parcela dela. Isso permite que alguém agregue mais facilmente as durações que representam operações em um determinado nome de tabela, por exemplo. Clicando na duração, você pode ver a instrução SQL completa nos detalhes da duração:

Análise dos detalhes da duração de uma consulta SQL no Elastic APM

Rastreamento de durações personalizadas

Na seção anterior, apresentamos as durações de consulta ao banco de dados relativas aos rastreamentos. Se você conhecer bem o serviço, poderá saber imediatamente que essas duas consultas fazem parte da mesma operação (consulte e atualize um contador); outra pessoa talvez não saiba. Além disso, se houver algum processamento significativo ocorrendo entre ou em torno dessas consultas, poderá ser útil atribuir isso ao método “updateRequestCount”. Podemos fazer isso relatando uma duração personalizada para essa função.

Você pode relatar uma duração personalizada de várias maneiras, e a mais fácil é usando apm.StartSpan. Você deve repassar a essa função um contexto que contenha uma transação, além de um nome de duração e um tipo. Vamos criar uma duração chamada “updateRequestCount”:

func updateRequestCount(ctx context.Context, name string) (int, error) {             
    span, ctx := apm.StartSpan(ctx, “updateRequestCount”, “custom”)
    defer span.End()
    ...
}

Como podemos ver pelo código anterior, apm.StartSpan retorna uma duração e um novo contexto. Esse novo contexto deve ser usado no lugar do contexto repassado; ele contém a nova duração. A seguir está a aparência dele na UI agora:

Rastreamento de durações personalizadas no Elastic APM

Rastreamento de solicitações HTTP de saída

O que descrevemos até o momento é realmente um rastreamento de processo único. Mesmo que vários serviços estejam envolvidos, estamos rastreando somente em um único processo: solicitações de entrada e consultas a bancos de dados da perspectiva do cliente.

Os microsserviços se tornaram cada vez mais um lugar comum nos últimos anos. Antes do advento dos microsserviços, a SOA (arquitetura orientada a serviços) era outra abordagem conhecida para dividir aplicativos monolíticos em componentes modulares. O efeito, em qualquer dos casos, é um aumento na complexidade, o que complica a observabilidade. Agora não só precisamos associar as operações em um processo, como também entre os processos, e possivelmente (provavelmente) em diferentes computadores, inclusive diferentes datacenters ou serviços de terceiros.

A maneira como manipulamos o rastreamento dentro e entre os processos no agente Go do Elastic APM é praticamente a mesma. Por exemplo, para rastrear uma solicitação HTTP de saída, você deve fazer a instrumentação do cliente HTTP, além de garantir que o contexto da solicitação circundante seja propagado para a solicitação de saída. O cliente instrumentado usará isso para criar uma duração e também para injetar cabeçalhos na solicitação HTTP de saída. Vamos ver como é a aparência disso na prática:

// apmhttp.WrapClient faz a instrumentação de http.Client fornecido
client := apmhttp.WrapClient(http.DefaultClient)
// Se “ctx” contém uma referência a uma transação em andamento, a chamada a seguir iniciará uma nova duração.
resp, err := client.Do(req.WithContext(ctx))
…
resp.Body.Close() // the span is ended when the response body is consumed or closed

Se essa solicitação de saída for manipulada por outro aplicativo instrumentado com o Elastic APM, você acabará tendo um “rastreamento distribuído” — um rastreamento (coleção de transações e durações relacionadas) que cruza os serviços. O cliente instrumentado injetará um cabeçalho identificando a duração para a solicitação HTTP de saída, e o serviço recebido vai extrair o cabeçalho e usá-lo para correlacionar a duração do cliente à transação que ele grava. Isso tudo é manipulado pelos vários módulos de instrumentação de estrutura da Web fornecidos em go.elastic.co/apm/module.

Para demonstrar isso, vamos estender nosso serviço para adicionar um pouco de trivialidade à resposta: o número de bebês nascidos com nome em 2018, no sul da Austrália. O serviço “hello” obterá essas informações de um segundo serviço por meio da API baseada em HTTP. O código para esse segundo serviço é omitido por questões de brevidade, mas você pode imaginar que ele está implementado e instrumentado praticamente da mesma maneira que o serviço “hello”.

func helloHandler(w http.ResponseWriter, req *http.Request) {                             
        ...                                                                               
        stats, err := getNameStats(req.Context(), name)                                   
        if err != nil {
                panic(err)
        }                                                                                 
        fmt.Fprintf(w, "Hello, %s! (#%d)\n", name, requestCount)                          
        fmt.Fprintf(w, "In %s, %d: ", stats.Region, stats.Year)                           
        switch n := stats.Male.N + stats.Female.N; n {                                    
        case 1:                                                                           
                fmt.Fprintf(w, "there was 1 baby born with the name %s!\n", name)         
        default:                                                                          
                fmt.Fprintf(w, "there were %d babies born with the name %s!\n", n, name)  
        }                                                                                 
}                                                                                         
type nameStatsResults struct {                                                                    
        Region string                                                                             
        Year   int                                                                                
        Male   nameStats                                                                          
        Female nameStats                                                                          
}                                                                                                 
type nameStats struct {                                                                           
        N    int                                                                                  
        Rank int                                                                                  
}                                                                                                 
func getNameStats(ctx context.Context, name string) (nameStatsResults, error) {                   
        span, ctx := apm.StartSpan(ctx, "getNameStats", "custom")                                 
        defer span.End()                                                                          
        req, _ := http.NewRequest("GET", "http://localhost:8001/stats/"+url.PathEscape(name),
 nil)
        // Faça instrumentação do cliente HTTP e adicione o contexto circundante à solicitação.
        // Isso fará com que uma duração seja gerada para a solicitação HTTP de saída, incluindo
        // um cabeçalho de rastreamento distribuído para continuar o rastreamento no serviço de destino.
        client := apmhttp.WrapClient(http.DefaultClient)                                          
        resp, err := client.Do(req.WithContext(ctx))                                              
        if err != nil {                                                                           
                return nameStatsResults{}, err                                                    
        }                                                                                         
        defer resp.Body.Close()                                                                   
        var res nameStatsResults                                                                  
        if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {                           
                return nameStatsResults{}, err                                                    
        }                                                                                         
        return res, nil                                                                           
}

Com ambos os serviços instrumentados, agora vemos um rastreamento distribuído para cada solicitação feita ao serviço “hello”:

Rastreamento distribuído no Elastic APM

Você pode ler mais sobre o tópico de rastreamento distribuído na postagem de blog de Adam Quan, Distributed Tracing, OpenTracing and Elastic APM (Rastreamento distribuído, OpenTracing e Elastic APM).

Rastreamento de exceção pânico

Como vimos, os módulos de instrumentação de estrutura da Web fornecem um middleware que grava transações. Além disso, eles também vão capturar os pânicos e relatá-los ao Elastic APM, para auxiliar na investigação das solicitações com falha. Vamos experimentar isso modificando updateRequestCount para entrar em pânico quando ele vir caracteres não ASCII:

func updateRequestCount(ctx context.Context, name string) (int, error) {
    span, ctx := apm.StartSpan(ctx, “updateRequestCount”, “custom”)
    defer span.End()
    if strings.IndexFunc(name, func(r rune) bool {return r >= unicode.MaxASCII}) >= 0 {
        panic(“non-ASCII name!”)
    }
    ...
}

Agora envie uma solicitação com alguns caracteres não ASCII:

curl -f http://localhost:8000/hello/世界
curl: (22) The requested URL returned error: 500 Internal Server Error

Puxa... Qual poderia ser o problema? Vamos analisar a UI do APM, na página Errors e procurar pelo serviço “hello”:

Localização de erros de serviço no Elastic APM

Aqui podemos ver que ocorreu um erro, incluindo a mensagem de erro (panic), “non-ASCII name!” (nome nã ASCII!); e o nome da função em que o pânico se originou, updateRequestCount. Clicando no nome do erro, estamos vinculados aos detalhes da instância mais recente desse erro. Na página de detalhes do erro, podemos ver o rastreamento de stack completo, um instantâneo dos detalhes da transação em que o erro ocorreu no momento do erro, juntamente com um link para a transação concluída.

Detalhamento dos erros de transação com o Elastic APM

Além de capturar esses pânicos, você também pode relatar explicitamente os erros ao Elastic APM usando a função apm.CaptureError. Você deve repassar a essa função um contexto que contenha uma transação, além de um valor de erro. CaptureError retornará um objeto “apm.Error”, que pode ser personalizado e finalizado usando o método Send:

if err != nil {
    apm.CaptureError(req.Context(), err).Send()
    return err
}

Por fim, é possível se integrar às estruturas de log para enviar logs de erro ao Elastic APM. Vamos falar mais sobre isso na seção a seguir.

Integração de logs

Em anos recentes, muito se fala sobre os “três pilares da observabilidade”: rastreamentos, logs e metrics. O que estamos analisando até o momento é o rastreamento, mas o Elastic Stack trata de todos esses três pilares e muito mais. Vamos falar sobre metrics um pouco mais adiante; vamos ver como o Elastic APM se integra aos logs de aplicativo.

Se você fez algum tipo de log centralizado, provavelmente já está bem familiarizado com o Elastic Stack, anteriormente conhecido como ELK (Elasticsearch, Logstash, Kibana) Stack. Quando você tem tanto os logs quanto os rastreamentos no Elasticsearch, fazer referência cruzada deles se torna uma tarefa simples.

O agente Go tem módulos de integração para várias estruturas de log conhecidas: Logrus (apmlogrus), Zap (apmzap) e Zerolog (apmzerolog). Vamos adicionar alguns logs ao nosso serviço da Web usando o Logrus e ver como podemos integrá-lo aos nossos dados de rastreamento.

import "github.com/sirupsen/logrus"
var log = &logrus.Logger{                                                    
        Out:   os.Stderr,                                                
        Hooks: make(logrus.LevelHooks),                                  
        Level: logrus.DebugLevel,                                        
        Formatter: &logrus.JSONFormatter{                                
                FieldMap: logrus.FieldMap{                               
                        logrus.FieldKeyTime:  "@timestamp",              
                        logrus.FieldKeyLevel: "log.level",               
                        logrus.FieldKeyMsg:   "message",                 
                        logrus.FieldKeyFunc:  "function.name", // non-ECS
                },                                                       
        },                                                               
}                                                                        
func init() {
        apm.DefaultTracer.SetLogger(log)
}

Nós construímos um logrus.Logger que grava em stderr, formatando logs como JSON. Para se adequar melhor ao Elastic Stack, estamos mapeando alguns dos campos de log padrão para seus equivalentes no Elastic Common Schema (ECS). Como alternativa poderíamos deixá-los como padrão e usar processadores do Filebeat para converter, mas isso é um pouco mais simples. Também informamos ao agente do APM para usar esse logger do Logrus para registrar logs de mensagens de depuração em nível de agente.

Agora vamos ver como podemos integrar os logs de aplicativo aos dados de rastreamento do APM. Vamos adicionar alguns logs aos nosso manipulador de rotas helloHandler e ver como podemos adicionar IDs de rastreamento às mensagens de log. Depois analisaremos como podemos enviar registros de log de erro ao Elastic APM para que apareçam na página “Errors” (Erros).

func helloHandler(w http.ResponseWriter, req *http.Request) {                
        vars := mux.Vars(req)
        log := log.WithFields(apmlogrus.TraceContext(req.Context()))                                            
        log.WithField("vars", vars).Info("handling hello request")       
        name := vars["name"]                                             
        requestCount, err := updateRequestCount(req.Context(), name, log)
        if err != nil {                                                  
                log.WithError(err).Error(“failed to update request count”)
                http.Error(w, "failed to update request count", 500)     
                return                                                   
        }                                                                
        stats, err := getNameStats(req.Context(), name)                                   
        if err != nil {
                log.WithError(err).Error(“failed to update request count”)
                http.Error(w, "failed to get name stats", 500)                            
                return                                                                    
        }                                                                                 
        fmt.Fprintf(w, "Hello, %s! (#%d)\n", name, requestCount)                          
        fmt.Fprintf(w, "In %s, %d: ", stats.Region, stats.Year)                           
        switch n := stats.Male.N + stats.Female.N; n {                                    
        case 1:                                                                           
                fmt.Fprintf(w, "there was 1 baby born with the name %s!\n", name)         
        default:                                                                          
                fmt.Fprintf(w, "there were %d babies born with the name %s!\n", n, name)  
        }  
}                                                                        
func updateRequestCount(ctx context.Context, name string, log *logrus.Entry) (int, error) {                          
        span, ctx := apm.StartSpan(ctx, "updateRequestCount", "custom")                                              
        defer span.End()                                                                                             
        if strings.IndexFunc(name, func(r rune) bool { return r >= unicode.MaxASCII }) >= 0 {                        
                panic("non-ASCII name!")                                                                             
        }                                                                                                            
        tx, err := db.BeginTx(ctx, nil)                                                                              
        if err != nil {                                                                                              
                return -1, err                                                                                       
        }                                                                                                            
        row := tx.QueryRowContext(ctx, "SELECT count FROM stats WHERE name=?", name)                                 
        var count int                                                                                                
        switch err := row.Scan(&count); err {                                                                        
        case nil:                                                                                                    
                if count == 4 {                                                                                      
                        return -1, errors.Errorf("no more")                                                          
                }                                                                                                    
                count++                                                                                              
                if _, err := tx.ExecContext(ctx, "UPDATE stats SET count=? WHERE name=?", count, name); err != nil { 
                        return -1, err                                                                               
                }                                                                                                    
                log.WithField("name", name).Infof("updated count to %d", count)                                      
        case sql.ErrNoRows:                                                                                          
                count = 1                                                                                            
                if _, err := tx.ExecContext(ctx, "INSERT INTO stats (name, count) VALUES (?, 1)", name); err != nil {
                        return -1, err                                                                               
                }                                                                                                    
                log.WithField("name", name).Info("initialised count to 1")                                           
        default:                                                                                                     
                return -1, err                                                                                       
        }                                                                                                            
        return count, tx.Commit()                                                                                    
}

Se agora executarmos o programa com a saída redirecionada a um arquivo (/tmp/hello.log, pressupondo que você está executando Linux ou Mac OS), poderemos instalar e executar o Filebeat para enviar os logs ao mesmo Elastic Stack que está recebendo os dados do APM. Depois de instalar o Filebeat, vamos modificar sua configuração no arquivo filebeat.yml desta maneira:

  • Defina “enabled: true” para a entrada de log em “filebeat.inputs” e altere o caminho para “/tmp/hello.log”.
  • Se estiver usando o Elastic Cloud, defina “cloud.id” e “cloud.auth”, caso contrário, defina “output.elasticsearch.hosts”.
  • Adicione um processador “decode_json_fields”, para que “processors” tenha esta aparência:
processors:
- add_host_metadata: ~
- decode_json_fields:
    fields: ["message"]
    target: ""
    overwrite_keys: true

Agora execute o Filebeat, e os logs começarão a fluir. Agora, se enviarmos algumas solicitações ao serviço, poderemos saltar dos rastreamentos aos logs no mesmo momento específico usando a ação “Show host logs” (Mostrar logs de host).

Salto dos dados do APM para os logs de origem

Essa ação nos levará à Logs UI (UI do Logs), filtrada para o host. Se o aplicativo estava sendo executado em um container do Docker ou no Kubernetes, as ações estariam disponíveis para vincular aos logs para o container do Docker ou o pod do Kubernetes.

A UI do Logs no Kibana

Expandindo os detalhes de registro de log, podemos ver que as IDs de rastreamento foram incluídas nas mensagens de log. No futuro, outra ação será adicionada para filtrar os logs ao rastreamento específico, permitindo ver somente as mensagens de log relacionadas.

Análise dos detalhes de log na UI de Log

Agora que temos a capacidade de saltar dos rastreamentos aos logs, vamos analisar o outro ponto de integração, enviando logs de erro ao Elastic APM, para que apareçam na página “Errors”. Para conseguir fazer isso, precisamos adicionar apmlogrus.Hook ao logger:

func init() {
        // apmlogrus.Hook enviará as mensagens de log "error", "panic" e "fatal" ao Elastic APM.
        log.AddHook(&apmlogrus.Hook{})
}

Anteriormente alteramos updateRequestCount para retornar um erro após a quarta chamada e alteramos helloHandler para registrar isso como erro. Vamos enviar cinco solicitações para o mesmo nome e ver o que aparece na página “Errors”.

Rastreamento de erros com o Elastic APM

Aqui podemos ver dois erros. Um deles é um erro inesperado devido ao uso de um banco de dados interno da memória, o que é mais bem explicado em https://github.com/mattn/go-sqlite3/issues/204. Boa! O erro de “falha ao atualizar contagem de solicitações” é o que acabamos vendo.

Observe que ambos os culpados desses erros são updateRequestCount. Como o Elastic APM sabe disso? Porque estamos usando github.com/pkg/errors, que adiciona um rastreamento do stack a cada erro que cria ou envolve, e o agente Go sabe fazer uso desses rastreamentos de stack.

Metrics de infraestrutura e aplicativo

Por fim, chegamos às metrics. De maneira semelhante à maneira como saltamos para logs de host e de container usando o Filebeat, você pode saltar para as metrics de infraestrutura de host e de container usando o Metricbeat. Além disso, os agentes do Elastic APM periodicamente relatam a CPU e a utilização de memória do sistema e do processo.

Rastreamento de metrics de infraestrutura com o Elastic APM

Os agentes também podem enviar metrics específicas de linguagem e de aplicativo. Por exemplo, o agente Java envia metrics específicas de JVM, enquanto o agente Go envia metrics para o tempo de execução Go, por exemplo, o número atual de goroutines, o número cumulativo de alocações de heap e a porcentagem de tempo gasto em coleta de lixo.

O trabalho está a meio caminho para estender a UI para oferecer suporte às metrics de aplicativo adicionais. Enquanto isso, você pode criar dashboards para visualizar as metrics específicas de Go.

Dashboard do Kibana cheio de metrics de Go

Mas espere... porque ainda tem mais!

Um aspecto que não analisamos é a integração com o agente RUM (Monitoramento real de usuário), que permitiria ver um rastreamento distribuído começando no navegador e seguindo todo o percurso até os serviços de backend. Vamos analisar esse tópico em uma postagem de blog futura. Enquanto isso, você pode saciar seu apetite com a Degustação do RUM da Elastic.

Tratamos do assunto em detalhes nesse artigo. Se ainda tiver mais dúvidas, associe-se ao nosso fórum de discussão e faremos o possível para responder a elas.