feat(dron): Migración a AtomicDrones con arquitectura DB-First
Implementación del sistema AtomicDrones basado en "Menos es Más": - Refactorización de dron.rb a dispatcher + módulos atómicos - Nuevos módulos: ejecutor, vigilante, sanador, bitacora - DronDB para acceso a datos en PostgreSQL - Tablas: dron_logs, dron_avances, dron_metricas - Views: vw_dron_estado, vw_dron_zombies, vw_dron_metricas_semanal - Health check con detección de zombies (heartbeat > 5min) - Dashboard compacto de la flota - Saneamiento con reintentos (backoff exponencial) y limpieza Comandos soportados: - dron lanzar: Ejecutar comando como dron - dron flota: Dashboard de la flota - dron salud: Health check activo - dron estado <ID>: Estado detallado - dron sanear: Saneamiento completo - dron limpiar: Limpieza de antiguos - dron reintentar: Re-intentar fallidos Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b5e89144e3
commit
3543e62263
@@ -0,0 +1,346 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'bitacora_db'
|
||||
require 'securerandom'
|
||||
|
||||
module ADN
|
||||
module DB
|
||||
# DronDB - Acceso a datos para el sistema de AtomicDrones
|
||||
# Gestiona registro y consulta de drones en tablas internas
|
||||
class DronDB
|
||||
# Estados posibles de un dron
|
||||
ESTADOS = {
|
||||
pending: 'pending',
|
||||
running: 'running',
|
||||
completed: 'completed',
|
||||
failed: 'failed',
|
||||
zombie: 'zombie'
|
||||
}.freeze
|
||||
|
||||
# Tipos de drones
|
||||
TIPOS = {
|
||||
ejecutor: 'ejecutor',
|
||||
vigilante: 'vigilante',
|
||||
sanador: 'sanador',
|
||||
planificador: 'planificador',
|
||||
mensajero: 'mensajero',
|
||||
orquestador: 'orquestador'
|
||||
}.freeze
|
||||
|
||||
# Tiempo máximo sin heartbeat antes de considerar zombie (5 minutos)
|
||||
ZOMBIE_TIMEOUT = 300
|
||||
|
||||
class << self
|
||||
# Generar ID único para un dron
|
||||
# Formato: dron_HHMMSS_PID_RANDOM
|
||||
def generar_dron_id
|
||||
timestamp = Time.now.strftime('%H%M%S')
|
||||
pid = Process.pid
|
||||
random = SecureRandom.hex(3)
|
||||
"dron_#{timestamp}_#{pid}_#{random}"
|
||||
end
|
||||
|
||||
# Registrar inicio de un dron
|
||||
# @param tipo [String] Tipo de dron (ejecutor, vigilante, etc.)
|
||||
# @param cmd [String] Comando a ejecutar
|
||||
# @param flujo_id [String, nil] ID de orquestación (si aplica)
|
||||
# @param metadata [Hash] Metadata adicional
|
||||
# @return [Hash] Datos del dron registrado
|
||||
def registrar_inicio(tipo:, cmd:, flujo_id: nil, metadata: {})
|
||||
sql = <<-SQL
|
||||
INSERT INTO bitacoras.dron_logs (dron_id, tipo, estado, cmd, flujo_id, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *
|
||||
SQL
|
||||
|
||||
dron_id = generar_dron_id
|
||||
params = [dron_id, tipo, ESTADOS[:running], cmd, flujo_id, metadata.to_json]
|
||||
|
||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, params) }
|
||||
|
||||
# Actualizar heartbeat inicial
|
||||
actualizar_heartbeat(dron_id)
|
||||
|
||||
{
|
||||
dron_id: dron_id,
|
||||
estado: ESTADOS[:running],
|
||||
started_at: resultado['started_at'],
|
||||
mensaje: "Dron #{dron_id} registrado como #{tipo}"
|
||||
}
|
||||
end
|
||||
|
||||
# Registrar finalización de un dron
|
||||
# @param dron_id [String] ID del dron
|
||||
# @param exit_code [Integer] Código de salida
|
||||
# @param output [String] Salida del comando
|
||||
# @return [Hash] Resultado de la operación
|
||||
def registrar_fin(dron_id, exit_code, output = nil)
|
||||
estado = exit_code == 0 ? ESTADOS[:completed] : ESTADOS[:failed]
|
||||
|
||||
sql = <<-SQL
|
||||
UPDATE bitacoras.dron_logs
|
||||
SET estado = $1,
|
||||
exit_code = $2,
|
||||
output = $3,
|
||||
completed_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE dron_id = $4
|
||||
RETURNING *
|
||||
SQL
|
||||
|
||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [estado, exit_code, output, dron_id]) }
|
||||
|
||||
# Actualizar métricas diarias
|
||||
actualizar_metricas(dron_id, estado)
|
||||
|
||||
{
|
||||
dron_id: dron_id,
|
||||
estado: estado,
|
||||
exit_code: exit_code,
|
||||
completed_at: resultado['completed_at']
|
||||
}
|
||||
end
|
||||
|
||||
# Actualizar heartbeat de un dron
|
||||
# @param dron_id [String] ID del dron
|
||||
def actualizar_heartbeat(dron_id)
|
||||
sql = <<-SQL
|
||||
UPDATE bitacoras.dron_logs
|
||||
SET heartbeat = NOW(), updated_at = NOW()
|
||||
WHERE dron_id = $1
|
||||
SQL
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }
|
||||
end
|
||||
|
||||
# Obtener información de un dron
|
||||
# @param dron_id [String] ID del dron
|
||||
# @return [Hash, nil] Datos del dron o nil si no existe
|
||||
def obtener_dron(dron_id)
|
||||
sql = 'SELECT * FROM bitacoras.dron_logs WHERE dron_id = $1'
|
||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }
|
||||
resultado.first
|
||||
end
|
||||
|
||||
# Obtener drones por estado
|
||||
# @param estado [String] Estado a filtrar
|
||||
# @return [Array<Hash>] Lista de drones
|
||||
def obtener_por_estado(estado)
|
||||
sql = 'SELECT * FROM bitacoras.dron_logs WHERE estado = $1 ORDER BY started_at DESC'
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [estado]) }
|
||||
end
|
||||
|
||||
# Obtener todos los drones activos (running o pending)
|
||||
# @return [Array<Hash>] Lista de drones activos
|
||||
def obtener_activos
|
||||
sql = <<-SQL
|
||||
SELECT * FROM bitacoras.dron_logs
|
||||
WHERE estado IN ('running', 'pending')
|
||||
ORDER BY started_at DESC
|
||||
SQL
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql).to_a }
|
||||
end
|
||||
|
||||
# Detectar drones zombies (heartbeat > 5min)
|
||||
# @return [Array<Hash>] Lista de drones zombies
|
||||
def detectar_zombies
|
||||
sql = <<-SQL
|
||||
SELECT * FROM bitacoras.vw_dron_zombies
|
||||
ORDER BY heartbeat ASC
|
||||
SQL
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql).to_a }
|
||||
end
|
||||
|
||||
# Marcar dron como zombie
|
||||
# @param dron_id [String] ID del dron
|
||||
def marcar_zombie(dron_id)
|
||||
sql = <<-SQL
|
||||
UPDATE bitacoras.dron_logs
|
||||
SET estado = $1, updated_at = NOW()
|
||||
WHERE dron_id = $2
|
||||
SQL
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [ESTADOS[:zombie], dron_id]) }
|
||||
end
|
||||
|
||||
# Incrementar contador de reintentos
|
||||
# @param dron_id [String] ID del dron
|
||||
# @return [Integer] Nuevo contador de reintentos
|
||||
def incrementar_reintentos(dron_id)
|
||||
sql = <<-SQL
|
||||
UPDATE bitacoras.dron_logs
|
||||
SET reintentos = reintentos + 1, updated_at = NOW()
|
||||
WHERE dron_id = $1
|
||||
RETURNING reintentos
|
||||
SQL
|
||||
|
||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }
|
||||
resultado.first['reintentos'].to_i
|
||||
end
|
||||
|
||||
# Registrar avance de un paso en flujo multi-paso
|
||||
# @param dron_id [String] ID del dron
|
||||
# @param paso [Integer] Número de paso
|
||||
# @param descripcion [String] Descripción del paso
|
||||
# @param estado [String] Estado del paso
|
||||
# @param output [String, nil] Salida del paso
|
||||
# @param flujo_id [String, nil] ID de orquestación
|
||||
def registrar_avance(dron_id, paso:, descripcion:, estado:, output: nil, flujo_id: nil)
|
||||
sql = <<-SQL
|
||||
INSERT INTO bitacoras.dron_avances (dron_id, flujo_id, paso, descripcion, estado, output)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *
|
||||
SQL
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id, flujo_id, paso, descripcion, estado, output]) }
|
||||
end
|
||||
|
||||
# Obtener avances de un dron
|
||||
# @param dron_id [String] ID del dron
|
||||
# @return [Array<Hash>] Lista de avances
|
||||
def obtener_avances(dron_id)
|
||||
sql = <<-SQL
|
||||
SELECT * FROM bitacoras.dron_avances
|
||||
WHERE dron_id = $1
|
||||
ORDER BY paso ASC
|
||||
SQL
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }
|
||||
end
|
||||
|
||||
# Obtener avances de un flujo
|
||||
# @param flujo_id [String] ID del flujo
|
||||
# @return [Array<Hash>] Lista de avances
|
||||
def obtener_avances_flujo(flujo_id)
|
||||
sql = <<-SQL
|
||||
SELECT * FROM bitacoras.dron_avances
|
||||
WHERE flujo_id = $1
|
||||
ORDER BY paso ASC
|
||||
SQL
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [flujo_id]) }
|
||||
end
|
||||
|
||||
# Obtener métricas por fecha y tipo
|
||||
# @param fecha [Date] Fecha de las métricas
|
||||
# @param tipo [String] Tipo de dron
|
||||
# @return [Hash, nil] Métricas o nil si no existen
|
||||
def obtener_metricas(fecha:, tipo:)
|
||||
sql = 'SELECT * FROM bitacoras.dron_metricas WHERE fecha = $1 AND dron_tipo = $2'
|
||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [fecha, tipo]) }
|
||||
resultado.first
|
||||
end
|
||||
|
||||
# Actualizar métricas diarias (llamado automáticamente al finalizar un dron)
|
||||
# @param dron_id [String] ID del dron
|
||||
# @param estado [String] Estado final del dron
|
||||
def actualizar_metricas(dron_id, estado)
|
||||
dron = obtener_dron(dron_id)
|
||||
return unless dron
|
||||
|
||||
fecha = dron['started_at'].to_date
|
||||
tipo = dron['tipo']
|
||||
|
||||
# Calcular duración
|
||||
started = dron['started_at']
|
||||
completed = dron['completed_at'] || Time.now
|
||||
duracion = completed - started
|
||||
|
||||
# Upsert de métricas
|
||||
sql = <<-SQL
|
||||
INSERT INTO bitacoras.dron_metricas (fecha, dron_tipo, total_ejecuciones, completados, fallidos, zombies, tiempo_promedio, reintentos_total)
|
||||
VALUES ($1, $2, 1, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (fecha, dron_tipo) DO UPDATE SET
|
||||
total_ejecuciones = bitacoras.dron_metricas.total_ejecuciones + 1,
|
||||
completados = bitacoras.dron_metricas.completados + $3,
|
||||
fallidos = bitacoras.dron_metricas.fallidos + $4,
|
||||
zombies = bitacoras.dron_metricas.zombies + $5,
|
||||
tiempo_promedio = (
|
||||
(bitacoras.dron_metricas.tiempo_promedio * bitacoras.dron_metricas.total_ejecuciones + $6) /
|
||||
(bitacoras.dron_metricas.total_ejecuciones + 1)
|
||||
),
|
||||
reintentos_total = bitacoras.dron_metricas.reintentos_total + $7,
|
||||
updated_at = NOW()
|
||||
SQL
|
||||
|
||||
es_completado = estado == ESTADOS[:completed] ? 1 : 0
|
||||
es_fallido = estado == ESTADOS[:failed] ? 1 : 0
|
||||
es_zombie = estado == ESTADOS[:zombie] ? 1 : 0
|
||||
reintentos = dron['reintentos'].to_i
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [fecha, tipo, es_completado, es_fallido, es_zombie, duracion, reintentos]) }
|
||||
end
|
||||
|
||||
# Obtener resumen de estado de la flota
|
||||
# @return [Hash] Resumen de la flota
|
||||
def resumen_flota
|
||||
sql = <<-SQL
|
||||
SELECT
|
||||
estado,
|
||||
COUNT(*) as cantidad
|
||||
FROM bitacoras.dron_logs
|
||||
GROUP BY estado
|
||||
SQL
|
||||
|
||||
resultados = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql).to_a }
|
||||
resumen = { total: 0 }
|
||||
|
||||
resultados.each do |fila|
|
||||
resumen[fila['estado'].to_sym] = fila['cantidad'].to_i
|
||||
resumen[:total] += fila['cantidad'].to_i
|
||||
end
|
||||
|
||||
resumen
|
||||
end
|
||||
|
||||
# Obtener drones por flujo
|
||||
# @param flujo_id [String] ID del flujo
|
||||
# @return [Array<Hash>] Lista de drones del flujo
|
||||
def obtener_por_flujo(flujo_id)
|
||||
sql = <<-SQL
|
||||
SELECT * FROM bitacoras.dron_logs
|
||||
WHERE flujo_id = $1
|
||||
ORDER BY started_at ASC
|
||||
SQL
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [flujo_id]) }
|
||||
end
|
||||
|
||||
# Consolidar estado de un flujo
|
||||
# @param flujo_id [String] ID del flujo
|
||||
# @return [String] Estado consolidado (running, completed, failed)
|
||||
def consolidar_estado_flujo(flujo_id)
|
||||
drones = obtener_por_flujo(flujo_id)
|
||||
return nil if drones.empty?
|
||||
|
||||
# Si todos completados → completed
|
||||
# Si alguno failed → failed
|
||||
# Si no → running
|
||||
if drones.all? { |d| d['estado'] == ESTADOS[:completed] }
|
||||
ESTADOS[:completed]
|
||||
elsif drones.any? { |d| d['estado'] == ESTADOS[:failed] }
|
||||
ESTADOS[:failed]
|
||||
else
|
||||
ESTADOS[:running]
|
||||
end
|
||||
end
|
||||
|
||||
# Limpiar drones antiguos (más de N días)
|
||||
# @param dias [Integer] Días de antigüedad
|
||||
# @return [Integer] Cantidad de drones eliminados
|
||||
def limpiar_antiguos(dias: 7)
|
||||
sql = <<-SQL
|
||||
DELETE FROM bitacoras.dron_logs
|
||||
WHERE estado IN ('completed', 'failed', 'zombie')
|
||||
AND completed_at < NOW() - INTERVAL '#{dias} days'
|
||||
SQL
|
||||
|
||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql).to_a }
|
||||
resultado.first['count'].to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,231 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Migration: 003 - Tablas para sistema de Drones (AtomicDrones)
|
||||
#
|
||||
# Usage:
|
||||
# ruby adn/tools/db/migrations/003_create_dron_tables.rb
|
||||
#
|
||||
# Propósito: Crear tablas internas para registro detallado de drones
|
||||
# - dron_logs: Logs detallados de cada dron
|
||||
# - dron_avances: Avances paso-a-paso de flujos multi-paso
|
||||
# - dron_metricas: Métricas agregadas diarias por tipo de dron
|
||||
|
||||
require_relative '../core/bitacora_db'
|
||||
|
||||
puts "🔧 Running migration 003: Create dron tables"
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
# ============================================================================
|
||||
# Tabla 1: dron_logs
|
||||
# ============================================================================
|
||||
|
||||
puts "\n📝 Creando tabla bitacoras.dron_logs..."
|
||||
|
||||
db.execute(<<~SQL)
|
||||
CREATE TABLE IF NOT EXISTS bitacoras.dron_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
dron_id VARCHAR(50) NOT NULL,
|
||||
flujo_id VARCHAR(50),
|
||||
tipo VARCHAR(20) NOT NULL,
|
||||
estado VARCHAR(20) NOT NULL,
|
||||
cmd TEXT,
|
||||
output TEXT,
|
||||
exit_code INTEGER,
|
||||
started_at TIMESTAMP DEFAULT NOW(),
|
||||
completed_at TIMESTAMP,
|
||||
heartbeat TIMESTAMP DEFAULT NOW(),
|
||||
reintentos INTEGER DEFAULT 0,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
SQL
|
||||
puts "✅ Tabla bitacoras.dron_logs creada"
|
||||
|
||||
# Índices para dron_logs
|
||||
indices_dron_logs = [
|
||||
["idx_dron_logs_estado", "CREATE INDEX IF NOT EXISTS idx_dron_logs_estado ON bitacoras.dron_logs(estado)"],
|
||||
["idx_dron_logs_heartbeat", "CREATE INDEX IF NOT EXISTS idx_dron_logs_heartbeat ON bitacoras.dron_logs(heartbeat)"],
|
||||
["idx_dron_logs_flujo", "CREATE INDEX IF NOT EXISTS idx_dron_logs_flujo ON bitacoras.dron_logs(flujo_id)"],
|
||||
["idx_dron_logs_tipo", "CREATE INDEX IF NOT EXISTS idx_dron_logs_tipo ON bitacoras.dron_logs(tipo)"],
|
||||
["idx_dron_logs_started_at", "CREATE INDEX IF NOT EXISTS idx_dron_logs_started_at ON bitacoras.dron_logs(started_at)"]
|
||||
]
|
||||
|
||||
indices_dron_logs.each do |nombre, sql|
|
||||
db.execute(sql)
|
||||
puts "📈 Índice creado: #{nombre}"
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Tabla 2: dron_avances
|
||||
# ============================================================================
|
||||
|
||||
puts "\n📝 Creando tabla bitacoras.dron_avances..."
|
||||
|
||||
db.execute(<<~SQL)
|
||||
CREATE TABLE IF NOT EXISTS bitacoras.dron_avances (
|
||||
id SERIAL PRIMARY KEY,
|
||||
dron_id VARCHAR(50) NOT NULL,
|
||||
flujo_id VARCHAR(50),
|
||||
paso INTEGER NOT NULL,
|
||||
descripcion TEXT NOT NULL,
|
||||
estado VARCHAR(20) NOT NULL,
|
||||
output TEXT,
|
||||
started_at TIMESTAMP DEFAULT NOW(),
|
||||
completed_at TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
SQL
|
||||
puts "✅ Tabla bitacoras.dron_avances creada"
|
||||
|
||||
# Índices para dron_avances
|
||||
indices_dron_avances = [
|
||||
["idx_dron_avances_dron", "CREATE INDEX IF NOT EXISTS idx_dron_avances_dron ON bitacoras.dron_avances(dron_id)"],
|
||||
["idx_dron_avances_flujo", "CREATE INDEX IF NOT EXISTS idx_dron_avances_flujo ON bitacoras.dron_avances(flujo_id)"],
|
||||
["idx_dron_avances_paso", "CREATE INDEX IF NOT EXISTS idx_dron_avances_paso ON bitacoras.dron_avances(paso)"]
|
||||
]
|
||||
|
||||
indices_dron_avances.each do |nombre, sql|
|
||||
db.execute(sql)
|
||||
puts "📈 Índice creado: #{nombre}"
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Tabla 3: dron_metricas
|
||||
# ============================================================================
|
||||
|
||||
puts "\n📝 Creando tabla bitacoras.dron_metricas..."
|
||||
|
||||
db.execute(<<~SQL)
|
||||
CREATE TABLE IF NOT EXISTS bitacoras.dron_metricas (
|
||||
id SERIAL PRIMARY KEY,
|
||||
fecha DATE NOT NULL,
|
||||
dron_tipo VARCHAR(20) NOT NULL,
|
||||
total_ejecuciones INTEGER DEFAULT 0,
|
||||
completados INTEGER DEFAULT 0,
|
||||
fallidos INTEGER DEFAULT 0,
|
||||
zombies INTEGER DEFAULT 0,
|
||||
tiempo_promedio INTERVAL DEFAULT '0 seconds',
|
||||
reintentos_total INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(fecha, dron_tipo)
|
||||
)
|
||||
SQL
|
||||
puts "✅ Tabla bitacoras.dron_metricas creada"
|
||||
|
||||
# Índices para dron_metricas
|
||||
db.execute("CREATE INDEX IF NOT EXISTS idx_dron_metricas_fecha ON bitacoras.dron_metricas(fecha)")
|
||||
puts "📈 Índice creado: idx_dron_metricas_fecha"
|
||||
|
||||
# ============================================================================
|
||||
# Trigger para actualizar updated_at
|
||||
# ============================================================================
|
||||
|
||||
puts "\n📝 Creando trigger para actualizar updated_at..."
|
||||
|
||||
db.execute(<<~SQL)
|
||||
CREATE OR REPLACE FUNCTION bitacoras.update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
SQL
|
||||
puts "✅ Función update_updated_at_column creada"
|
||||
|
||||
# Triggers para cada tabla
|
||||
triggers = [
|
||||
["update_dron_logs_updated_at", "dron_logs"],
|
||||
["update_dron_metricas_updated_at", "dron_metricas"]
|
||||
]
|
||||
|
||||
triggers.each do |nombre_trigger, tabla|
|
||||
db.execute("DROP TRIGGER IF EXISTS #{nombre_trigger} ON bitacoras.#{tabla}")
|
||||
db.execute(<<~SQL)
|
||||
CREATE TRIGGER #{nombre_trigger}
|
||||
BEFORE UPDATE ON bitacoras.#{tabla}
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION bitacoras.update_updated_at_column()
|
||||
SQL
|
||||
puts "📈 Trigger creado: #{nombre_trigger} en #{tabla}"
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Vistas útiles
|
||||
# ============================================================================
|
||||
|
||||
puts "\n📝 Creando vistas útiles..."
|
||||
|
||||
# Vista: Drones activos por estado
|
||||
db.execute(<<~SQL)
|
||||
CREATE OR REPLACE VIEW bitacoras.vw_dron_estado AS
|
||||
SELECT
|
||||
estado,
|
||||
tipo,
|
||||
COUNT(*) as cantidad,
|
||||
MIN(started_at) as mas_antiguo,
|
||||
MAX(started_at) as mas_reciente
|
||||
FROM bitacoras.dron_logs
|
||||
WHERE estado IN ('running', 'pending')
|
||||
GROUP BY estado, tipo
|
||||
SQL
|
||||
puts "✅ Vista creada: vw_dron_estado"
|
||||
|
||||
# Vista: Drones zombies
|
||||
db.execute(<<~SQL)
|
||||
CREATE OR REPLACE VIEW bitacoras.vw_dron_zombies AS
|
||||
SELECT
|
||||
id,
|
||||
dron_id,
|
||||
tipo,
|
||||
cmd,
|
||||
started_at,
|
||||
heartbeat,
|
||||
EXTRACT(EPOCH FROM (NOW() - heartbeat))/60 as minutos_sin_heartbeat
|
||||
FROM bitacoras.dron_logs
|
||||
WHERE estado = 'running'
|
||||
AND heartbeat < NOW() - INTERVAL '5 minutes'
|
||||
SQL
|
||||
puts "✅ Vista creada: vw_dron_zombies"
|
||||
|
||||
# Vista: Métricas semanales
|
||||
db.execute(<<~SQL)
|
||||
CREATE OR REPLACE VIEW bitacoras.vw_dron_metricas_semanal AS
|
||||
SELECT
|
||||
fecha,
|
||||
dron_tipo,
|
||||
total_ejecuciones,
|
||||
completados,
|
||||
fallidos,
|
||||
zombies,
|
||||
tiempo_promedio,
|
||||
reintentos_total,
|
||||
ROUND(100.0 * completados / NULLIF(total_ejecuciones, 0), 2) as tasa_exito
|
||||
FROM bitacoras.dron_metricas
|
||||
WHERE fecha >= CURRENT_DATE - INTERVAL '7 days'
|
||||
ORDER BY fecha DESC, dron_tipo
|
||||
SQL
|
||||
puts "✅ Vista creada: vw_dron_metricas_semanal"
|
||||
|
||||
# ============================================================================
|
||||
# Comentarios de documentación
|
||||
# ============================================================================
|
||||
|
||||
puts "\n📝 Agregando comentarios de documentación..."
|
||||
|
||||
db.execute("COMMENT ON TABLE bitacoras.dron_logs IS 'Logs detallados de cada dron en la colonia (NO visible directamente en Bitácora Web)'")
|
||||
db.execute("COMMENT ON TABLE bitacoras.dron_avances IS 'Avances paso-a-paso de flujos multi-paso (orquestación)'")
|
||||
db.execute("COMMENT ON TABLE bitacoras.dron_metricas IS 'Métricas agregadas diarias por tipo de dron (para dashboard y estadísticas)'")
|
||||
puts "✅ Comentarios agregados"
|
||||
end
|
||||
|
||||
puts "\n🎉 Migration 003 completed successfully!"
|
||||
puts "\nResumen:"
|
||||
puts " - bitacoras.dron_logs: Logs detallados"
|
||||
puts " - bitacoras.dron_avances: Avances paso-a-paso"
|
||||
puts " - bitacoras.dron_metricas: Métricas agregadas"
|
||||
puts " - Vistas: vw_dron_estado, vw_dron_zombies, vw_dron_metricas_semanal"
|
||||
puts "\n🏁 Migration finished."
|
||||
@@ -0,0 +1,177 @@
|
||||
-- Migración 003: Tablas para sistema de Drones (AtomicDrones)
|
||||
-- Fecha: 2026-04-07
|
||||
-- Propósito: Crear tablas internas para registro detallado de drones
|
||||
|
||||
-- ============================================================================
|
||||
-- Tabla 1: dron_logs - Logs detallados de cada dron
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bitacoras.dron_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
dron_id VARCHAR(50) NOT NULL, -- ej: dron_083045_123
|
||||
flujo_id VARCHAR(50), -- ID de orquestación (si aplica)
|
||||
tipo VARCHAR(20) NOT NULL, -- ejecutor|vigilante|sanador|planificador|mensajero|orquestador
|
||||
estado VARCHAR(20) NOT NULL, -- pending|running|completed|failed|zombie
|
||||
cmd TEXT, -- Comando ejecutado
|
||||
output TEXT, -- Salida del comando
|
||||
exit_code INTEGER, -- Código de retorno
|
||||
started_at TIMESTAMP DEFAULT NOW(),
|
||||
completed_at TIMESTAMP,
|
||||
heartbeat TIMESTAMP DEFAULT NOW(), -- Último heartbeat
|
||||
reintentos INTEGER DEFAULT 0,
|
||||
metadata JSONB DEFAULT '{}'::jsonb, -- Metadata adicional
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índices para consultas eficientes
|
||||
CREATE INDEX IF NOT EXISTS idx_dron_logs_estado ON bitacoras.dron_logs(estado);
|
||||
CREATE INDEX IF NOT EXISTS idx_dron_logs_heartbeat ON bitacoras.dron_logs(heartbeat);
|
||||
CREATE INDEX IF NOT EXISTS idx_dron_logs_flujo ON bitacoras.dron_logs(flujo_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_dron_logs_tipo ON bitacoras.dron_logs(tipo);
|
||||
CREATE INDEX IF NOT EXISTS idx_dron_logs_started_at ON bitacoras.dron_logs(started_at);
|
||||
|
||||
-- ============================================================================
|
||||
-- Tabla 2: dron_avances - Avances paso-a-paso (para flujos multi-paso)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bitacoras.dron_avances (
|
||||
id SERIAL PRIMARY KEY,
|
||||
dron_id VARCHAR(50) NOT NULL,
|
||||
flujo_id VARCHAR(50), -- ID de orquestación
|
||||
paso INTEGER NOT NULL, -- Número de paso en flujo
|
||||
descripcion TEXT NOT NULL, -- Qué se está haciendo
|
||||
estado VARCHAR(20) NOT NULL, -- pending|running|completed|failed
|
||||
output TEXT, -- Salida del paso
|
||||
started_at TIMESTAMP DEFAULT NOW(),
|
||||
completed_at TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índices para consultas eficientes
|
||||
CREATE INDEX IF NOT EXISTS idx_dron_avances_dron ON bitacoras.dron_avances(dron_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_dron_avances_flujo ON bitacoras.dron_avances(flujo_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_dron_avances_paso ON bitacoras.dron_avances(paso);
|
||||
|
||||
-- ============================================================================
|
||||
-- Tabla 3: dron_metricas - Métricas de rendimiento (agregaciones diarias)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bitacoras.dron_metricas (
|
||||
id SERIAL PRIMARY KEY,
|
||||
fecha DATE NOT NULL,
|
||||
dron_tipo VARCHAR(20) NOT NULL,
|
||||
total_ejecuciones INTEGER DEFAULT 0,
|
||||
completados INTEGER DEFAULT 0,
|
||||
fallidos INTEGER DEFAULT 0,
|
||||
zombies INTEGER DEFAULT 0,
|
||||
tiempo_promedio INTERVAL DEFAULT '0 seconds',
|
||||
reintentos_total INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(fecha, dron_tipo)
|
||||
);
|
||||
|
||||
-- Índice para consultas por fecha
|
||||
CREATE INDEX IF NOT EXISTS idx_dron_metricas_fecha ON bitacoras.dron_metricas(fecha);
|
||||
|
||||
-- ============================================================================
|
||||
-- Trigger: Actualizar updated_at automáticamente
|
||||
-- ============================================================================
|
||||
|
||||
-- Función para actualizar updated_at
|
||||
CREATE OR REPLACE FUNCTION bitacoras.update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger para dron_logs
|
||||
DROP TRIGGER IF EXISTS update_dron_logs_updated_at ON bitacoras.dron_logs;
|
||||
CREATE TRIGGER update_dron_logs_updated_at
|
||||
BEFORE UPDATE ON bitacoras.dron_logs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION bitacoras.update_updated_at_column();
|
||||
|
||||
-- Trigger para dron_metricas
|
||||
DROP TRIGGER IF EXISTS update_dron_metricas_updated_at ON bitacoras.dron_metricas;
|
||||
CREATE TRIGGER update_dron_metricas_updated_at
|
||||
BEFORE UPDATE ON bitacoras.dron_metricas
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION bitacoras.update_updated_at_column();
|
||||
|
||||
-- ============================================================================
|
||||
-- Comentario de documentación
|
||||
-- ============================================================================
|
||||
|
||||
COMMENT ON TABLE bitacoras.dron_logs IS 'Logs detallados de cada dron en la colonia (NO visible directamente en Bitácora Web)';
|
||||
COMMENT ON TABLE bitacoras.dron_avances IS 'Avances paso-a-paso de flujos multi-paso (orquestación)';
|
||||
COMMENT ON TABLE bitacoras.dron_metricas IS 'Métricas agregadas diarias por tipo de dron (para dashboard y estadísticas)';
|
||||
|
||||
COMMENT ON COLUMN bitacoras.dron_logs.dron_id IS 'Identificador único del dron (ej: dron_083045_123)';
|
||||
COMMENT ON COLUMN bitacoras.dron_logs.flujo_id IS 'ID de orquestación para agrupar drones de un mismo flujo';
|
||||
COMMENT ON COLUMN bitacoras.dron_logs.tipo IS 'Tipo de dron: ejecutor, vigilante, sanador, planificador, mensajero, orquestador';
|
||||
COMMENT ON COLUMN bitacoras.dron_logs.estado IS 'Estado actual: pending, running, completed, failed, zombie';
|
||||
COMMENT ON COLUMN bitacoras.dron_logs.heartbeat IS 'Último heartbeat - si > 5min, dron se considera zombie';
|
||||
|
||||
-- ============================================================================
|
||||
-- Vistas útiles para consultas frecuentes
|
||||
-- ============================================================================
|
||||
|
||||
-- Vista: Drones activos por estado
|
||||
CREATE OR REPLACE VIEW bitacoras.vw_dron_estado AS
|
||||
SELECT
|
||||
estado,
|
||||
tipo,
|
||||
COUNT(*) as cantidad,
|
||||
MIN(started_at) as mas_antiguo,
|
||||
MAX(started_at) as mas_reciente
|
||||
FROM bitacoras.dron_logs
|
||||
WHERE estado IN ('running', 'pending')
|
||||
GROUP BY estado, tipo;
|
||||
|
||||
-- Vista: Drones zombies (heartbeat > 5min)
|
||||
CREATE OR REPLACE VIEW bitacoras.vw_dron_zombies AS
|
||||
SELECT
|
||||
id,
|
||||
dron_id,
|
||||
tipo,
|
||||
cmd,
|
||||
started_at,
|
||||
heartbeat,
|
||||
EXTRACT(EPOCH FROM (NOW() - heartbeat))/60 as minutos_sin_heartbeat
|
||||
FROM bitacoras.dron_logs
|
||||
WHERE estado = 'running'
|
||||
AND heartbeat < NOW() - INTERVAL '5 minutes';
|
||||
|
||||
-- Vista: Métricas de los últimos 7 días
|
||||
CREATE OR REPLACE VIEW bitacoras.vw_dron_metricas_semanal AS
|
||||
SELECT
|
||||
fecha,
|
||||
dron_tipo,
|
||||
total_ejecuciones,
|
||||
completados,
|
||||
fallidos,
|
||||
zombies,
|
||||
tiempo_promedio,
|
||||
reintentos_total,
|
||||
ROUND(100.0 * completados / NULLIF(total_ejecuciones, 0), 2) as tasa_exito
|
||||
FROM bitacoras.dron_metricas
|
||||
WHERE fecha >= CURRENT_DATE - INTERVAL '7 days'
|
||||
ORDER BY fecha DESC, dron_tipo;
|
||||
|
||||
-- ============================================================================
|
||||
-- Mensaje de confirmación
|
||||
-- ============================================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '✅ Migración 003 completada: Tablas de Drones creadas exitosamente';
|
||||
RAISE NOTICE ' - bitacoras.dron_logs: Logs detallados';
|
||||
RAISE NOTICE ' - bitacoras.dron_avances: Avances paso-a-paso';
|
||||
RAISE NOTICE ' - bitacoras.dron_metricas: Métricas agregadas';
|
||||
RAISE NOTICE ' - Vistas: vw_dron_estado, vw_dron_zombies, vw_dron_metricas_semanal';
|
||||
END $$;
|
||||
Reference in New Issue
Block a user