737 lines
19 KiB
TypeScript
737 lines
19 KiB
TypeScript
import { v } from 'convex/values';
|
|
import { mutation, query, internalMutation } from './_generated/server';
|
|
import { internal } 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: {},
|
|
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) => {
|
|
// 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()
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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) => {
|
|
// Ú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, solicitacoesAcesso, alertas, metricas] =
|
|
await Promise.all([
|
|
ctx.db.query('usuarios').collect(),
|
|
ctx.db.query('funcionarios').collect(),
|
|
ctx.db.query('simbolos').collect(),
|
|
ctx.db.query('solicitacoesAcesso').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 +
|
|
solicitacoesAcesso.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
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Atividade do banco no último minuto (agregada em buckets)
|
|
* Usa mensagensPorMinuto como proxy de atividade quando disponível.
|
|
*/
|
|
export const getAtividadeBancoDados = query({
|
|
args: {},
|
|
returns: v.object({
|
|
historico: v.array(
|
|
v.object({
|
|
entradas: v.number(),
|
|
saidas: v.number()
|
|
})
|
|
)
|
|
}),
|
|
handler: async (ctx) => {
|
|
const agora = Date.now();
|
|
const haUmMinuto = agora - 60 * 1000;
|
|
|
|
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;
|
|
const bucketMetricas = metricasRecentes.filter(
|
|
(m) => m.timestamp >= inicio && m.timestamp < fim
|
|
);
|
|
|
|
// Usar mensagensPorMinuto como proxy de "entradas"; "saídas" como fração
|
|
const somaMensagens =
|
|
bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0;
|
|
const entradas = Math.max(0, Math.round(somaMensagens));
|
|
const saidas = Math.max(0, Math.round(entradas * 0.6));
|
|
|
|
historico.push({ entradas, saidas });
|
|
}
|
|
|
|
return { historico };
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Distribuição de operações (estimada a partir das 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) => {
|
|
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);
|
|
|
|
const totalOps = Math.max(
|
|
0,
|
|
Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0))
|
|
);
|
|
|
|
const queries = Math.round(totalOps * 0.7);
|
|
const mutations = Math.max(0, totalOps - queries);
|
|
const leituras = queries;
|
|
const escritas = mutations;
|
|
|
|
return { queries, mutations, leituras, escritas };
|
|
}
|
|
});
|