Files
sgse-app/packages/backend/convex/monitoramento.ts

822 lines
22 KiB
TypeScript

import { v } from 'convex/values';
import { mutation, query, internalMutation } from './_generated/server';
import { internal, api } from './_generated/api';
import { Id } from './_generated/dataModel';
import type { QueryCtx } from './_generated/server';
/**
* Helper para obter usuário autenticado
*/
async function getUsuarioAutenticado(ctx: QueryCtx) {
const usuariosOnline = await ctx.db.query('usuarios').collect();
const usuarioOnline = usuariosOnline.find((u) => 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: {},
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) => {
// Construir consulta respeitando tipos sem reatribuições
let metricas;
if (args.dataInicio !== undefined && args.dataFim !== undefined) {
const inicio: number = args.dataInicio as number;
const fim: number = args.dataFim as number;
metricas = await ctx.db
.query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', inicio).lte('timestamp', fim))
.order('desc')
.collect();
} else if (args.dataInicio !== undefined) {
const inicio: number = args.dataInicio as number;
metricas = await ctx.db
.query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', inicio))
.order('desc')
.collect();
} else if (args.dataFim !== undefined) {
const fim: number = args.dataFim as number;
metricas = await ctx.db
.query('systemMetrics')
.withIndex('by_timestamp', (q) => q.lte('timestamp', fim))
.order('desc')
.collect();
} else {
metricas = await ctx.db.query('systemMetrics').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, validando tipo número
const rawValue = (metrica as Record<string, unknown>)[alerta.metricName];
if (typeof rawValue !== 'number') continue;
const metricValue = rawValue;
// 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 roles administrativas (nível <= 1) e filtrar usuários por roleId
const rolesAdminOuTi = await ctx.db
.query('roles')
.filter((q) => q.lte(q.field('nivel'), 1))
.collect();
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
const usuarios = await ctx.db.query('usuarios').collect();
const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId));
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()
});
}
}
// Enviar email se configurado (usar template HTML padronizado)
if (alerta.notifyByEmail) {
// Buscar usuários administradores/TI para receber o alerta por email
const rolesAdminOuTi = await ctx.db
.query('roles')
.filter((q) => q.lte(q.field('nivel'), 1))
.collect();
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
const usuarios = await ctx.db.query('usuarios').collect();
const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId) && !!u.email);
for (const usuario of usuariosTI) {
const email = usuario.email;
if (!email) continue;
// Montar variáveis para template de alerta de sistema
const variaveisEmail = {
destinatarioNome: usuario.nome,
metricName: alerta.metricName,
metricValue: metricValue.toFixed(2),
threshold: alerta.threshold.toString()
};
// Importante: usar api.email.enviarEmailComTemplate (action pública),
// e não internal.email, para corresponder à tipagem gerada em ./_generated/api.
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
destinatario: email,
destinatarioId: usuario._id,
templateCodigo: 'monitoramento_alerta_sistema',
variaveis: variaveisEmail,
enviadoPor: usuario._id
});
}
}
}
}
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;
}
});
/**
* Status consolidado do sistema para o dashboard
*/
export const getStatusSistema = query({
args: {},
returns: v.object({
usuariosOnline: v.number(),
totalRegistros: v.number(),
tempoMedioResposta: v.number(),
cpuUsada: v.number(),
memoriaUsada: v.number(),
ultimaAtualizacao: v.number()
}),
handler: async (ctx) => {
try {
// Última métrica, se existir
const ultimaMetrica = (await ctx.db.query('systemMetrics').order('desc').first()) ?? null;
// Usuários online: usar métrica se disponível, senão derivar de usuários
let usuariosOnline = 0;
if (ultimaMetrica?.usuariosOnline !== undefined) {
usuariosOnline = ultimaMetrica.usuariosOnline;
} else {
const usuarios = await ctx.db.query('usuarios').collect();
usuariosOnline = usuarios.filter((u) => u.statusPresenca === 'online').length;
}
// Total de registros (estimativa baseada em tabelas principais)
const [usuarios, funcionarios, simbolos, alertas, metricas] = await Promise.all([
ctx.db.query('usuarios').collect(),
ctx.db.query('funcionarios').collect(),
ctx.db.query('simbolos').collect(),
ctx.db.query('alertConfigurations').collect(),
ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
]);
const totalRegistros =
usuarios.length + funcionarios.length + simbolos.length + alertas.length + metricas.length;
// Métricas de performance com fallbacks seguros
const tempoMedioResposta = ultimaMetrica?.tempoRespostaMedio ?? 0;
const cpuUsada = Math.max(
0,
Math.min(100, Math.round((ultimaMetrica?.cpuUsage ?? 0) * 100) / 100)
);
const memoriaUsada = Math.max(
0,
Math.min(100, Math.round((ultimaMetrica?.memoryUsage ?? 0) * 100) / 100)
);
const ultimaAtualizacao = ultimaMetrica?.timestamp ?? Date.now();
return {
usuariosOnline,
totalRegistros,
tempoMedioResposta,
cpuUsada,
memoriaUsada,
ultimaAtualizacao
};
} catch (error) {
console.error('Erro em getStatusSistema:', error);
// Retornar valores padrão em caso de erro
return {
usuariosOnline: 0,
totalRegistros: 0,
tempoMedioResposta: 0,
cpuUsada: 0,
memoriaUsada: 0,
ultimaAtualizacao: Date.now()
};
}
}
});
/**
* Atividade do banco no último minuto (agregada em buckets)
* Usa logsAtividades e systemMetrics para calcular atividade real.
*/
export const getAtividadeBancoDados = query({
args: {},
returns: v.object({
historico: v.array(
v.object({
entradas: v.number(),
saidas: v.number()
})
)
}),
handler: async (ctx) => {
try {
const agora = Date.now();
const haUmMinuto = agora - 60 * 1000;
// Buscar atividades reais do sistema
const atividadesRecentes = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
.order('asc')
.collect();
// Buscar métricas também (para mensagens se houver)
const metricasRecentes = await ctx.db
.query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
.order('asc')
.collect();
// Bucketizar em 30 pontos (~2s cada) para visualização
const numBuckets = 30;
const bucketSizeMs = Math.ceil(60_000 / numBuckets);
const historico: Array<{ entradas: number; saidas: number }> = [];
for (let i = 0; i < numBuckets; i++) {
const inicio = haUmMinuto + i * bucketSizeMs;
const fim = inicio + bucketSizeMs;
// Contar atividades de criação/inserção (entradas)
const atividadesBucket = atividadesRecentes.filter(
(a) => a.timestamp >= inicio && a.timestamp < fim
);
const entradasAtividades = atividadesBucket.filter(
(a) => a.acao === 'criar' || a.acao === 'inserir' || a.acao === 'cadastrar'
).length;
// Contar atividades de exclusão/remoção (saídas)
const saidasAtividades = atividadesBucket.filter(
(a) => a.acao === 'excluir' || a.acao === 'remover' || a.acao === 'deletar'
).length;
// Usar mensagensPorMinuto como adicional se disponível
const bucketMetricas = metricasRecentes.filter(
(m) => m.timestamp >= inicio && m.timestamp < fim
);
const somaMensagens =
bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0;
// Combinar atividades reais com métricas de mensagens
const entradas = Math.max(0, Math.round(entradasAtividades + somaMensagens * 0.3));
const saidas = Math.max(0, Math.round(saidasAtividades + somaMensagens * 0.2));
historico.push({ entradas, saidas });
}
return { historico };
} catch (error) {
console.error('Erro em getAtividadeBancoDados:', error);
// Retornar histórico vazio em caso de erro
return { historico: Array(30).fill({ entradas: 0, saidas: 0 }) };
}
}
});
/**
* Distribuição de operações (calculada a partir de logsAtividades e métricas)
*/
export const getDistribuicaoRequisicoes = query({
args: {},
returns: v.object({
queries: v.number(),
mutations: v.number(),
leituras: v.number(),
escritas: v.number()
}),
handler: async (ctx) => {
try {
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
// Buscar atividades reais do sistema
const atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
.collect();
// Buscar métricas também
const metricas = await ctx.db
.query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
.order('desc')
.take(100);
// Contar operações de leitura (consultas, visualizações)
const leituras = atividades.filter(
(a) =>
a.acao === 'consultar' ||
a.acao === 'visualizar' ||
a.acao === 'listar' ||
a.acao === 'buscar'
).length;
// Contar operações de escrita (criar, editar, excluir)
const escritas = atividades.filter(
(a) =>
a.acao === 'criar' ||
a.acao === 'editar' ||
a.acao === 'excluir' ||
a.acao === 'inserir' ||
a.acao === 'atualizar' ||
a.acao === 'deletar' ||
a.acao === 'cadastrar' ||
a.acao === 'remover'
).length;
// Adicionar estimativa baseada em mensagens se disponível
const totalMensagens = Math.max(
0,
Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0))
);
// Queries são leituras + parte das mensagens (como consultas de chat)
const queries = leituras + Math.round(totalMensagens * 0.5);
// Mutations são escritas + parte das mensagens (como envio de mensagens)
const mutations = escritas + Math.round(totalMensagens * 0.3);
return { queries, mutations, leituras, escritas };
} catch (error) {
console.error('Erro em getDistribuicaoRequisicoes:', error);
// Retornar valores padrão em caso de erro
return { queries: 0, mutations: 0, leituras: 0, escritas: 0 };
}
}
});