Logs Estruturados com Serilog e Seq: Melhorando a Visibilidade e Análise de Dados em Aplicações

Logs Estruturados com Serilog e Seq: Melhorando a Visibilidade e Análise de Dados em Aplicações

O registro de logs é uma prática fundamental no desenvolvimento de software, fornecendo informações valiosas sobre o comportamento, desempenho e erros de uma aplicação. No entanto, o simples armazenamento de mensagens de texto em arquivos ou consoles pode não ser suficiente para obter uma compreensão profunda e eficaz do funcionamento de uma aplicação. É aqui que os logs estruturados entram em jogo, oferecendo uma abordagem mais organizada, legível e analisável para o registro de eventos. Neste artigo, vamos explorar os conceitos de logs estruturados e como o Serilog em conjunto com o Seq pode melhorar significativamente a visibilidade e análise de dados em aplicações. O objetivo desse artigo é mostrar um aplicação usando Serilog com Seq, para isso vamos do início até a integração completa.

A Importância dos Logs

Os logs, registros detalhados de atividades e eventos em um sistema, assumem um papel crucial em diversos contextos, desde o desenvolvimento de software até a segurança de rede e a análise de negócios. Além disso, possibilitam uma atuação mais proativa do que reativa na resolução e identificação de problemas. Sua importância se manifesta em três pilares principais:

1 – Solução de Problemas e Depuração:

Rastreamento de Falhas: Logs facilitam a identificação de erros e falhas em sistemas, fornecendo informações sobre:

a – Origem do Erro: Localização específica no código ou componente onde o erro ocorreu.

b – Causa do Erro: Mensagens de erro e detalhes técnicos que auxiliam na compreensão da causa raiz do problema.

c – Contexto do Erro: Informações sobre o estado do sistema e as ações que antecederam o erro, facilitando a análise e a correção.

d – Análise de Desempenho: Logs permitem a análise do desempenho do sistema, identificando gargalos e áreas de otimização.

e – Tempo de Resposta: Monitoramento do tempo de resposta de operações e requisições, detectando lentidão e gargalos.

f – Detecção de Anomalias: Monitoramento de padrões de comportamento e identificação de anomalias que podem indicar problemas em potencial.

2 – Segurança e Conformidade:

Auditoria e Rastreamento: Logs fornecem um registro completo das atividades em um sistema, permitindo:

a – Investigação de Incidentes: Rastreamento de atividades suspeitas e investigação de incidentes de segurança.

b – Análise de Conformidade: Demonstração da conformidade com regulamentações e normas através da auditoria de logs.

c – Detecção de Fraude: Identificação de atividades fraudulentas e acesso não autorizado ao sistema.

d – Prevenção de Ataques: Logs podem ser usados para identificar e prevenir ataques cibernéticos.

3 – Análise de Negócios e Inteligência:

Melhoria da Experiência do Cliente: Análise de logs de aplicativos e websites para:

a – Identificar pontos de atrito: Compreender os desafios que os usuários enfrentam e identificar áreas de melhoria na experiência do cliente.

b – Compreender o comportamento do cliente: Analisar padrões de uso e preferências para personalizar a experiência do cliente e otimizar campanhas de marketing.

O que são Logs Estruturados?

Logs estruturados são registros de eventos que seguem um formato predefinido e organizado, geralmente em formato JSON, XML ou similar. Diferentes dos logs tradicionais, logs estruturados contém uma série informações e metadados adicionais, campos nomeados e valores associados usados para agrupamento e consultas,tornando-os mais legíveis, analisáveis e fáceis de correlacionar. Esta estruturação facilita a consulta, filtragem e visualização de logs, especialmente em ambientes de microserviços, contêineres e aplicações distribuídas. Logs estruturados devem conter informações relevantes ao negócio e ao desenvolvedor.

Um log comum de exceções, por exemplo, envia dados da exceção, como mensagem, stack trace, inner exceptions. Um log estruturado de exceções, poderia conter informações que diagnosticam o servidor no qual o log foi gerado, qual a operação estava sendo processada, qual o ID ou dados mais completos dos seus objetos de negócio durante a operação, além da comum severidade. E não para por aí, ainda seria possível informar qual o cliente da requisição, no caso de WebApps, qual o usuário logado, entre outras diversas informações de aplicação que poderiam ser passadas para o log, ajudando no troubleshooting e análise de problemas ou mesmo de fluxos de negócio. Para o artigo vamos usar Serilog para criar os logs estruturados.

Introdução ao Serilog

Serilog é uma biblioteca de logging para .NET e também outras linguagens que suporta a geração de logs estruturados de forma nativa. Ele permite que os desenvolvedores registrem facilmente eventos e erros em seus aplicativos, facilitando a depuração e a solução de problemas, além de oferece uma configuração flexível e extensível, permitindo que os desenvolvedores capturem e registrem informações detalhadas, contextuais e enriquecidas sobre o comportamento e o desempenho de suas aplicações. Ao contrário das estruturas de log tradicionais, o Serilog se concentra no log estruturado, o que significa que os logs são registrados em um formato estruturado que pode ser facilmente consultado e analisado.

Por que usar o Serilog?

O Serilog é uma biblioteca de logging que se destaca em muitos aspectos e oferece diversas vantagens sobre outras estruturas de log disponíveis no mercado. Abaixo estão algumas razões pelas quais muitos desenvolvedores optam por usar o Serilog em seus projetos em vez de outras bibliotecas de logging:

1 – Logs Estruturados

Uma das características mais distintivas do Serilog é o suporte nativo para logs estruturados. Em vez de simplesmente gravar mensagens de texto em arquivos ou consoles, o Serilog permite que os desenvolvedores gerem logs em formatos organizados, como JSON, XML ou outros, tornando-os mais fáceis de analisar, consultar e correlacionar. Logs estruturados são especialmente úteis em ambientes distribuídos e complexos, onde a capacidade de rastrear e analisar o fluxo de dados entre diferentes componentes é crucial.

2 – Flexibilidade e Extensibilidade

O Serilog é altamente flexível e extensível, permitindo que os desenvolvedores personalizem e configurem o sistema de logging de acordo com as necessidades específicas do projeto. Ele oferece uma variedade de sinks que representa qual o destino dos logs, existem também os enriquecedores de log, facilitando a integração com diferentes sistemas, serviços e plataformas de monitoramento e análise de logs.

O Serilog fornece Sinks que são os coletores para gravar eventos de log no armazenamento em vários formatos.

3 – Enriquecimento de Logs

O Serilog facilita o enriquecimento de logs com informações adicionais, como propriedades personalizadas, contexto da aplicação, dados do ambiente e muito mais. Isso ajuda a fornecer informações mais detalhados e contextuais sobre o comportamento e o desempenho da aplicação, melhorando a capacidade de monitoramento, diagnóstico e resolução de problemas em tempo real.

4 – Performance e Eficiência

O Serilog foi projetado com foco em performance e eficiência, minimizando o impacto no desempenho da aplicação durante a geração e gravação de logs. Ele utiliza técnicas avançadas de buffering, assincronia e otimização para garantir que o logging não se torne um gargalo ou afete negativamente a experiência do usuário final.

5 – Comunidade Ativa e Suporte

O Serilog possui uma comunidade ativa, engajada e colaborativa de desenvolvedores, mantenedores e contribuidores que continuamente trabalham juntos para melhorar, aprimorar e estender a biblioteca. Além disso, o Serilog é bem documentado, possui uma ampla gama de recursos, tutoriais e exemplos disponíveis, e oferece suporte através de fóruns, repositórios e plataformas de discussão, proporcionando uma base sólida e confiável para os desenvolvedores e organizações que escolhem adotá-lo em seus projetos.

Seq: Gerenciamento de Logs e Rastreamentos em Tempo Real

Seq é um servidor de pesquisa e análise em tempo real para logs e rastreamentos de aplicativos estruturados. Com sua interface intuitiva, armazenamento de eventos JSON, o que facilita a análise e a integração com outras ferramentas.

1 – Principais Recursos e Funcionalidades:

a – Interface de Usuário Amigável: A interface do Seq foi cuidadosamente projetada para facilitar a navegação e a análise de logs. Com recursos como busca rápida, filtros avançados e visualizações personalizadas, os usuários podem encontrar rapidamente as informações necessárias.

b – Armazenamento em Formato JSON: Os eventos de log no Seq são armazenados no formato JSON, o que permite uma estruturação flexível e facilita a integração com outras ferramentas e sistemas.

c – Busca e Análise em Tempo Real: Uma das principais vantagens do Seq é sua capacidade de buscar e analisar logs em tempo real. Isso permite que os usuários identifiquem e respondam rapidamente a problemas e anomalias em seus sistemas.

d – Integração com Plataformas e Ferramentas: O Seq oferece uma ampla gama de integrações com outras ferramentas e plataformas, incluindo sistemas de monitoramento, alerta e gerenciamento de incidentes. Isso permite uma integração perfeita em ambientes de desenvolvimento e operações.

Vamos colocar a mão na massa!

Vamos desenvolver uma API simples com integração ao Seq para armazenar nossos logs. Para isso, utilizaremos o Serilog. Serão duas aplicações com as seguintes tecnologias: C# e Node.js. A ideia não é focar na criação das APIs, mas sim na geração de logs e no monitoramento. Portanto, será uma API simples.”

1 – API .NET C

Será uma API simples para o cadastro de eventos. A ideia é utilizar logs e visualizá-los no Seq.”

Pré-requisitos:

.NET 8
Docker

Para instalar o Serilog em nossa aplicação é bastante simples, os desenvolvedores podem usar o NuGet para instalar o pacote Serilog e, em seguida, configurá-lo usando uma variedade de abordagens, como JSON ou configuração baseada em código.

dotnet add package Serilog.Sinks.Seq –version 7.0.0
dotnet add package Serilog.Sinks.Dynatrace –version 1.0.7
dotnet add package Serilog.Enrichers.Thread –version 3.1.0
dotnet add package Serilog.Enrichers.Process –version 2.0.2
dotnet add package Serilog.Enrichers.Memory –version 1.0.4
dotnet add package Serilog.Enrichers.Environment –version 2.3.0
dotnet add package Serilog.Enrichers.CorrelationId –version 3.0.1

a – Serviço

using EventAPI.Models;

namespace EventAPI.Services;

public interface IEventService
{
List<Event> GetAll();
Event? GetById(int id);
Event Create(Event newEvent);
void Update(int id, Event updatedEvent);
void Delete(int id);
void BatchDelete();
}

using EventAPI.Models;

namespace EventAPI.Services;

public class EventService : IEventService
{
private readonly ILogger<EventService> _logger;
private List<Event> _events;

public EventService(ILogger<EventService> logger)
{
_logger = logger;
_events = new List<Event>();
}

public List<Event> GetAll()
{
return _events;
}

public Event? GetById(int id)
{
return _events.FirstOrDefault(e => e.Id == id);
}

public Event Create(Event newEvent)
{
_logger.LogInformation(“Creating new event with title {title}”, newEvent.Title);
newEvent.Id = _events.Count > 0 ? _events.Max(e => e.Id) + 1 : 1;
_events.Add(newEvent);
return newEvent;
}

public void Update(int id, Event updatedEvent)
{
_logger.LogInformation(“Updating event with id {id}”, id);
var existingEvent = _events.FirstOrDefault(e => e.Id == id);
if (existingEvent is null) throw new InvalidOperationException(“Event not found”);
existingEvent.Title = updatedEvent.Title;
existingEvent.StartDateTime = updatedEvent.StartDateTime;
existingEvent.EndDateTime = updatedEvent.EndDateTime;
}

public void Delete(int id)
{
_logger.LogInformation(“Deleting event with id {id}”, id);
var existingEvent = _events.FirstOrDefault(e => e.Id == id);
if (existingEvent is null) throw new InvalidOperationException(“Event not found”);
_events.Remove(existingEvent);
}

public void BatchDelete()
{
if (_events.Count == 0)
{
throw new InvalidOperationException(“No events to delete”);
}
_events.Clear();
}
}

Para algumas ações do serviço, foi disponibilizado o registro de log através da chamada ‘logger.LogInformation‘, onde serão gravadas as informações passadas.

b – Configuração Serilog

using Serilog;

namespace EventAPI.Extensions;

public static class LogSettings
{
public static void AddLogSettings(this WebApplicationBuilder builder, string applicationName, ConfigurationManager configuration)
{
var seq = configuration[“Settings:Seq”];
if (string.IsNullOrWhiteSpace(seq)) throw new ArgumentNullException(“Seq is required”);
Console.WriteLine($”Environment: {builder.Environment.EnvironmentName});
builder.Logging.ClearProviders();
var logger = new LoggerConfiguration();
logger.Enrich.WithProperty(“ApplicationName”, $”{applicationName}{Environment.GetEnvironmentVariable(“DOTNET_ENVIRONMENT”)})
.MinimumLevel.Information()
.Enrich.WithEnvironmentName()
.Enrich.WithMachineName()
.Enrich.WithProcessId()
.Enrich.WithThreadId()
.Enrich.WithMemoryUsage()
.Enrich.FromLogContext()
.Enrich.WithCorrelationId()
.Enrich.WithCorrelationIdHeader();
if (builder.Environment.EnvironmentName == “Development”)
{
logger.WriteTo.Console(outputTemplate: “{Timestamp:yyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}”);
}
logger.WriteTo.Seq(seq, restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information);
builder.Logging.AddSerilog(logger.CreateLogger());
}
}

Foi configurado alguns Enrichments permitem adicionar contexto adicional às suas mensagens de log, o que pode ser extremamente útil para depuração e solução de problemas.

Enrich.WithEnvironmentName()
Enrich.WithMachineName()
Enrich.WithProcessId()
Enrich.WithThreadId()
Enrich.WithMemoryUsage()
Enrich.FromLogContext()
Enrich.WithCorrelationId()
Enrich.WithCorrelationIdHeader();

Foi utilizado o Seq para registrar os logs, uma vez que a coleta de informações por console é apenas para o ambiente de desenvolvimento.

if (builder.Environment.EnvironmentName == “Development”)
{
logger.WriteTo.Console(outputTemplate: “{Timestamp:yyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}”);
}
logger.WriteTo.Seq(seq, restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information);

c – API

using EventAPI.Models;
using EventAPI.Services;

namespace EventAPI.Endpoints;

public static class Events
{
public static void RegisterEndpoints(this IEndpointRouteBuilder routes)
{
var events = routes.MapGroup(“/api/v1/events”)
.WithName(“Events”)
.WithOpenApi();
events.MapGet(“”, (IEventService service) => service.GetAll())
.WithName(“GetAllEvents”)
.WithTags(“Events”);
events.MapGet(“/{id}”, (int id, IEventService service) => service.GetById(id))
.WithName(“GetEventById”)
.WithTags(“Events”);
events.MapPost(“”, (Event newEvent, IEventService service) => service.Create(newEvent))
.WithName(“CreateEvent”)
.WithTags(“Events”);
events.MapPut(“/{id}”, (int id, Event updatedEvent, IEventService service) =>
{
service.Update(id, updatedEvent);
})
.WithName(“UpdateEvent”)
.WithTags(“Events”);
events.MapDelete(“/{id}”, (int id, IEventService service) =>
{
service.Delete(id);
})
.WithName(“DeleteEvent”)
.WithTags(“Events”);
events.MapDelete(“/batch”, (IEventService service) =>
{
service.BatchDelete();
})
.WithName(“BatchDeleteEvents”)
.WithTags(“Events”);
}
}

Caso queira subir a **API local siga os passos:**

a – Crie um arquivo docker-compose.yml:

version: ‘3.4’

services:
seq:
image: datalust/seq:latest
container_name: seq
environment:
ACCEPT_EULA=Y
ports:
– 5341:5341
– 8081:80

b – Depois execute o comando para subir o Seq:

docker-compose up

c – Acesse o Seq link:

http://localhost:5341/

Caso queria testar como a aplicação e Seq compartilhando a mesma rede em containers:

Na raíz do projeto existe docker-compose.yml, execute os comandos:

a – Subir containers

docker-compose up -d –build

b – Descer containers

docker-compose down

Para acessar a aplicação web Seq pode ser via http://localhost:5341/

Para acessar o código completo Serilog Seq Api

Dicas

1 – Uma melhoria que podemos fazer no código é usar Serilog.Sinks.Async para registar logs no console. Use esse coletor para reduzir a sobrecarga de registro de chamadas ,ficando o trabalho a cargo de uma Thread em segundo plano.

Instalando a partir do Nuget

dotnet add package Serilog.Sinks.Async
using Serilog;

class Program
{
static async Task Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Async(wt => wt.Console())
.WriteTo.File(“logs/my-logs.txt”, rollingInterval: RollingInterval.Day)
.CreateLogger();

Log.Information(“Meu log!”);

int a = 10, b = 0;
try
{
Log.Debug(“Dividindo {A} by {B}”, a, b);
Console.WriteLine(a / b);
}
catch (Exception ex)
{
Log.Error(ex, “Algo deu errado”);
}
finally
{
await Log.CloseAndFlushAsync();
}
}
}

2 – Não utilizar o Serilog.Sinks.Console em produção, O Console Sink é adequado para uso em desenvolvimento e depuração, por isso deve ser utilizado em ambiente local. Em produção é recomendado utilizar coletores como: Elasticsearch, Rabbitmq, Seq, Sentry, Splunk entre outros que podem ser visto em Provided Sinks

3 – Podemo formatar a saída para temos logs para estruturados. Os coletores baseados em console e em arquivo, geralmente aceitam modelos de saída para controlar como os dados de eventos de log são formatados.

a – Formato de Texto Simples:

Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();

b – Formato JSON:

Log.Logger = new LoggerConfiguration()
.WriteTo.Console(new JsonFormatter())
.CreateLogger();

Saída

{“Timestamp”:“2024-04-04T22:43:50.3868987-03:00”,“Level”:“Information”,“MessageTemplate”:“Meu log!”}
{“Timestamp”:“2024-04-04T22:43:50.4401178-03:00”,“Level”:“Error”,“MessageTemplate”:“Algo deu errado”,“Exception”:“System.DivideByZeroException: Attempted to divide by zero.n at Program.Main() in /home/wanderson/Developer/Repositorios/serilog_log_console/AppSerilogConsole/Program.cs:line 26″}

c – Formato Estruturado:

O formato logs gravados podem ser modificados usando o outputTemplateparâmetro.

Log.Logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate:
“[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}”)
.CreateLogger();

Propriedades de saída:

Exception– A mensagem de exceção completa e o rastreamento de pilha, formatados em várias linhas. Vazio se nenhuma exceção estiver associada ao evento.

Level– O nível de evento de log, formatado como o nome completo do nível. Para nomes de níveis mais compactos, use um formato como {Level:u3}ou {Level:w3}para nomes de níveis com três caracteres maiúsculos ou minúsculos, respectivamente.

Message– A mensagem do evento de log, renderizada como texto simples. O :lespecificador de formato desativa a citação de strings e :jusa a renderização no estilo JSON para quaisquer dados estruturados incorporados.

NewLine– Uma propriedade com o valor de System.Environment.NewLine.

Properties– Todos os valores de propriedade de evento que não aparecem em nenhum outro lugar da saída. Use o :jformato para usar a renderização JSON.

Timestamp– O carimbo de data/hora do evento, como um arquivo DateTimeOffset.

TraceId– O ID do rastreamento que estava ativo quando o evento foi criado, se houver.

SpanId– O ID do período que estava ativo quando o evento foi criado, se houver.

2 – API com Node.js

Será uma API simples para o cadastro de eventos. A ideia é utilizar logs e visualizá-los no Seq.”

Pré-requisitos:

Node.js
Docker

Logs com Winston:

Winston é uma biblioteca de registro popular para node.js, com suporte para registro estruturado. Winston utiliza “transportes” para representar diferentes destinos de log, incluindo o console padrão e opções de arquivo. Winston-seq é um transporte para Winston, que possibilita o envio de logs para o Seq.

Pacotes:

npm install winston
npm install @datalust/winston-seq

Configuração:

import em sua aplicação Node.js:

const winston = require(winston);
const winston = require(winston);
const { combine, timestamp, json, printf } = winston.format;
const { SeqTransport } = require(@datalust/winston-seq);

const logLevels = {
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
};

const customFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} [${label}] ${level}: ${message}`;
});

const setup = () => {
const logger = winston.createLogger({
levels: logLevels,
level: process.env.LOG_LEVEL || info,
format: combine(
timestamp({
format: YYYY-MM-DD hh:mm:ss.SSS A
}),
json()
),
transports: [
new SeqTransport({
serverUrl: process.env.SEQ_URL || http://localhost:5341,
onError: e => {
console.error(e);
}
}),
new winston.transports.Console()
]
});
return logger;
};

module.exports = { setup };

No código de configuração podemos ver os níveis do Log no Winston que são:

{
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
}

Uso dos logs na API:

const { v4: uuidv4 } = require(uuid);

let events = [];

const list = (req, res) => {
res.json(events);
};

const create = (req, res) => {
const { code, title } = req.body;
const newEvent = { id: uuidv4(), code, title };
events.push(newEvent);
res.status(201).json(newEvent);
};

const update = (req, res) => {
const { id } = req.params;
const { title } = req.body;
const index = events.findIndex(event => event.id === id);
if (index !== 1) {
events[index].title = title;
res.json(events[index]);
} else {
res.status(404).json({ mensagem: Evento não encontrada });
}
};

const remove = (req, res) => {
const { id } = req.params;
const index = events.findIndex(event => event.id === id);
if (index !== 1) {
events.splice(index, 1);
res.json({ mensagem: Tarefa excluída com sucesso });
} else {
logger.error(Tarefa não encontrada);
res.status(404).json({ mensagem: Tarefa não encontrada });
}
};

module.exports = { list, create, update, remove };

Os seis níveis de log acima de cada um correspondem a um método no criador de logs:

logger.error(error);
logger.warn(warn);
logger.info(info);
logger.verbose(verbose);
logger.debug(debug);
logger.silly(silly);

Caso queira subir a API local siga os passos:

a – Crie um arquivo docker-compose.yml:

version: ‘3.4’

services:
seq:
image: datalust/seq:latest
container_name: seq
environment:
ACCEPT_EULA=Y
ports:
– 5341:5341
– 8081:80

b – Depois execute o comando para subir o Seq:

docker-compose up

c – Acesse o Seq link:

http://localhost:5341/

Caso queria testar como **docker-compose da aplicação:**

Na raíz do projeto existe docker-compose.yml, execute os comandos:

a – Subir containers

docker-compose up -d –build

b – Descer containers

docker-compose down

Obs.: É necessário conceder permissão local ao arquivo wait-for-seq.sh com o comando:

chmod +x wait-for-seq.sh

Para acessar a aplicação Node.js completa acesse Serilog Seq API

Referências:

Seq
Serilog
Github – Serilog
Henrique Mauri – Melhores práticas de utilização do Serilog
Henrique Mauri -Coletando logs com o Serilog no .NET 6
Milan Jovanović – 5 Serilog Best Practices For Better Structured Logging
Logging from Node.js
Github – Winston
Github – Winston-seq
A Complete Guide to Winston Logging in Node.js
Node.js Logging with Winston
Automated Logging in Express.js
Best Practices for Logging in Node.js
Winston Logger – Full tutorial with a sample Nodejs application

Leave a Reply

Your email address will not be published. Required fields are marked *