ajuste do readme e inclusão de novas ferramentas

This commit is contained in:
João Pedro Toledo Goncalves 2025-10-17 22:47:25 -03:00
parent d01ae7ec46
commit 03415c7922
3 changed files with 718 additions and 74 deletions

181
README.md
View File

@ -1,109 +1,142 @@
# Painel de Atividade de Usuários NGINX
# Suíte de Análise de Logs para Zabbix e NGINX
Este projeto contém um script Python avançado para processar logs de acesso do NGINX (em formato CSV), analisar a atividade de usuários autenticados e gerar um painel de visualização interativo em um único arquivo HTML.
A ferramenta foi projetada para extrair insights sobre como e quando os usuários interagem com um sistema web (como o Nextcloud), categorizando suas ações e apresentando os dados em gráficos claros e detalhados.
![Placeholder para a imagem do relatório](https://via.placeholder.com/800x450.png?text=Insira+um+screenshot+do+relatório+aqui)
*(Recomendação: Substitua esta imagem por um screenshot do relatório `painel_de_atividade_final.html`)*
Este repositório contém uma suíte de scripts Python para análise avançada de logs de aplicações web. As ferramentas extraem dados de fontes como Zabbix e arquivos CSV do NGINX para gerar relatórios interativos em HTML, oferecendo insights sobre performance do sistema, auditoria de acesso de usuários e categorização de atividades.
---
## Funcionalidades Principais
## As Ferramentas
* **Processamento de Logs:** Lê arquivos de log NGINX em formato CSV, otimizado para lidar com grandes volumes de dados.
* **Inferência de Ações:** Analisa `User-Agent`, método HTTP (`GET`, `POST`, `PUT`, etc.) e URI para categorizar a atividade do usuário em três níveis:
* **Ativo:** Ações diretas com arquivos (edição, upload, download, exclusão).
* **Online:** Atividade geral de navegação e checagens em segundo plano.
* **Outros:** Sincronizações, interações com formulários e requisições não classificadas.
* **Visualizações Detalhadas:** Gera múltiplos gráficos para análise:
* **Nuvem de Atividade:** Um gráfico de dispersão que mostra a intensidade das atividades ao longo do tempo.
* **Heatmap Agregado:** Mostra os padrões de atividade de todos os usuários por dia da semana e hora.
* **Heatmap por Colaborador:** Gera um mapa de calor individual para cada usuário.
* **Timeline Diária:** Gráficos de linha suavizados que detalham a intensidade de cada categoria de ação por hora para um usuário e dia específicos.
* **Painel Interativo:** Produz um único arquivo `HTML` com filtros que permitem selecionar um colaborador específico e navegar entre sua visão agregada e a análise diária.
A suíte é composta por três scripts principais, cada um com um objetivo específico:
---
### 1. `performance-insights.py` (O Analista de Performance)
Este script conecta-se à API do Zabbix para analisar a saúde técnica e a performance do sistema.
## Como Funciona
* **O que faz?** Mede a velocidade das respostas do servidor e identifica a frequência e o tipo de erros HTTP.
* **Responde a perguntas como:**
* "Qual o tempo médio de resposta do sistema?"
* "Quantas requisições foram rápidas, aceitáveis ou lentas?"
* "Quais páginas ou arquivos estão gerando mais erros 404 (Não Encontrado)?"
* **Resultado:** Gera um relatório chamado `relatorio_insights_AAAA-MM-DD.html` com gráficos de pizza sobre a velocidade e status das requisições, além de tabelas com os principais erros.
O script opera em quatro etapas principais:
### 2. `audit-logins.py` (O Auditor de Acesso)
Este script também utiliza a API do Zabbix, mas com foco em auditar o comportamento e o tempo de atividade dos usuários.
1. **Leitura e Parsing:** Cada linha do arquivo de log CSV é lida. Uma função de parsing extrai informações cruciais como timestamp, username (extraído de cookies como `nc_username`), `User-Agent` e a requisição HTTP.
2. **Classificação de Ações:** A função `get_action_details` atua como o cérebro da análise. Ela usa uma série de regras para determinar a ação do usuário e o tipo de cliente (Web ou App de sincronização) com base nos dados extraídos.
3. **Visualização de Dados:** Utilizando as bibliotecas `pandas` para manipulação de dados e `matplotlib`/`seaborn` para a geração de gráficos, o script cria as visualizações. Cada gráfico é salvo em memória como uma imagem.
4. **Geração do Relatório:** As imagens dos gráficos são codificadas em Base64 e embutidas diretamente em um template HTML. Este template também inclui JavaScript para criar os menus de filtro interativos, resultando em um arquivo `painel_de_atividade_final.html` totalmente funcional e portátil.
* **O que faz?** Identifica sessões de trabalho, calculando o início, o fim e a duração total da atividade de cada usuário em um dia.
* **Responde a perguntas como:**
* "Quais usuários acessaram o sistema hoje?"
* "A que horas cada pessoa começou e terminou de trabalhar?"
* "Quanto tempo cada um ficou efetivamente online?"
* **Resultado:** Gera um relatório chamado `relatorio_acesso_AAAA-MM-DD.html` com uma linha do tempo interativa (gráfico de Gantt) e uma tabela que resume o tempo de serviço de cada colaborador.
### 3. `relatorio-de-atividade.py` (O Categorizador de Ações)
Diferente dos outros, este script analisa um **arquivo de log NGINX em formato CSV** para classificar os *tipos* de interação do usuário com o sistema.
* **O que faz?** Categoriza cada ação do usuário como "Ativa" (edição, upload), "Online" (navegação) ou "Outras" (sincronização em segundo plano).
* **Responde a perguntas como:**
* "Qual o perfil de uso de cada colaborador? Ele passa mais tempo editando arquivos ou apenas navegando?"
* "Quais os horários de pico para atividades produtivas (uploads, edições)?"
* **Resultado:** Gera um relatório único chamado `painel_de_atividade_final.html` com heatmaps de atividade, nuvens de ações e timelines detalhadas por tipo de ação.
---
## Pré-requisitos
* **Python 3.8+**
* **Logs do NGINX em formato CSV:** O script espera um formato específico. Veja um exemplo de diretiva `log_format` para NGINX que gera a saída compatível:
* **Fonte de Logs:**
* Para os scripts `performance-insights.py` e `audit-logins.py`: Acesso à API do Zabbix com um item que colete os logs da aplicação em formato JSON.
* Para o script `relatorio-de-atividade.py`: Um arquivo de log `access.log` do NGINX exportado em formato CSV, conforme o `log_format` abaixo.
* **Dependências Python:** Todas as bibliotecas necessárias estão no arquivo `requirements.txt`.
```nginx
log_format custom_csv escape=json
'$time_iso8601,'
'$remote_addr,'
'"$http_user_agent",'
'"$http_cookie",'
'$status,'
'$body_bytes_sent,'
'$remote_user,'
'$request_time,'
'$upstream_response_time,'
'"$request"';
#### Exemplo de `log_format` para `relatorio-de-atividade.py`
Adicione este formato à sua configuração do NGINX (`nginx.conf`):
```nginx
log_format custom_csv escape=json
'$time_iso8601,'
'$remote_addr,'
'"$http_user_agent",'
'"$http_cookie",'
'$status,'
'$body_bytes_sent,'
'$remote_user,'
'$request_time,'
'$upstream_response_time,'
'"$request"';
access_log /var/log/nginx/seu-site.access.log custom_csv;
access_log /var/log/nginx/seu-site.access.log custom_csv;
```
---
## Instalação
1. **Clone o Repositório**
```bash
git clone [URL_DO_SEU_REPO_GITEA]
cd [NOME_DO_REPOSITORIO]
```
2. **Crie e Ative um Ambiente Virtual (Recomendado)**
```bash
python3 -m venv venv
source venv/bin/activate
```
3. **Instale as Dependências**
Crie um arquivo `requirements.txt` com o seguinte conteúdo e depois execute o comando `pip install`.
**`requirements.txt`:**
```
pandas
plotly
py-zabbix
matplotlib
seaborn
numpy
scipy
```
**Comando de instalação:**
```bash
pip install -r requirements.txt
```
---
## Como Utilizar
1. **Clone o Repositório**
```bash
git clone [URL_DO_SEU_REPO_GITEA]
cd relatorio-atividade-nginx
```
A execução varia dependendo do script que você deseja usar.
2. **Crie e Ative um Ambiente Virtual (Recomendado)**
```bash
# Para Linux/macOS
python3 -m venv venv
source venv/bin/activate
#### Para os Scripts de Zabbix (`performance-insights.py` e `audit-logins.py`)
# Para Windows
python -m venv venv
.\venv\Scripts\activate
```
Ambos os scripts compartilham os mesmos parâmetros de linha de comando para se conectar ao Zabbix.
3. **Instale as Dependências**
```bash
pip install -r requirements.txt
```
**Argumentos:**
* `--server`: URL completa do seu servidor Zabbix.
* `--token`: Token da API do Zabbix para autenticação.
* `--host`: Nome do host no Zabbix (o nome técnico, não o visível).
* `--item`: Nome do item que contém os logs.
* `--dias`: (Opcional) Número de dias para analisar (padrão: 1, ou seja, o dia anterior).
4. **Execute o Script**
O script precisa do caminho para o seu arquivo de log como argumento.
**Exemplo de execução para Performance:**
```bash
python src/performance-insights.py --server "[https://zabbix.suaempresa.com.br/](https://zabbix.suaempresa.com.br/)" --token "seu_token_aqui" --host "nome_do_host" --item "nome_do_item_de_log" --dias 7
```
```bash
python src/process_log.py /caminho/para/seu/nginx_logs.csv
```
*Substitua `/caminho/para/seu/nginx_logs.csv` pelo caminho real do seu arquivo.*
**Exemplo de execução para Auditoria de Acesso:**
```bash
python src/audit-logins.py --server "[https://zabbix.suaempresa.com.br/](https://zabbix.suaempresa.com.br/)" --token "seu_token_aqui" --host "nome_do_host" --item "nome_do_item_de_log"
```
5. **Abra o Relatório**
Após a execução, um arquivo chamado `painel_de_atividade_final.html` será criado no diretório raiz do projeto. Abra-o em qualquer navegador web para explorar o painel.
#### Para o Script de Análise NGINX (`relatorio-de-atividade.py`)
---
## Customização
* **Categorias de Ação:** Para ajustar as categorias ou adicionar novas, modifique o dicionário `ACTION_CATEGORIES` no início do script `src/process_log.py`.
* **Cores dos Gráficos:** As cores para cada categoria podem ser alteradas no dicionário `CATEGORY_COLORS`.
* **Lógica de Inferência:** A função `get_action_details` pode ser expandida com mais regras para identificar atividades específicas do seu sistema.
Este script requer apenas o caminho para o arquivo de log em formato CSV.
**Exemplo de execução:**
```bash
python src/relatorio-de-atividade.py /caminho/para/seu/nginx_logs.csv
```
---
## Licença
Este projeto está licenciado sob a Licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes.
Este projeto está licenciado sob a Licença MIT. Veja o arquivo `LICENSE` para mais detalhes.

332
src/audit-logins.py Normal file
View File

@ -0,0 +1,332 @@
# -*- coding: utf-8 -*-
import os
import json
import re
from datetime import datetime, timedelta, date
import pandas as pd
import plotly.express as px
from pyzabbix import ZabbixAPI
import time
import locale
import argparse
# --- CONFIGURAÇÕES ---
# As configurações agora são passadas via argumentos de linha de comando.
# Timeout para considerar uma nova sessão (em minutos)
SESSION_TIMEOUT_MINUTES = 5
# --- FIM DAS CONFIGURAÇÕES ---
def conectar_zabbix(server_url, api_token):
"""Conecta-se à API do Zabbix e retorna o objeto da API."""
try:
zapi = ZabbixAPI(server_url)
zapi.login(api_token=api_token)
print(f"Conectado com sucesso à API do Zabbix na versão: {zapi.api_version()}")
return zapi
except Exception as e:
print(f"Erro ao conectar ao Zabbix: {e}")
return None
def buscar_dados_zabbix(zapi, host_name, item_name, target_date):
"""Busca o histórico do item para uma data específica, hora por hora."""
try:
host = zapi.host.get(filter={"host": host_name}, output=["hostid"])
if not host:
print(f"Erro: Host '{host_name}' não encontrado.")
return None
hostid = host[0]['hostid']
print(f"Host '{host_name}' encontrado com ID: {hostid}")
item = zapi.item.get(filter={"name": item_name, "hostid": hostid}, output=["itemid", "name", "value_type"])
if not item:
print(f"Erro: Item '{item_name}' não encontrado no host '{host_name}'.")
return None
itemid = item[0]['itemid']
value_type = int(item[0]['value_type'])
print(f"Item '{item_name}' encontrado com ID: {itemid}")
all_history = []
print(f"Iniciando busca de histórico para o dia {target_date.strftime('%Y-%m-%d')} em blocos de 1 hora...")
for hora in range(24):
inicio_hora = datetime.combine(target_date, datetime.min.time()) + timedelta(hours=hora)
fim_hora = inicio_hora + timedelta(hours=1) - timedelta(seconds=1)
time_from = int(inicio_hora.timestamp())
time_till = int(fim_hora.timestamp())
history_chunk = zapi.history.get(
itemids=[itemid], time_from=time_from, time_till=time_till,
history=value_type, output='extend', sortfield='clock', sortorder='ASC'
)
if history_chunk:
all_history.extend(history_chunk)
time.sleep(0.2)
print(f"\nBusca finalizada para {target_date.strftime('%Y-%m-%d')}. Foram encontrados {len(all_history)} registros de log.")
return all_history
except Exception as e:
print(f"Erro ao buscar dados do Zabbix para o dia {target_date.strftime('%Y-%m-%d')}: {e}")
return None
def is_human_like_user(username):
"""Verifica se o nome de usuário parece ser de um humano, não um ID de máquina."""
# Aceita letras, números, ponto, underscore e hífen. Rejeita o resto.
if not username or not isinstance(username, str):
return False
return bool(re.match(r'^[a-zA-Z0-9._-]+$', username))
def normalizar_usuario(username):
"""Normaliza o nome do usuário para minúsculas e remove o domínio."""
if not isinstance(username, str): return ""
return re.sub(r'@.*$', '', username.lower())
def parsear_logs(history_data):
"""Analisa os dados brutos de log e extrai informações relevantes."""
parsed_data = []
potential_cookie_sessions = []
json_pattern = re.compile(r'\{.*\}')
if not history_data: return pd.DataFrame()
print("Analisando logs para extrair sessões...")
for record in history_data:
for line in record['value'].strip().split('\n'):
match = json_pattern.search(line)
if not match: continue
try:
log_json = json.loads(match.group(0))
user = log_json.get('remote_user')
if user and user != "-" and is_human_like_user(user):
parsed_data.append({'timestamp': log_json.get('@timestamp'), 'remote_user': user})
elif 'http_cookie' in log_json:
potential_cookie_sessions.append({'timestamp': log_json.get('@timestamp'), 'cookie': log_json.get('http_cookie')})
except json.JSONDecodeError: continue
known_users = {normalizar_usuario(entry['remote_user']) for entry in parsed_data}
if potential_cookie_sessions and known_users:
print(f"Usuários conhecidos: {len(known_users)}. Verificando cookies...")
for item in potential_cookie_sessions:
for user in known_users:
if re.search(r'\b' + re.escape(user) + r'\b', item['cookie']):
parsed_data.append({'timestamp': item['timestamp'], 'remote_user': user})
break
if not parsed_data: return pd.DataFrame()
for entry in parsed_data: entry['remote_user'] = normalizar_usuario(entry['remote_user'])
df = pd.DataFrame(parsed_data)
df['timestamp'] = pd.to_datetime(df['timestamp'], errors='coerce')
df.dropna(subset=['timestamp', 'remote_user'], inplace=True)
df.sort_values(by='timestamp', inplace=True)
return df
def calcular_sessoes(df, timeout_minutes):
"""Calcula as sessoes de usuário com base no tempo entre os requests."""
if df.empty: return pd.DataFrame()
df = df.sort_values(by=['remote_user', 'timestamp'])
df['time_diff'] = df.groupby('remote_user')['timestamp'].diff()
df['new_session'] = df['time_diff'] > pd.Timedelta(minutes=timeout_minutes)
df['session_id'] = df.groupby('remote_user')['new_session'].cumsum()
sessions = df.groupby(['remote_user', 'session_id']).agg(
start_time=('timestamp', 'min'),
end_time=('timestamp', 'max'),
event_count=('timestamp', 'count') # Conta eventos por sessão
).reset_index()
sessions['duration'] = sessions['end_time'] - sessions['start_time']
return sessions
def formatar_duracao_human(td):
"""Formata duração para formato amigável (ex: 8h 15min)."""
total_seconds = int(td.total_seconds())
if total_seconds < 60: return f"{total_seconds} seg"
minutes = total_seconds // 60
if minutes < 60: return f"{minutes} min"
hours, minutes_rem = divmod(minutes, 60)
if minutes_rem == 0: return f"{hours}h"
return f"{hours}h {minutes_rem}min"
def gerar_relatorio_html(sessions_df, summary_df, dia_relatorio_ymd):
"""Gera o relatório final em formato HTML com um gráfico interativo."""
if sessions_df.empty or summary_df.empty:
return f"<html><body><h1>Relatório de Sessões de Usuário - {dia_relatorio_ymd}</h1><p>Nenhuma atividade de usuário válida foi encontrada para este dia.</p></body></html>"
dia_obj = datetime.strptime(dia_relatorio_ymd, '%Y-%m-%d')
dia_relatorio_display = dia_obj.strftime('%d de %B de %Y').capitalize()
start_of_day, end_of_day = datetime.combine(dia_obj.date(), datetime.min.time()), datetime.combine(dia_obj.date(), datetime.max.time())
sorted_users_az = summary_df['remote_user'].tolist()
sorted_users_za_for_axis = sorted_users_az[::-1]
fig = px.timeline(sessions_df, x_start="start_time", x_end="end_time", y="remote_user", color="remote_user", custom_data=['duration_hover_str'])
fig.update_traces(hovertemplate="<b>Usuário:</b> %{y}<br><b>Início:</b> %{base|%H:%M:%S}<br><b>Fim:</b> %{x|%H:%M:%S}<br><b>Duração:</b> %{customdata[0]}<extra></extra>")
fig.update_yaxes(categoryorder="array", categoryarray=sorted_users_za_for_axis, showgrid=True, gridwidth=1, gridcolor='#E5E7EB')
fig.update_layout(title_x=0.5, xaxis_title=None, yaxis_title=None, showlegend=False, plot_bgcolor='white', paper_bgcolor='white', xaxis_range=[start_of_day, end_of_day], font=dict(family="Inter, sans-serif"))
graph_html = fig.to_html(full_html=False, include_plotlyjs='cdn', div_id='timeline-graph')
filter_controls = '<button class="user-btn active" id="select-all">Selecionar Todos</button>'
for user in sorted_users_az: filter_controls += f'<button class="user-btn" data-user="{user}">{user}</button>'
summary_html = ""
for _, row in summary_df.iterrows():
summary_html += f"""<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{row['remote_user']}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{row['start_of_service'].strftime('%H:%M:%S')}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{row['end_of_service'].strftime('%H:%M:%S')}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-blue-600">{row['total_duration_str']}</td>
</tr>"""
return f"""
<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8">
<title>Relatório de Acesso - {dia_relatorio_display}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> body {{ font-family: 'Inter', sans-serif; }} </style>
</head>
<body class="bg-gray-100">
<div class="container mx-auto p-4 sm:p-6 lg:p-8">
<header class="mb-8">
<h1 class="text-3xl font-bold text-gray-800">Relatório de Sessões de Usuário</h1>
<p class="text-lg text-gray-500">{dia_relatorio_display}</p>
</header>
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Linha do Tempo de Atividade</h2>
<div class="filter-controls flex flex-wrap gap-2 mb-4">{filter_controls}</div>
<div class="w-full h-full overflow-hidden">{graph_html}</div>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Resumo por Usuário</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"><tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Usuário</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Início do Serviço</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fim do Serviço</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tempo Total Online</th>
</tr></thead>
<tbody class="bg-white divide-y divide-gray-200">{summary_html}</tbody>
</table>
</div>
</div>
</div>
<style>
.user-btn {{ background-color: #F3F4F6; color: #374151; border: 1px solid #D1D5DB; padding: 6px 12px; border-radius: 9999px; cursor: pointer; transition: all 0.2s; font-size: 0.875rem; }}
.user-btn:hover {{ background-color: #E5E7EB; }}
.user-btn.active {{ background-color: #3B82F6; color: white; border-color: #3B82F6; font-weight: 600; }}
</style>
<script>
window.onload = function() {{
const graphDiv = document.getElementById('timeline-graph');
if (!graphDiv) return;
const observer = new MutationObserver((mutations, me) => {{
if (graphDiv.classList.contains('js-plotly-plot')) {{
initializeFilters(graphDiv);
me.disconnect();
}}
}});
observer.observe(document.body, {{ childList: true, subtree: true }});
}};
function initializeFilters(graphDiv) {{
const originalData = JSON.parse(JSON.stringify(graphDiv.data));
const allUserButtons = document.querySelectorAll('.user-btn[data-user]');
const selectAllButton = document.getElementById('select-all');
let selectedUsers = [];
function updateGraph() {{
const newData = (selectedUsers.length === 0) ? originalData : originalData.filter(trace => selectedUsers.includes(trace.name));
Plotly.react(graphDiv, newData, graphDiv.layout);
}}
allUserButtons.forEach(button => {{
button.addEventListener('click', () => {{
const user = button.dataset.user;
selectAllButton.classList.remove('active');
const index = selectedUsers.indexOf(user);
if (index > -1) {{ selectedUsers.splice(index, 1); button.classList.remove('active'); }}
else {{ selectedUsers.push(user); button.classList.add('active'); }}
if (selectedUsers.length === 0) selectAllButton.classList.add('active');
updateGraph();
}});
}});
selectAllButton.addEventListener('click', () => {{
selectedUsers = [];
allUserButtons.forEach(btn => btn.classList.remove('active'));
selectAllButton.classList.add('active');
updateGraph();
}});
}}
</script>
</body></html>"""
def main():
"""Função principal que orquestra a execução do script."""
parser = argparse.ArgumentParser(description="Gera relatórios de atividade de usuários do Zabbix.")
parser.add_argument("--server", required=True, help="URL do servidor Zabbix (ex: https://zabbix.example.com/zabbix/)")
parser.add_argument("--token", required=True, help="Token da API do Zabbix para autenticação.")
parser.add_argument("--host", required=True, help="Nome (não visível) do host no Zabbix.")
parser.add_argument("--item", required=True, help="Nome do item que contém os logs de acesso.")
parser.add_argument("--dias", type=int, default=1, help="Número de dias anteriores para gerar relatórios (padrão: 1).")
args = parser.parse_args()
try: locale.setlocale(locale.LC_TIME, 'pt_BR.UTF-8')
except locale.Error: print("Locale 'pt_BR.UTF-8' não encontrado. Usando locale padrão.")
zapi = conectar_zabbix(args.server, args.token)
if not zapi:
return
today = date.today()
for i in range(args.dias, 0, -1):
target_date = today - timedelta(days=i)
date_str = target_date.strftime('%Y-%m-%d')
print(f"\n--- Processando relatório para {date_str} ---")
history = buscar_dados_zabbix(zapi, args.host, args.item, target_date)
if history is None:
print(f"Não foi possível buscar dados para {date_str}. Pulando para o próximo dia.")
continue
logs_df = parsear_logs(history)
if logs_df.empty:
print(f"Nenhum dado de log válido encontrado para {date_str}.")
continue
sessions_df = calcular_sessoes(logs_df, SESSION_TIMEOUT_MINUTES)
# Filtro para remover usuários com apenas uma única conexão no dia
session_counts = sessions_df.groupby('remote_user')['session_id'].nunique()
event_counts = sessions_df.groupby('remote_user')['event_count'].sum()
single_event_users = session_counts[(session_counts == 1) & (event_counts == 1)].index
if not single_event_users.empty:
print(f"Removendo {len(single_event_users)} usuário(s) com atividade isolada: {', '.join(single_event_users)}")
sessions_df = sessions_df[~sessions_df['remote_user'].isin(single_event_users)]
if sessions_df.empty:
print(f"Nenhuma sessão de trabalho válida encontrada para {date_str} após a filtragem.")
continue
summary = sessions_df.groupby('remote_user').agg(
start_of_service=('start_time', 'min'),
end_of_service=('end_time', 'max'),
total_duration=('duration', 'sum')
).reset_index()
summary['total_duration_str'] = summary['total_duration'].apply(formatar_duracao_human)
summary = summary.sort_values('remote_user', ascending=True)
sessions_df['duration_hover_str'] = sessions_df['duration'].apply(formatar_duracao_human)
report_html = gerar_relatorio_html(sessions_df, summary, date_str)
file_name = f"relatorio_acesso_{date_str}.html"
try:
with open(file_name, 'w', encoding='utf-8') as f: f.write(report_html)
print(f"Relatório para {date_str} gerado com sucesso! Arquivo: {os.path.abspath(file_name)}")
except Exception as e:
print(f"Erro ao salvar o arquivo de relatório para {date_str}: {e}")
if __name__ == "__main__":
main()

279
src/performance-insights.py Normal file
View File

@ -0,0 +1,279 @@
# -*- coding: utf-8 -*-
import os
import json
import re
import ipaddress
from datetime import datetime, timedelta, date
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from pyzabbix import ZabbixAPI
import time
import locale
import argparse
# --- FUNÇÕES AUXILIARES ---
def conectar_zabbix(server_url, api_token):
"""Conecta-se à API do Zabbix."""
try:
zapi = ZabbixAPI(server_url)
zapi.login(api_token=api_token)
print(f"Conectado com sucesso à API do Zabbix na versão: {zapi.api_version()}")
return zapi
except Exception as e:
print(f"Erro ao conectar ao Zabbix: {e}")
return None
def buscar_dados_zabbix(zapi, host_name, item_name, target_date):
"""Busca o histórico do item para uma data específica."""
try:
host = zapi.host.get(filter={"host": host_name}, output=["hostid"])
if not host:
print(f"Erro: Host '{host_name}' não encontrado.")
return None
hostid = host[0]['hostid']
item = zapi.item.get(filter={"name": item_name, "hostid": hostid}, output=["itemid", "value_type"])
if not item:
print(f"Erro: Item '{item_name}' não encontrado no host '{host_name}'.")
return None
itemid = item[0]['itemid']
value_type = int(item[0]['value_type'])
all_history = []
print(f"Iniciando busca de histórico para o dia {target_date.strftime('%Y-%m-%d')}...")
for hora in range(24):
inicio_hora = datetime.combine(target_date, datetime.min.time()) + timedelta(hours=hora)
fim_hora = inicio_hora + timedelta(hours=1) - timedelta(seconds=1)
time_from, time_till = int(inicio_hora.timestamp()), int(fim_hora.timestamp())
history_chunk = zapi.history.get(
itemids=[itemid], time_from=time_from, time_till=time_till,
history=value_type, output='extend', sortfield='clock', sortorder='ASC'
)
if history_chunk:
all_history.extend(history_chunk)
time.sleep(0.1)
print(f"Busca finalizada. {len(all_history)} registros de log encontrados.")
return all_history
except Exception as e:
print(f"Erro ao buscar dados do Zabbix: {e}")
return None
def parsear_logs(history_data):
"""Analisa os dados brutos de log e extrai informações detalhadas."""
parsed_data = []
if not history_data: return pd.DataFrame()
print("Analisando logs para extrair insights...")
for record in history_data:
log_content = record['value'].strip()
last_brace = log_content.rfind('}')
if last_brace != -1:
log_content = log_content[:last_brace+1]
match = re.search(r'\{.*\}', log_content)
if not match: continue
try:
log_json = json.loads(match.group(0))
user = log_json.get('remote_user') or (re.search(r'nc_username=([^;]+)', log_json.get('http_cookie', '')).group(1) if 'nc_username=' in log_json.get('http_cookie', '') else None)
if user and re.match(r'^[a-zA-Z0-9._-]+$', user):
parsed_data.append({
'remote_user': re.sub(r'@.*$', '', user.lower()),
'request_time': float(log_json.get('request_time', 0)),
'status': int(log_json.get('status', 0)),
'request_uri': log_json.get('request_uri', '/')
})
except (json.JSONDecodeError, AttributeError, ValueError, TypeError): continue
if not parsed_data: return pd.DataFrame()
return pd.DataFrame(parsed_data)
def gerar_relatorio_html(df, dia_relatorio_ymd):
"""Gera o relatório HTML final com os insights de performance e acesso."""
if df.empty:
return f"<html><body><h1>Relatório de Performance - {dia_relatorio_ymd}</h1><p>Nenhuma atividade válida foi encontrada.</p></body></html>"
dia_obj = datetime.strptime(dia_relatorio_ymd, '%Y-%m-%d')
dia_relatorio_display = dia_obj.strftime('%d de %B de %Y').capitalize()
# --- Cálculos dos KPIs ---
office_pattern = r'^/(?:m|x|we|o|p|wv|op|wd|rtc|rtc2|layouts|view)/'
is_office_request = df['request_uri'].str.contains(office_pattern, na=False, regex=True)
df_office = df[is_office_request]
df_cloud = df[~is_office_request]
avg_total = df['request_time'].mean()
avg_office = df_office['request_time'].mean() if not df_office.empty else 0
avg_cloud = df_cloud['request_time'].mean() if not df_cloud.empty else 0
# Tiers de Velocidade Granulares
req_imediata = df[df['request_time'] < 0.05].shape[0]
req_rapida = df[(df['request_time'] >= 0.05) & (df['request_time'] < 0.2)].shape[0]
req_aceitavel = df[(df['request_time'] >= 0.2) & (df['request_time'] < 0.5)].shape[0]
req_lenta = df[(df['request_time'] >= 0.5) & (df['request_time'] < 2.0)].shape[0]
req_muito_lenta = df[df['request_time'] >= 2.0].shape[0]
# Análise de Erros 4xx
df_4xx = df[df['status'].between(400, 499)]
total_4xx = len(df_4xx)
total_requests = len(df)
erros_404 = len(df_4xx[df_4xx['status'] == 404])
erros_403 = len(df_4xx[df_4xx['status'] == 403])
outros_4xx = total_4xx - erros_404 - erros_403
top_404_uris = df_4xx[df_4xx['status'] == 404]['request_uri'].value_counts().head(5).reset_index()
top_404_uris.columns = ['request_uri', 'count']
top_403_uris = df_4xx[df_4xx['status'] == 403]['request_uri'].value_counts().head(5).reset_index()
top_403_uris.columns = ['request_uri', 'count']
top_outros_4xx_uris = df_4xx[~df_4xx['status'].isin([403, 404])]['request_uri'].value_counts().head(5).reset_index()
top_outros_4xx_uris.columns = ['request_uri', 'count']
# --- Gráficos ---
# Gráfico de Velocidade (Pizza com 5 tiers)
speed_data = pd.DataFrame({
'Categoria': ['Imediata (<50ms)', 'Rápida (50-200ms)', 'Aceitável (200-500ms)', 'Lenta (0.5-2s)', 'Muito Lenta (>2s)'],
'Count': [req_imediata, req_rapida, req_aceitavel, req_lenta, req_muito_lenta]
})
fig_speed = px.pie(speed_data, values='Count', names='Categoria', title='Classificação de Velocidade',
color_discrete_map={
'Imediata (<50ms)': '#10B981',
'Rápida (50-200ms)': '#84cc16',
'Aceitável (200-500ms)': '#F59E0B',
'Lenta (0.5-2s)': '#f97316',
'Muito Lenta (>2s)': '#EF4444'
},
category_orders={'Categoria': ['Imediata (<50ms)', 'Rápida (50-200ms)', 'Aceitável (200-500ms)', 'Lenta (0.5-2s)', 'Muito Lenta (>2s)']}
)
fig_speed.update_traces(textposition='inside', textinfo='percent', sort=False, hovertemplate='<b>%{label}</b><br>Requisições: %{value}<extra></extra>')
fig_speed.update_layout(title_x=0.5, font=dict(family="Inter, sans-serif"), legend_title_text='Categorias')
speed_chart_html = fig_speed.to_html(full_html=False, include_plotlyjs='cdn')
# Gráfico de Status (Pizza)
status_data = pd.DataFrame({
'Categoria': ['Acessos Normais', 'Não Encontrado (404)', 'Acesso Bloqueado (403)', 'Outros Erros 4xx'],
'Count': [total_requests - total_4xx, erros_404, erros_403, outros_4xx]
})
fig_status = px.pie(status_data, values='Count', names='Categoria', title='Proporção de Status HTTP',
color_discrete_map={
'Acessos Normais': '#10B981',
'Não Encontrado (404)': '#EF4444',
'Acesso Bloqueado (403)': '#f97316',
'Outros Erros 4xx': '#F59E0B'
})
fig_status.update_traces(textposition='inside', textinfo='percent', hovertemplate='<b>%{label}</b><br>Requisições: %{value}<extra></extra>')
fig_status.update_layout(title_x=0.5, font=dict(family="Inter, sans-serif"), legend_title_text='Status')
status_chart_html = fig_status.to_html(full_html=False, include_plotlyjs=False)
# --- Tabelas HTML ---
top_404_html = "".join([f'<tr><td class="px-6 py-2 text-sm text-gray-500 break-all">{row["request_uri"]}</td><td class="px-6 py-2 text-sm text-gray-500">{row["count"]}</td></tr>' for _, row in top_404_uris.iterrows()])
top_403_html = "".join([f'<tr><td class="px-6 py-2 text-sm text-gray-500 break-all">{row["request_uri"]}</td><td class="px-6 py-2 text-sm text-gray-500">{row["count"]}</td></tr>' for _, row in top_403_uris.iterrows()])
top_outros_4xx_html = "".join([f'<tr><td class="px-6 py-2 text-sm text-gray-500 break-all">{row["request_uri"]}</td><td class="px-6 py-2 text-sm text-gray-500">{row["count"]}</td></tr>' for _, row in top_outros_4xx_uris.iterrows()])
return f"""
<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8">
<title>Relatório de Performance - {dia_relatorio_display}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> body {{ font-family: 'Inter', sans-serif; }} </style>
</head>
<body class="bg-gray-100">
<div class="container mx-auto p-4 sm:p-6 lg:p-8">
<header class="mb-8"><h1 class="text-3xl font-bold text-gray-800">Relatório de Performance e Acesso</h1><p class="text-lg text-gray-500">{dia_relatorio_display}</p></header>
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Métricas de Tempo de Resposta</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-center">
<div class="bg-blue-50 p-4 rounded-lg"><p class="text-sm text-blue-600 font-medium">Média Total</p><p class="text-3xl font-bold text-blue-800">{avg_total:.3f}s</p></div>
<div class="bg-green-50 p-4 rounded-lg"><p class="text-sm text-green-600 font-medium">Média Cloud (Genérico)</p><p class="text-3xl font-bold text-green-800">{avg_cloud:.3f}s</p></div>
<div class="bg-purple-50 p-4 rounded-lg"><p class="text-sm text-purple-600 font-medium">Média Office Online</p><p class="text-3xl font-bold text-purple-800">{avg_office:.3f}s</p></div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<div class="bg-white rounded-lg shadow-md p-6">{speed_chart_html}</div>
<div class="bg-white rounded-lg shadow-md p-6">{status_chart_html}</div>
</div>
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Análise Detalhada de Erros 4xx (Erros do Cliente)</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-center mb-6">
<div class="bg-yellow-50 p-4 rounded-lg"><p class="text-sm text-yellow-600 font-medium">Não Encontrado (404)</p><p class="text-2xl font-bold text-yellow-800">{erros_404}</p></div>
<div class="bg-red-50 p-4 rounded-lg"><p class="text-sm text-red-600 font-medium">Acesso Bloqueado (403)</p><p class="text-2xl font-bold text-red-800">{erros_403}</p></div>
<div class="bg-gray-50 p-4 rounded-lg"><p class="text-sm text-gray-600 font-medium">Outros Erros 4xx</p><p class="text-2xl font-bold text-gray-800">{outros_4xx}</p></div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div>
<h3 class="font-semibold text-gray-600 mb-2">Top 5 URIs com Erro 404</h3>
<div class="overflow-x-auto border rounded-lg"><table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"><tr><th class="px-6 py-2 text-left text-xs font-medium text-gray-500 uppercase">URI</th><th class="px-6 py-2 text-left text-xs font-medium text-gray-500 uppercase">Count</th></tr></thead>
<tbody class="bg-white divide-y divide-gray-200">{top_404_html}</tbody>
</table></div>
</div>
<div>
<h3 class="font-semibold text-gray-600 mb-2">Top 5 URIs com Erro 403</h3>
<div class="overflow-x-auto border rounded-lg"><table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"><tr><th class="px-6 py-2 text-left text-xs font-medium text-gray-500 uppercase">URI</th><th class="px-6 py-2 text-left text-xs font-medium text-gray-500 uppercase">Count</th></tr></thead>
<tbody class="bg-white divide-y divide-gray-200">{top_403_html}</tbody>
</table></div>
</div>
<div>
<h3 class="font-semibold text-gray-600 mb-2">Top 5 URIs com Outros Erros 4xx</h3>
<div class="overflow-x-auto border rounded-lg"><table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"><tr><th class="px-6 py-2 text-left text-xs font-medium text-gray-500 uppercase">URI</th><th class="px-6 py-2 text-left text-xs font-medium text-gray-500 uppercase">Count</th></tr></thead>
<tbody class="bg-white divide-y divide-gray-200">{top_outros_4xx_html}</tbody>
</table></div>
</div>
</div>
</div>
</div>
</body></html>"""
def main():
parser = argparse.ArgumentParser(description="Gera relatórios de performance a partir de logs do Zabbix.")
parser.add_argument("--server", required=True, help="URL do servidor Zabbix")
parser.add_argument("--token", required=True, help="Token da API do Zabbix.")
parser.add_argument("--host", required=True, help="Nome do host no Zabbix.")
parser.add_argument("--item", required=True, help="Nome do item que contém os logs.")
parser.add_argument("--dias", type=int, default=1, help="Número de dias anteriores para gerar relatórios.")
args = parser.parse_args()
try: locale.setlocale(locale.LC_TIME, 'pt_BR.UTF-8')
except locale.Error: print("Locale 'pt_BR.UTF-8' não encontrado.")
zapi = conectar_zabbix(args.server, args.token)
if not zapi: return
today = date.today()
for i in range(args.dias, 0, -1):
target_date = today - timedelta(days=i)
date_str = target_date.strftime('%Y-%m-%d')
print(f"\n--- Processando relatório para {date_str} ---")
history = buscar_dados_zabbix(zapi, args.host, args.item, target_date)
if not history:
print(f"Nenhum dado encontrado para {date_str}.")
continue
logs_df = parsear_logs(history)
if logs_df.empty:
print(f"Nenhum dado válido para processar em {date_str}.")
continue
report_html = gerar_relatorio_html(logs_df, date_str)
file_name = f"relatorio_insights_{date_str}.html"
try:
with open(file_name, 'w', encoding='utf-8') as f: f.write(report_html)
print(f"Relatório gerado com sucesso: {os.path.abspath(file_name)}")
except Exception as e:
print(f"Erro ao salvar o relatório: {e}")
if __name__ == "__main__":
main()