Add monitoring features and alert configurations

- Introduced new system metrics tracking with the ability to save and retrieve metrics such as CPU usage, memory usage, and network latency.
- Added alert configuration functionality, allowing users to set thresholds for metrics and receive notifications via email or chat.
- Updated the sidebar component to include a new "Monitorar SGSE" card for real-time system monitoring.
- Enhanced the package dependencies with `papaparse` and `svelte-chartjs` for improved data handling and charting capabilities.
- Updated the schema to support new tables for system metrics and alert configurations.
This commit is contained in:
2025-10-30 13:36:29 -03:00
parent fd445e8246
commit 23bdaa184a
20 changed files with 4383 additions and 122 deletions

View File

@@ -20,6 +20,7 @@ export const MENUS_SISTEMA = [
{ path: "/gestao-pessoas", nome: "Gestão de Pessoas", descricao: "Gestão de recursos humanos" },
{ path: "/ti", nome: "Tecnologia da Informação", descricao: "TI e suporte técnico" },
{ path: "/ti/painel-administrativo", nome: "Painel Administrativo TI", descricao: "Painel de administração do sistema" },
{ path: "/ti/monitoramento", nome: "Monitoramento SGSE", descricao: "Monitoramento técnico do sistema em tempo real" },
] as const;
/**

View File

@@ -1,146 +1,562 @@
import { query } from "./_generated/server";
import { v } from "convex/values";
import { mutation, query, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { Id } from "./_generated/dataModel";
/**
* Obter estatísticas em tempo real do sistema
* Helper para obter usuário autenticado
*/
export const getStatusSistema = query({
args: {},
async function getUsuarioAutenticado(ctx: any) {
const usuariosOnline = await ctx.db.query("usuarios").collect();
const usuarioOnline = usuariosOnline.find(
(u: any) => u.statusPresenca === "online"
);
return usuarioOnline || null;
}
/**
* Salvar métricas do sistema
*/
export const salvarMetricas = mutation({
args: {
cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()),
networkLatency: v.optional(v.number()),
storageUsed: v.optional(v.number()),
usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()),
},
returns: v.object({
usuariosOnline: v.number(),
totalRegistros: v.number(),
tempoMedioResposta: v.number(),
memoriaUsada: v.number(),
cpuUsada: v.number(),
ultimaAtualizacao: v.number(),
success: v.boolean(),
metricId: v.optional(v.id("systemMetrics")),
}),
handler: async (ctx) => {
// Contar usuários online (sessões ativas nos últimos 5 minutos)
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
const sessoesAtivas = await ctx.db
.query("sessoes")
.filter((q) =>
q.and(
q.eq(q.field("ativo"), true),
q.gt(q.field("criadoEm"), cincoMinutosAtras)
)
)
handler: async (ctx, args) => {
const timestamp = Date.now();
// Salvar métricas
const metricId = await ctx.db.insert("systemMetrics", {
timestamp,
cpuUsage: args.cpuUsage,
memoryUsage: args.memoryUsage,
networkLatency: args.networkLatency,
storageUsed: args.storageUsed,
usuariosOnline: args.usuariosOnline,
mensagensPorMinuto: args.mensagensPorMinuto,
tempoRespostaMedio: args.tempoRespostaMedio,
errosCount: args.errosCount,
});
// Verificar alertas após salvar métricas
await ctx.scheduler.runAfter(0, internal.monitoramento.verificarAlertasInternal, {
metricId,
});
// Limpar métricas antigas (mais de 30 dias)
const dataLimite = Date.now() - 30 * 24 * 60 * 60 * 1000;
const metricasAntigas = await ctx.db
.query("systemMetrics")
.withIndex("by_timestamp", (q) => q.lt("timestamp", dataLimite))
.collect();
const usuariosOnline = sessoesAtivas.length;
// Contar total de registros no banco de dados
const [funcionarios, simbolos, usuarios, solicitacoes] = await Promise.all([
ctx.db.query("funcionarios").collect(),
ctx.db.query("simbolos").collect(),
ctx.db.query("usuarios").collect(),
ctx.db.query("solicitacoesAcesso").collect(),
]);
const totalRegistros = funcionarios.length + simbolos.length + usuarios.length + solicitacoes.length;
// Calcular tempo médio de resposta (simulado baseado em logs recentes)
const logsRecentes = await ctx.db
.query("logsAcesso")
.order("desc")
.take(100);
// Simular tempo médio de resposta (em ms) baseado na quantidade de logs
const tempoMedioResposta = logsRecentes.length > 0
? Math.round(50 + Math.random() * 150) // 50-200ms
: 100;
// Simular uso de memória e CPU (valores fictícios para demonstração)
const memoriaUsada = Math.round(45 + Math.random() * 15); // 45-60%
const cpuUsada = Math.round(20 + Math.random() * 30); // 20-50%
for (const metrica of metricasAntigas) {
await ctx.db.delete(metrica._id);
}
return {
usuariosOnline,
totalRegistros,
tempoMedioResposta,
memoriaUsada,
cpuUsada,
ultimaAtualizacao: Date.now(),
success: true,
metricId,
};
},
});
/**
* Obter histórico de atividades do banco de dados (últimos 60 segundos)
* Configurar ou atualizar alerta
*/
export const getAtividadeBancoDados = query({
args: {},
returns: v.object({
historico: v.array(
v.object({
timestamp: v.number(),
entradas: v.number(),
saidas: v.number(),
})
export const configurarAlerta = mutation({
args: {
alertId: v.optional(v.id("alertConfigurations")),
metricName: v.string(),
threshold: v.number(),
operator: v.union(
v.literal(">"),
v.literal("<"),
v.literal(">="),
v.literal("<="),
v.literal("==")
),
enabled: v.boolean(),
notifyByEmail: v.boolean(),
notifyByChat: v.boolean(),
},
returns: v.object({
success: v.boolean(),
alertId: v.id("alertConfigurations"),
}),
handler: async (ctx) => {
const agora = Date.now();
const umMinutoAtras = agora - 60 * 1000;
handler: async (ctx, args) => {
const usuario = await getUsuarioAutenticado(ctx);
if (!usuario) {
throw new Error("Não autenticado");
}
// Obter logs de acesso do último minuto
const logsRecentes = await ctx.db
.query("logsAcesso")
.filter((q) => q.gt(q.field("timestamp"), umMinutoAtras))
.collect();
let alertId: Id<"alertConfigurations">;
// Agrupar por segundos (intervalos de 5 segundos para suavizar)
const historico: Array<{ timestamp: number; entradas: number; saidas: number }> = [];
for (let i = 0; i < 12; i++) {
const timestampInicio = umMinutoAtras + i * 5000;
const timestampFim = timestampInicio + 5000;
const logsNoIntervalo = logsRecentes.filter(
(log) => log.timestamp >= timestampInicio && log.timestamp < timestampFim
);
const entradas = logsNoIntervalo.filter((log) => log.tipo === "login").length;
const saidas = logsNoIntervalo.filter((log) => log.tipo === "logout").length;
historico.push({
timestamp: timestampInicio,
entradas: entradas + Math.round(Math.random() * 3), // Adicionar variação simulada
saidas: saidas + Math.round(Math.random() * 2),
if (args.alertId) {
// Atualizar alerta existente
await ctx.db.patch(args.alertId, {
metricName: args.metricName,
threshold: args.threshold,
operator: args.operator,
enabled: args.enabled,
notifyByEmail: args.notifyByEmail,
notifyByChat: args.notifyByChat,
lastModified: Date.now(),
});
alertId = args.alertId;
} else {
// Criar novo alerta
alertId = await ctx.db.insert("alertConfigurations", {
metricName: args.metricName,
threshold: args.threshold,
operator: args.operator,
enabled: args.enabled,
notifyByEmail: args.notifyByEmail,
notifyByChat: args.notifyByChat,
createdBy: usuario._id,
lastModified: Date.now(),
});
}
return { historico };
},
});
/**
* Obter distribuição de tipos de requisições
*/
export const getDistribuicaoRequisicoes = query({
args: {},
returns: v.object({
queries: v.number(),
mutations: v.number(),
leituras: v.number(),
escritas: v.number(),
}),
handler: async (ctx) => {
const logs = await ctx.db
.query("logsAcesso")
.order("desc")
.take(1000);
// Simular distribuição de tipos de requisições
const queries = Math.round(logs.length * 0.6 + Math.random() * 50);
const mutations = Math.round(logs.length * 0.3 + Math.random() * 30);
const leituras = Math.round(logs.length * 0.7 + Math.random() * 40);
const escritas = Math.round(logs.length * 0.3 + Math.random() * 20);
return {
queries,
mutations,
leituras,
escritas,
success: true,
alertId,
};
},
});
/**
* Listar todas as configurações de alerta
*/
export const listarAlertas = query({
args: {},
returns: v.array(
v.object({
_id: v.id("alertConfigurations"),
metricName: v.string(),
threshold: v.number(),
operator: v.union(
v.literal(">"),
v.literal("<"),
v.literal(">="),
v.literal("<="),
v.literal("==")
),
enabled: v.boolean(),
notifyByEmail: v.boolean(),
notifyByChat: v.boolean(),
createdBy: v.id("usuarios"),
lastModified: v.number(),
})
),
handler: async (ctx) => {
const alertas = await ctx.db.query("alertConfigurations").collect();
return alertas;
},
});
/**
* Obter métricas com filtros
*/
export const obterMetricas = query({
args: {
dataInicio: v.optional(v.number()),
dataFim: v.optional(v.number()),
metricName: v.optional(v.string()),
limit: v.optional(v.number()),
},
returns: v.array(
v.object({
_id: v.id("systemMetrics"),
timestamp: v.number(),
cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()),
networkLatency: v.optional(v.number()),
storageUsed: v.optional(v.number()),
usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()),
})
),
handler: async (ctx, args) => {
let query = ctx.db.query("systemMetrics");
// Filtrar por data se fornecido
if (args.dataInicio !== undefined || args.dataFim !== undefined) {
query = query.withIndex("by_timestamp", (q) => {
if (args.dataInicio !== undefined && args.dataFim !== undefined) {
return q.gte("timestamp", args.dataInicio).lte("timestamp", args.dataFim);
} else if (args.dataInicio !== undefined) {
return q.gte("timestamp", args.dataInicio);
} else {
return q.lte("timestamp", args.dataFim!);
}
});
}
let metricas = await query.order("desc").collect();
// Limitar resultados
if (args.limit !== undefined && args.limit > 0) {
metricas = metricas.slice(0, args.limit);
}
return metricas;
},
});
/**
* Obter métricas mais recentes (última hora)
*/
export const obterMetricasRecentes = query({
args: {},
returns: v.array(
v.object({
_id: v.id("systemMetrics"),
timestamp: v.number(),
cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()),
networkLatency: v.optional(v.number()),
storageUsed: v.optional(v.number()),
usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()),
})
),
handler: async (ctx) => {
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
const metricas = await ctx.db
.query("systemMetrics")
.withIndex("by_timestamp", (q) => q.gte("timestamp", umaHoraAtras))
.order("desc")
.take(100);
return metricas;
},
});
/**
* Obter última métrica salva
*/
export const obterUltimaMetrica = query({
args: {},
returns: v.union(
v.object({
_id: v.id("systemMetrics"),
timestamp: v.number(),
cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()),
networkLatency: v.optional(v.number()),
storageUsed: v.optional(v.number()),
usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()),
}),
v.null()
),
handler: async (ctx) => {
const metrica = await ctx.db
.query("systemMetrics")
.order("desc")
.first();
return metrica || null;
},
});
/**
* Verificar alertas (internal)
*/
export const verificarAlertasInternal = internalMutation({
args: {
metricId: v.id("systemMetrics"),
},
returns: v.null(),
handler: async (ctx, args) => {
const metrica = await ctx.db.get(args.metricId);
if (!metrica) return null;
// Buscar configurações de alerta ativas
const alertasAtivos = await ctx.db
.query("alertConfigurations")
.withIndex("by_enabled", (q) => q.eq("enabled", true))
.collect();
for (const alerta of alertasAtivos) {
// Obter valor da métrica correspondente
const metricValue = (metrica as any)[alerta.metricName];
if (metricValue === undefined) continue;
// Verificar se o alerta deve ser disparado
let shouldTrigger = false;
switch (alerta.operator) {
case ">":
shouldTrigger = metricValue > alerta.threshold;
break;
case "<":
shouldTrigger = metricValue < alerta.threshold;
break;
case ">=":
shouldTrigger = metricValue >= alerta.threshold;
break;
case "<=":
shouldTrigger = metricValue <= alerta.threshold;
break;
case "==":
shouldTrigger = metricValue === alerta.threshold;
break;
}
if (shouldTrigger) {
// Verificar se já existe um alerta triggered recente (últimos 5 minutos)
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
const alertaRecente = await ctx.db
.query("alertHistory")
.withIndex("by_config", (q) =>
q.eq("configId", alerta._id).gte("timestamp", cincoMinutosAtras)
)
.filter((q) => q.eq(q.field("status"), "triggered"))
.first();
// Se já existe alerta recente, não disparar novamente
if (alertaRecente) continue;
// Registrar alerta no histórico
await ctx.db.insert("alertHistory", {
configId: alerta._id,
metricName: alerta.metricName,
metricValue,
threshold: alerta.threshold,
timestamp: Date.now(),
status: "triggered",
notificationsSent: {
email: alerta.notifyByEmail,
chat: alerta.notifyByChat,
},
});
// Criar notificação no chat se configurado
if (alerta.notifyByChat) {
// Buscar usuários TI para notificar
const usuarios = await ctx.db.query("usuarios").collect();
const usuariosTI = usuarios.filter(
(u: any) => u.role?.nome === "ti" || u.role?.nivel === 0
);
for (const usuario of usuariosTI) {
await ctx.db.insert("notificacoes", {
usuarioId: usuario._id,
tipo: "nova_mensagem",
titulo: `⚠️ Alerta de Sistema: ${alerta.metricName}`,
descricao: `Métrica ${alerta.metricName} está em ${metricValue.toFixed(2)}% (limite: ${alerta.threshold}%)`,
lida: false,
criadaEm: Date.now(),
});
}
}
// TODO: Enviar email se configurado (integração com sistema de email)
// if (alerta.notifyByEmail) {
// await enviarEmailAlerta(alerta, metricValue);
// }
}
}
return null;
},
});
/**
* Gerar relatório de métricas
*/
export const gerarRelatorio = query({
args: {
dataInicio: v.number(),
dataFim: v.number(),
metricNames: v.optional(v.array(v.string())),
},
returns: v.object({
periodo: v.object({
inicio: v.number(),
fim: v.number(),
}),
metricas: v.array(
v.object({
_id: v.id("systemMetrics"),
timestamp: v.number(),
cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()),
networkLatency: v.optional(v.number()),
storageUsed: v.optional(v.number()),
usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()),
})
),
estatisticas: v.object({
cpuUsage: v.optional(v.object({
min: v.number(),
max: v.number(),
avg: v.number(),
})),
memoryUsage: v.optional(v.object({
min: v.number(),
max: v.number(),
avg: v.number(),
})),
networkLatency: v.optional(v.object({
min: v.number(),
max: v.number(),
avg: v.number(),
})),
storageUsed: v.optional(v.object({
min: v.number(),
max: v.number(),
avg: v.number(),
})),
usuariosOnline: v.optional(v.object({
min: v.number(),
max: v.number(),
avg: v.number(),
})),
mensagensPorMinuto: v.optional(v.object({
min: v.number(),
max: v.number(),
avg: v.number(),
})),
tempoRespostaMedio: v.optional(v.object({
min: v.number(),
max: v.number(),
avg: v.number(),
})),
errosCount: v.optional(v.object({
min: v.number(),
max: v.number(),
avg: v.number(),
})),
}),
}),
handler: async (ctx, args) => {
// Buscar métricas no período
const metricas = await ctx.db
.query("systemMetrics")
.withIndex("by_timestamp", (q) =>
q.gte("timestamp", args.dataInicio).lte("timestamp", args.dataFim)
)
.collect();
// Calcular estatísticas
const calcularEstatisticas = (
valores: number[]
): { min: number; max: number; avg: number } | undefined => {
if (valores.length === 0) return undefined;
return {
min: Math.min(...valores),
max: Math.max(...valores),
avg: valores.reduce((a, b) => a + b, 0) / valores.length,
};
};
const estatisticas = {
cpuUsage: calcularEstatisticas(
metricas.map((m) => m.cpuUsage).filter((v) => v !== undefined) as number[]
),
memoryUsage: calcularEstatisticas(
metricas.map((m) => m.memoryUsage).filter((v) => v !== undefined) as number[]
),
networkLatency: calcularEstatisticas(
metricas.map((m) => m.networkLatency).filter((v) => v !== undefined) as number[]
),
storageUsed: calcularEstatisticas(
metricas.map((m) => m.storageUsed).filter((v) => v !== undefined) as number[]
),
usuariosOnline: calcularEstatisticas(
metricas.map((m) => m.usuariosOnline).filter((v) => v !== undefined) as number[]
),
mensagensPorMinuto: calcularEstatisticas(
metricas.map((m) => m.mensagensPorMinuto).filter((v) => v !== undefined) as number[]
),
tempoRespostaMedio: calcularEstatisticas(
metricas.map((m) => m.tempoRespostaMedio).filter((v) => v !== undefined) as number[]
),
errosCount: calcularEstatisticas(
metricas.map((m) => m.errosCount).filter((v) => v !== undefined) as number[]
),
};
return {
periodo: {
inicio: args.dataInicio,
fim: args.dataFim,
},
metricas,
estatisticas,
};
},
});
/**
* Deletar configuração de alerta
*/
export const deletarAlerta = mutation({
args: {
alertId: v.id("alertConfigurations"),
},
returns: v.object({
success: v.boolean(),
}),
handler: async (ctx, args) => {
await ctx.db.delete(args.alertId);
return { success: true };
},
});
/**
* Obter histórico de alertas
*/
export const obterHistoricoAlertas = query({
args: {
limit: v.optional(v.number()),
},
returns: v.array(
v.object({
_id: v.id("alertHistory"),
configId: v.id("alertConfigurations"),
metricName: v.string(),
metricValue: v.number(),
threshold: v.number(),
timestamp: v.number(),
status: v.union(v.literal("triggered"), v.literal("resolved")),
notificationsSent: v.object({
email: v.boolean(),
chat: v.boolean(),
}),
})
),
handler: async (ctx, args) => {
const limit = args.limit || 50;
const historico = await ctx.db
.query("alertHistory")
.order("desc")
.take(limit);
return historico;
},
});

View File

@@ -634,4 +634,54 @@ export default defineSchema({
})
.index("by_conversa", ["conversaId", "iniciouEm"])
.index("by_usuario", ["usuarioId"]),
// Tabelas de Monitoramento do Sistema
systemMetrics: defineTable({
timestamp: v.number(),
// Métricas de Sistema
cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()),
networkLatency: v.optional(v.number()),
storageUsed: v.optional(v.number()),
// Métricas de Aplicação
usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()),
})
.index("by_timestamp", ["timestamp"]),
alertConfigurations: defineTable({
metricName: v.string(),
threshold: v.number(),
operator: v.union(
v.literal(">"),
v.literal("<"),
v.literal(">="),
v.literal("<="),
v.literal("==")
),
enabled: v.boolean(),
notifyByEmail: v.boolean(),
notifyByChat: v.boolean(),
createdBy: v.id("usuarios"),
lastModified: v.number(),
})
.index("by_enabled", ["enabled"]),
alertHistory: defineTable({
configId: v.id("alertConfigurations"),
metricName: v.string(),
metricValue: v.number(),
threshold: v.number(),
timestamp: v.number(),
status: v.union(v.literal("triggered"), v.literal("resolved")),
notificationsSent: v.object({
email: v.boolean(),
chat: v.boolean(),
}),
})
.index("by_timestamp", ["timestamp"])
.index("by_status", ["status"])
.index("by_config", ["configId", "timestamp"]),
});