280 lines
15 KiB
Python
280 lines
15 KiB
Python
# -*- 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()
|
|
|