- 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.
563 lines
16 KiB
TypeScript
563 lines
16 KiB
TypeScript
import { v } from "convex/values";
|
|
import { mutation, query, internalMutation } from "./_generated/server";
|
|
import { internal } from "./_generated/api";
|
|
import { Id } from "./_generated/dataModel";
|
|
|
|
/**
|
|
* Helper para obter usuário autenticado
|
|
*/
|
|
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({
|
|
success: v.boolean(),
|
|
metricId: v.optional(v.id("systemMetrics")),
|
|
}),
|
|
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();
|
|
|
|
for (const metrica of metricasAntigas) {
|
|
await ctx.db.delete(metrica._id);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
metricId,
|
|
};
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Configurar ou atualizar alerta
|
|
*/
|
|
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, args) => {
|
|
const usuario = await getUsuarioAutenticado(ctx);
|
|
if (!usuario) {
|
|
throw new Error("Não autenticado");
|
|
}
|
|
|
|
let alertId: Id<"alertConfigurations">;
|
|
|
|
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 {
|
|
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;
|
|
},
|
|
});
|