ajuste do readme e inclusão de novas ferramentas
This commit is contained in:
parent
d01ae7ec46
commit
03415c7922
161
README.md
161
README.md
|
|
@ -1,48 +1,56 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||

|
|
||||||
*(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`.
|
||||||
|
|
||||||
```nginx
|
#### Exemplo de `log_format` para `relatorio-de-atividade.py`
|
||||||
log_format custom_csv escape=json
|
Adicione este formato à sua configuração do NGINX (`nginx.conf`):
|
||||||
|
```nginx
|
||||||
|
log_format custom_csv escape=json
|
||||||
'$time_iso8601,'
|
'$time_iso8601,'
|
||||||
'$remote_addr,'
|
'$remote_addr,'
|
||||||
'"$http_user_agent",'
|
'"$http_user_agent",'
|
||||||
|
|
@ -54,56 +62,81 @@ O script opera em quatro etapas principais:
|
||||||
'$upstream_response_time,'
|
'$upstream_response_time,'
|
||||||
'"$request"';
|
'"$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
|
## Como Utilizar
|
||||||
|
|
||||||
1. **Clone o Repositório**
|
A execução varia dependendo do script que você deseja usar.
|
||||||
```bash
|
|
||||||
git clone [URL_DO_SEU_REPO_GITEA]
|
|
||||||
cd relatorio-atividade-nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Crie e Ative um Ambiente Virtual (Recomendado)**
|
#### Para os Scripts de Zabbix (`performance-insights.py` e `audit-logins.py`)
|
||||||
```bash
|
|
||||||
# Para Linux/macOS
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# Para Windows
|
Ambos os scripts compartilham os mesmos parâmetros de linha de comando para se conectar ao Zabbix.
|
||||||
python -m venv venv
|
|
||||||
.\venv\Scripts\activate
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Instale as Dependências**
|
**Argumentos:**
|
||||||
```bash
|
* `--server`: URL completa do seu servidor Zabbix.
|
||||||
pip install -r requirements.txt
|
* `--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**
|
**Exemplo de execução para Performance:**
|
||||||
O script precisa do caminho para o seu arquivo de log como argumento.
|
```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
|
**Exemplo de execução para Auditoria de Acesso:**
|
||||||
python src/process_log.py /caminho/para/seu/nginx_logs.csv
|
```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"
|
||||||
*Substitua `/caminho/para/seu/nginx_logs.csv` pelo caminho real do seu arquivo.*
|
```
|
||||||
|
|
||||||
5. **Abra o Relatório**
|
#### Para o Script de Análise NGINX (`relatorio-de-atividade.py`)
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
Este script requer apenas o caminho para o arquivo de log em formato CSV.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
Loading…
Reference in New Issue