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

133
README.md
View File

@ -1,46 +1,54 @@
# 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. 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.
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`)*
--- ---
## Funcionalidades Principais ## As Ferramentas
* **Processamento de Logs:** Lê arquivos de log NGINX em formato CSV, otimizado para lidar com grandes volumes de dados. A suíte é composta por três scripts principais, cada um com um objetivo específico:
* **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.
--- ### 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. * **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.
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. * **Responde a perguntas como:**
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. * "Quais usuários acessaram o sistema hoje?"
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. * "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 ## Pré-requisitos
* **Python 3.8+** * **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`.
#### Exemplo de `log_format` para `relatorio-de-atividade.py`
Adicione este formato à sua configuração do NGINX (`nginx.conf`):
```nginx ```nginx
log_format custom_csv escape=json log_format custom_csv escape=json
'$time_iso8601,' '$time_iso8601,'
@ -59,51 +67,76 @@ O script opera em quatro etapas principais:
--- ---
## Como Utilizar ## Instalação
1. **Clone o Repositório** 1. **Clone o Repositório**
```bash ```bash
git clone [URL_DO_SEU_REPO_GITEA] git clone [URL_DO_SEU_REPO_GITEA]
cd relatorio-atividade-nginx cd [NOME_DO_REPOSITORIO]
``` ```
2. **Crie e Ative um Ambiente Virtual (Recomendado)** 2. **Crie e Ative um Ambiente Virtual (Recomendado)**
```bash ```bash
# Para Linux/macOS
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate
# Para Windows
python -m venv venv
.\venv\Scripts\activate
``` ```
3. **Instale as Dependências** 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 ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
4. **Execute o Script**
O script precisa do caminho para o seu arquivo de log como argumento.
```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.*
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.
--- ---
## Customização ## Como Utilizar
* **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`. A execução varia dependendo do script que você deseja usar.
* **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.
#### Para os Scripts de Zabbix (`performance-insights.py` e `audit-logins.py`)
Ambos os scripts compartilham os mesmos parâmetros de linha de comando para se conectar ao Zabbix.
**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).
**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
```
**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"
```
#### Para o Script de Análise NGINX (`relatorio-de-atividade.py`)
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 ## 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()