From 03415c7922f766dd4430760e33b6f716240831d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Toledo?= Date: Fri, 17 Oct 2025 22:47:25 -0300 Subject: [PATCH] =?UTF-8?q?ajuste=20do=20readme=20e=20inclus=C3=A3o=20de?= =?UTF-8?q?=20novas=20ferramentas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 181 ++++++++++++-------- src/audit-logins.py | 332 ++++++++++++++++++++++++++++++++++++ src/performance-insights.py | 279 ++++++++++++++++++++++++++++++ 3 files changed, 718 insertions(+), 74 deletions(-) create mode 100644 src/audit-logins.py create mode 100644 src/performance-insights.py diff --git a/README.md b/README.md index 5a415f7..dbc7809 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file +Este projeto está licenciado sob a Licença MIT. Veja o arquivo `LICENSE` para mais detalhes. \ No newline at end of file diff --git a/src/audit-logins.py b/src/audit-logins.py new file mode 100644 index 0000000..272175c --- /dev/null +++ b/src/audit-logins.py @@ -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"

Relatório de Sessões de Usuário - {dia_relatorio_ymd}

Nenhuma atividade de usuário válida foi encontrada para este dia.

" + + 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="Usuário: %{y}
Início: %{base|%H:%M:%S}
Fim: %{x|%H:%M:%S}
Duração: %{customdata[0]}") + 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 = '' + for user in sorted_users_az: filter_controls += f'' + + summary_html = "" + for _, row in summary_df.iterrows(): + summary_html += f""" + {row['remote_user']} + {row['start_of_service'].strftime('%H:%M:%S')} + {row['end_of_service'].strftime('%H:%M:%S')} + {row['total_duration_str']} + """ + + return f""" + + Relatório de Acesso - {dia_relatorio_display} + + + + + + + +
+
+

Relatório de Sessões de Usuário

+

{dia_relatorio_display}

+
+ +
+

Linha do Tempo de Atividade

+
{filter_controls}
+
{graph_html}
+
+ +
+

Resumo por Usuário

+
+ + + + + + + + {summary_html} +
UsuárioInício do ServiçoFim do ServiçoTempo Total Online
+
+
+
+ + + """ + +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() \ No newline at end of file diff --git a/src/performance-insights.py b/src/performance-insights.py new file mode 100644 index 0000000..a7e0568 --- /dev/null +++ b/src/performance-insights.py @@ -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"

Relatório de Performance - {dia_relatorio_ymd}

Nenhuma atividade válida foi encontrada.

" + + 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='%{label}
Requisições: %{value}') + 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='%{label}
Requisições: %{value}') + 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'{row["request_uri"]}{row["count"]}' for _, row in top_404_uris.iterrows()]) + top_403_html = "".join([f'{row["request_uri"]}{row["count"]}' for _, row in top_403_uris.iterrows()]) + top_outros_4xx_html = "".join([f'{row["request_uri"]}{row["count"]}' for _, row in top_outros_4xx_uris.iterrows()]) + + return f""" + + Relatório de Performance - {dia_relatorio_display} + + + + + +
+

Relatório de Performance e Acesso

{dia_relatorio_display}

+ +
+

Métricas de Tempo de Resposta

+
+

Média Total

{avg_total:.3f}s

+

Média Cloud (Genérico)

{avg_cloud:.3f}s

+

Média Office Online

{avg_office:.3f}s

+
+
+ +
+
{speed_chart_html}
+
{status_chart_html}
+
+ +
+

Análise Detalhada de Erros 4xx (Erros do Cliente)

+
+

Não Encontrado (404)

{erros_404}

+

Acesso Bloqueado (403)

{erros_403}

+

Outros Erros 4xx

{outros_4xx}

+
+
+
+

Top 5 URIs com Erro 404

+
+ + {top_404_html} +
URICount
+
+
+

Top 5 URIs com Erro 403

+
+ + {top_403_html} +
URICount
+
+
+

Top 5 URIs com Outros Erros 4xx

+
+ + {top_outros_4xx_html} +
URICount
+
+
+
+
+ """ + +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() +