Como recolher, padronizar e centralizar logs Golang

>

Dica / logs /golang /microserviços

>

>

As organizações que dependem de sistemas distribuídos frequentemente escrevem as suas aplicações em Go para tirar partido de características de concorrência como canais e goroutinos (por exemplo, Heroku, Basecamp, Cockroach Labs, e Datadog). Se você é responsável por criar ou suportar aplicações Go, uma estratégia de registro bem pensada pode ajudá-lo a entender o comportamento do usuário, localizar erros e monitorar o desempenho de suas aplicações.

Este post mostrará algumas ferramentas e técnicas para gerenciar os logs Golang. Vamos começar com a questão de qual pacote de log para usar para diferentes tipos de requisitos. Em seguida, vamos explicar algumas técnicas para tornar seus logs mais pesquisáveis e confiáveis, reduzindo a pegada de recursos de sua configuração de logs e padronizando suas mensagens de logs.

Conheça seu pacote de logs

Vá lhe dar uma variedade de opções ao escolher um pacote de logs, e vamos explorar várias delas abaixo. Enquanto logrus é a mais popular das bibliotecas que cobrimos, e o ajuda a implementar um formato de log consistente, as outras têm casos de uso especializado que vale a pena mencionar. Esta secção irá pesquisar os logs das bibliotecas, logrus e glog.

Use log for simplicity

A biblioteca de log integrada do Golang, chamada log, vem com um logger padrão que escreve em erro padrão e adiciona um timestamp sem a necessidade de configuração. Você pode usar esses logs rudimentares e prontos para desenvolvimento local, ao obter feedback rápido do seu código pode ser mais importante do que gerar logs ricos e estruturados.

Por exemplo, você pode definir uma função de divisão que retorna um erro ao chamador, ao invés de sair do programa, quando você tenta dividir por zero.

package mainimport ( "log" "errors" "fmt" )func divide(a float32, b float32) (float32, error) { if b == 0 { return 0, errors.New("can't divide by zero") } return a / b, nil}func main() { var a float32 = 10 var b float32 ret, err := divide(a,b) if err != nil{ log.Print(err) } fmt.Println(ret)}

Por causa da divisão do nosso exemplo por zero, ele irá emitir a seguinte mensagem de log:

2019/01/31 11:48:00 can't divide by zero

Utilizar logrus para logs formatados

Recomendamos a escrita de logs Golang usando logrus, um pacote de logs estruturados que é bem adequado para logar no JSON. O formato JSON permite que as máquinas analisem facilmente os seus logs Golang. E uma vez que o JSON é um padrão bem definido, torna simples adicionar contexto através da inclusão de novos campos – um analisador deve ser capaz de pegá-los automaticamente.

Usando logrus, você pode definir campos padrão para adicionar aos seus logs JSON usando a função WithFields, como mostrado abaixo. Você pode então fazer chamadas para o registrador em diferentes níveis, tais como Info(), Warn() e Error(). A biblioteca do logrus irá escrever o log como JSON automaticamente e inserir os campos padrão, juntamente com quaisquer campos que você tenha definido na hora.

package mainimport ( log "github.com/sirupsen/logrus")func main() { log.SetFormatter(&log.JSONFormatter{}) standardFields := log.Fields{ "hostname": "staging-1", "appname": "foo-app", "session": "1ce3f6v", } log.WithFields(standardFields).WithFields(log.Fields{"string": "foo", "int": 1, "float": 1.1}).Info("My first ssl event from Golang")}

O log resultante incluirá a mensagem, nível de log, carimbo da hora e campos padrão em um objeto JSON:

{"appname":"foo-app","float":1.1,"hostname":"staging-1","int":1,"level":"info","msg":"My first ssl event from Golang","session":"1ce3f6v","string":"foo","time":"2019-03-06T13:37:12-05:00"}

Utilizar glog se você estiver preocupado com o volume

Algumas bibliotecas de log permitem ativar ou desativar o log em níveis específicos, o que é útil para manter o volume de log em controle quando se move entre desenvolvimento e produção. Uma dessas bibliotecas é glog, que lhe permite usar flags na linha de comando (por exemplo, -v para verbosidade) para definir o nível de registro quando você executa seu código. Você pode então usar uma função V() em if comandos para escrever seus logs do Golang somente em um certo nível de log.

Por exemplo, você pode usar glog para escrever o mesmo erro “Can’t divide by zero” de antes, mas somente se você estiver logando no nível de verbosidade de 2. Você pode definir a verbosidade para qualquer número inteiro de 32 bits assinado, ou usar as funções Info(), Warning(), Error(), e Fatal() para atribuir níveis de verbosidade 0 até 3 (respectivamente).

 if err != nil && glog.V(2){ glog.Warning(err) }

Você pode tornar a sua aplicação menos intensiva em recursos, registrando apenas certos níveis na produção. Ao mesmo tempo, se não houver impacto nos usuários, muitas vezes é uma boa idéia registrar o maior número possível de interações com sua aplicação, então use softwares de gerenciamento de logs como Datadog para encontrar os dados que você precisa para sua investigação

Melhores práticas para escrever e armazenar logs Golang

Após você ter escolhido uma biblioteca de logs, você também vai querer planejar para onde em seu código você vai fazer chamadas para o registrador, como armazenar seus logs, e como fazer sentido deles. Nesta seção, nós recomendaremos uma série de melhores práticas para organizar seus logs Golang:

  • Faça chamadas para o logger a partir de seu processo principal de aplicação, não dentro de goroutines.
  • Escreva logs de sua aplicação para um arquivo local, mesmo que você os envie para uma plataforma central mais tarde.
  • Padronize seus logs com um conjunto de mensagens pré-definidas.
  • Envie seus logs para uma plataforma central para que você possa analisá-los e agregá-los.
  • Utilize cabeçalhos HTTP e IDs exclusivos para registrar o comportamento do usuário através de microserviços.

Evite declarar goroutinos para registro

Existem duas razões para evitar criar seus próprios goroutinos para lidar com a escrita de logs. Primeiro, pode levar a problemas de concurrência, pois duplicatas do logger tentariam acessar o mesmo io.Writer. Em segundo lugar, as bibliotecas de registro geralmente começam os próprios goroutinos, gerenciando quaisquer problemas de concorrência internamente, e iniciar seus próprios goroutinos só irá interferir.

Escreva seus logs em um arquivo

Even se você estiver enviando seus logs para uma plataforma central, nós recomendamos escrevê-los em um arquivo em sua máquina local primeiro. Você vai querer ter certeza de que seus logs estão sempre disponíveis localmente e não se perdem na rede. Além disso, escrever em um arquivo significa que você pode desacoplar a tarefa de escrever seus logs da tarefa de enviá-los para uma plataforma central. Suas aplicações em si não precisarão estabelecer conexões ou transmitir seus logs, e você pode deixar esses trabalhos para softwares especializados como o Datadog Agent. Se estiver a executar as suas aplicações Go dentro de uma infra-estrutura de contentores que ainda não inclui armazenamento persistente – por exemplo, contentores a correr na AWS Fargate – pode querer configurar a sua ferramenta de gestão de logs para recolher logs directamente dos fluxos STDOUT e STDERR dos seus contentores (isto é tratado de forma diferente em Docker e Kubernetes).

Implemente uma interface de registo padrão

Quando escreve chamadas para os loggers a partir do seu código, as equipas usam frequentemente nomes de atributos diferentes para descrever a mesma coisa. Atributos inconsistentes podem confundir usuários e tornar impossível correlacionar logs que deveriam fazer parte da mesma figura. Por exemplo, dois desenvolvedores podem registrar o mesmo erro, um nome de cliente ausente ao lidar com um upload, de maneiras diferentes.

Golang logs para o mesmo erro com mensagens diferentes de localizações diferentes.
Golang logs para o mesmo erro com mensagens diferentes de localizações diferentes.

Uma boa maneira de reforçar a padronização é criar uma interface entre o código da sua aplicação e a biblioteca de logs. A interface contém mensagens de registo predefinidas que implementam um determinado formato, facilitando a investigação de problemas, assegurando que as mensagens de registo podem ser pesquisadas, agrupadas e filtradas.

Golang logs para um erro usando uma interface padrão para criar uma mensagem consistente.
Golang logs para um erro usando uma interface padrão para criar uma mensagem consistente.

Neste exemplo, vamos declarar um tipo Event com uma mensagem pré-definida. Depois usaremos mensagens Event para fazer chamadas para um registrador. Os colegas de equipe podem escrever logs Golang fornecendo uma quantidade mínima de informação personalizada, deixando a aplicação fazer o trabalho de implementar um formato padrão.

Primeiro, vamos escrever um pacote logwrapper que os desenvolvedores podem incluir dentro do seu código.

package logwrapperimport ( "github.com/sirupsen/logrus")// Event stores messages to log later, from our standard interfacetype Event struct { id int message string}// StandardLogger enforces specific log message formatstype StandardLogger struct { *logrus.Logger}// NewLogger initializes the standard loggerfunc NewLogger() *StandardLogger {var baseLogger = logrus.New()var standardLogger = &StandardLogger{baseLogger}standardLogger.Formatter = &logrus.JSONFormatter{}return standardLogger}// Declare variables to store log messages as new Eventsvar ( invalidArgMessage = Event{1, "Invalid arg: %s"} invalidArgValueMessage = Event{2, "Invalid value for argument: %s: %v"} missingArgMessage = Event{3, "Missing arg: %s"})// InvalidArg is a standard error messagefunc (l *StandardLogger) InvalidArg(argumentName string) { l.Errorf(invalidArgMessage.message, argumentName)}// InvalidArgValue is a standard error messagefunc (l *StandardLogger) InvalidArgValue(argumentName string, argumentValue string) { l.Errorf(invalidArgValueMessage.message, argumentName, argumentValue)}// MissingArg is a standard error messagefunc (l *StandardLogger) MissingArg(argumentName string) { l.Errorf(missingArgMessage.message, argumentName)}

Para usar nossa interface de log, só temos que incluí-la em nosso código e fazer chamadas para uma instância de StandardLogger.

package mainimport ( li "<PATH_TO_PACKAGE>/logwrapper")func main() { var standardLogger = := li.NewLogger() // You can then call a method of our standard logger in the context of an error // you would like to log. standardLogger.InvalidArgValue("client", "nil")}

Quando executarmos nosso código, teremos o seguinte log JSON:

{"level":"error","msg":"Invalid value for argument: client: nil","time":"2019-03-04T11:21:07-05:00"}

Centralizar logs Golang

Se sua aplicação for implantada através de um cluster de hosts, não é sustentável para o SSH em cada um deles a fim de seguir, grepear e investigar seus logs. Uma alternativa mais escalável é passar logs de arquivos locais para uma plataforma central.

Uma solução é usar o pacote Golang syslog para encaminhar logs de toda sua infra-estrutura para um único servidor syslog.

Personalizar e agilizar seu gerenciamento de logs Golang com Datadog.

Outra é usar uma solução de gerenciamento de logs. O Datadog, por exemplo, pode adaptar seus arquivos de log e encaminhar logs para uma plataforma central para processamento e análise.

A vista do Datadog Log Explorer pode mostrar logs Golang de várias fontes.

Pode usar atributos para graficar os valores de certos campos de log ao longo do tempo, ordenados por grupo. Por exemplo, você pode rastrear o número de erros por service para que você saiba se há um incidente em um de seus serviços. Mostrando logs apenas do serviço go-logging-demo, podemos ver quantos logs de erro esse serviço produziu em um dado intervalo.

Grupos de logs Golang por status.

Vocês também podem usar atributos para detalhar as possíveis causas, por exemplo ver se um pico nos logs de erro pertence a um host específico. Você pode então criar um alerta automático baseado nos valores dos seus logs.

Track Golang logs através de microservices

Quando se resolve um erro, muitas vezes é útil ver qual o padrão de comportamento que levou a ele, mesmo que esse comportamento envolva um número de microservices. Você pode conseguir isso com rastreamento distribuído, visualizando a ordem em que sua aplicação executa funções, consultas a bancos de dados e outras tarefas, e seguindo esses passos de execução à medida que eles percorrem o caminho através de uma rede. Uma maneira de implementar o rastreamento distribuído dentro de seus logs é passar informações contextuais como cabeçalhos HTTP.

Neste exemplo, um microserviço recebe uma requisição e verifica por um trace ID no cabeçalho x-trace, gerando um se ele não existir. Ao fazer uma requisição para outro microserviço, nós então geramos um novo spanID para isto e para cada requisição – e adicionamos ao cabeçalho x-span.

func microService1(w http.ResponseWriter, r *http.Request) { client := &http.Client{} trace := r.Header.Get("x-trace") if ( trace == "") { trace = generateTraceId() } span := generateSpanId() // Hit the second microservice with the appropriate headers reqService2, _ := http.NewRequest("GET", "<ADDRESS>", nil) reqService2.Header.Add("x-trace", trace) reqService2.Header.Add("x-span", span) resService2, _ := client.Do(reqService2)}

Downstream microservices usam os cabeçalhos x-span das requisições de entrada para especificar os pais dos spans que eles geram, e enviam essa informação como o cabeçalho x-parent para o próximo microserviço da cadeia.

func microService2(w http.ResponseWriter, r *http.Request) { trace := r.Header.Get("x-trace") span := generateSpanId() parent := r.Header.Get("x-span") if (trace == "") { w.Header().Set("x-parent", parent) } w.Header().Set("x-trace", trace) w.Header().Set("x-span", span) if (parent == "") { w.Header().Set("x-parent", span) } w.WriteHeader(http.StatusOK) io.WriteString(w, fmt.Sprintf(aResponseMessage, 2, trace, span, parent))}

Se ocorrer um erro em um de nossos microserviços, podemos usar os atributos trace, parent, e span para ver a rota que uma requisição tomou, deixando-nos saber quais hospedeiros – e possivelmente quais partes do código da aplicação – para investigar.

No primeiro microserviço:

{"appname":"go-logging","level":"debug","msg":"Hello from Microservice One","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"UzWHRihF"}

No segundo:

{"appname":"go-logging","level":"debug","msg":"Hello from Microservice Two","parent":"UzWHRihF","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"DPRHBMuE"}

Se você quiser se aprofundar mais nas possibilidades de rastreamento do Golang, você pode usar uma biblioteca de rastreamento como o OpenTracing ou uma plataforma de monitoramento que suporte rastreamento distribuído para aplicações Go. Por exemplo, o Datadog pode construir automaticamente um mapa de serviços usando dados de sua biblioteca de rastreamento Golang; visualizar tendências em seus traços ao longo do tempo; e deixar você saber sobre serviços com taxas de requisição incomuns, taxas de erro ou latência.

Um exemplo de visualização mostrando traços de requisições entre microserviços.
Um exemplo de visualização mostrando traços de pedidos entre microserviços.

Limpar e compreender os logs de Golang

Neste post, nós destacamos os benefícios e tradeoffs de várias bibliotecas de logon Go. Também recomendamos maneiras de garantir que seus logs estejam disponíveis e acessíveis quando você precisar deles, e que a informação que eles contêm seja consistente e fácil de analisar.

Para começar a analisar todos os seus logs Go com Datadog, inscreva-se para uma avaliação gratuita.

Deixe uma resposta

O seu endereço de email não será publicado.