🎸 Elvis — Framework para Microservicios en Go

🚀 Librería de infraestructura para construir microservicios escalables en Go
📑 Tabla de Contenidos
📖 Descripción
Elvis es una librería Go (github.com/celsiainternet/elvis) que provee primitivas de infraestructura para construir microservicios. No es una aplicación en sí misma, sino una librería compartida que otros servicios consumen.
Incluye:
- 🗄️ Abstracción de base de datos multi-driver (PostgreSQL, MySQL, Oracle)
- 🔍 Query builder estilo LINQ con soporte a JSONB
- 💾 Cache multi-backend (Redis + memoria)
- 🔄 Sistema de eventos local (in-process) y distribuido (NATS)
- 🔐 Autenticación JWT con invalidación en Redis
- 🛡️ Middleware HTTP integrado (auth, CORS, logging, telemetría)
- 🔁 Sistema de resiliencia con reintentos automáticos
- 📋 Workflows con pasos, rollback y expresiones condicionales
- 📅 Tareas programadas (Crontab)
- 🛠️ CLI de scaffolding para generar nuevos proyectos microservicio
📋 Requisitos Previos
- Go 1.23 o superior
- PostgreSQL — base de datos principal
- Redis — cache y almacenamiento de tokens JWT
- NATS — mensajería distribuida (eventos y RPC entre servicios)
🚀 Instalación
# En el módulo de tu proyecto
go get github.com/celsiainternet/elvis@latest
go get github.com/celsiainternet/elvis@v1.1.222
go run github.com/celsiainternet/elvis/cmd/install
Para generar un nuevo microservicio con la estructura base de Elvis:
go run github.com/celsiainternet/elvis/cmd/create go
⚡ Quick Start
package main
import (
"net/http"
"github.com/celsiainternet/elvis/cache"
"github.com/celsiainternet/elvis/envar"
"github.com/celsiainternet/elvis/et"
"github.com/celsiainternet/elvis/jdb"
"github.com/celsiainternet/elvis/logs"
"github.com/celsiainternet/elvis/middleware"
"github.com/celsiainternet/elvis/response"
"github.com/celsiainternet/elvis/router"
"github.com/go-chi/chi/v5"
)
func main() {
// Conectar a la base de datos
db, err := jdb.Load()
if err != nil {
logs.Alert(err)
return
}
defer db.Close()
// Conectar a Redis
_, err = cache.Load()
if err != nil {
logs.Alert(err)
return
}
defer cache.Close()
// Crear router chi
r := chi.NewRouter()
r.Use(middleware.Cors)
r.Use(middleware.Logger)
host := envar.GetStr("localhost", "HOST")
packagePath := "/api/v1"
// Ruta pública
router.PublicRoute(r, "GET", "/health", func(w http.ResponseWriter, req *http.Request) {
response.JSON(w, req, http.StatusOK, et.Json{"status": "ok"})
}, "mi-servicio", packagePath, host)
// Ruta protegida (requiere JWT)
router.ProtectRoute(r, "GET", "/me", func(w http.ResponseWriter, req *http.Request) {
response.JSON(w, req, http.StatusOK, et.Json{"mensaje": "autenticado"})
}, "mi-servicio", packagePath, host)
addr := envar.GetStr(":3400", "ADDR")
logs.Logf("HTTP", "Escuchando en %s", addr)
http.ListenAndServe(addr, r)
}
🧱 Tipos de Datos Centrales (et)
El paquete et es la base de toda la librería. Define los tipos de dato comunes usados en toda la API.
et.Json
map[string]interface{} con métodos de acceso tipados:
data := et.Json{
"nombre": "Elvis",
"edad": 42,
"activo": true,
}
nombre := data.Str("nombre") // "Elvis"
edad := data.Int("edad") // 42
activo := data.Bool("activo") // true
// Escribir valores
data.Set("pais", "Colombia")
// Serializar a string JSON
str := data.ToString()
et.Item
Resultado unitario de una consulta:
// Uso típico
item, err := modelo.QueryOne("SELECT * FROM tabla WHERE _id=$1", id)
if item.Ok {
nombre := item.Result.Str("nombre")
}
et.Items
Resultado paginado de una consulta:
// Acceder al primer elemento
first := items.First()
// Iterar
for _, item := range items.Result {
fmt.Println(item.Str("nombre"))
}
et.List
Resultado con metadatos completos de paginación: Rows, All, Count, Page, Start, End, Result.
🗄️ Base de Datos (jdb)
Abstracción multi-driver que soporta PostgreSQL, MySQL y Oracle.
Conexión
import "github.com/celsiainternet/elvis/jdb"
// Carga usando variables de entorno
db, err := jdb.Load()
// Conexión a una base de datos específica
db, err := jdb.LoadTo("nombre_base_datos")
// Conexión manual con parámetros
db, err := jdb.ConnectTo(et.Json{
"driver": "postgres",
"host": "localhost",
"port": 5432,
"dbname": "mi_db",
"user": "postgres",
"password": "secreto",
"application_name": "mi-servicio",
})
Consultas directas
// Consulta múltiples filas
items, err := db.Query("SELECT * FROM usuarios WHERE activo = $1", true)
// Consulta una fila
item, err := db.QueryOne("SELECT * FROM usuarios WHERE _id = $1", id)
// Ejecutar DDL
err := db.Ddl("CREATE TABLE IF NOT EXISTS ejemplo (_id VARCHAR(80) PRIMARY KEY)")
// Ejecutar comando DML
items, err := db.Command("INSERT INTO tabla (_id, nombre) VALUES ($1, $2) RETURNING *", id, nombre)
Core Tables
Cuando USE_CORE=true (default), jdb.Load() inicializa tres tablas internas:
- series — secuencias auto-incrementales por tag
- records — auditoría de cambios
- recycling — papelera (soft deletes)
// Siguiente valor de serie numérica
siguiente := jdb.NextSerie(db, "mi.tabla")
// Siguiente código con prefijo
codigo := jdb.NextCode(db, "mi.tabla", "USR")
// Resultado: "USR000001"
🔍 ORM / Query Builder (linq)
Basado en LINQ, permite definir modelos tipados y construir consultas de forma fluida.
Definir Schema y Modelo
import (
"github.com/celsiainternet/elvis/jdb"
"github.com/celsiainternet/elvis/linq"
)
// Schema = esquema PostgreSQL
schema := linq.NewSchema(db, "public")
// Modelo = tabla dentro del schema
modelo := linq.NewModel(schema, "USUARIOS", "Tabla de usuarios", 1)
// Columnas reales
modelo.DefineColum(jdb.KEY, "", "VARCHAR(80)", "-1") // llave primaria (_id)
modelo.DefineColum("NOMBRE", "", "VARCHAR(250)", "")
modelo.DefineColum("EMAIL", "", "VARCHAR(250)", "")
modelo.DefineColum("DATE_MAKE", "", "TIMESTAMP", "NOW()") // activa UseDateMake
modelo.DefineColum("DATE_UPDATE", "", "TIMESTAMP", "NOW()") // activa UseDateUpdate
modelo.DefineColum("_STATE", "", "VARCHAR(20)", "0") // activa UseState
modelo.DefineColum("_DATA", "", "JSONB", "{}") // activa UseSource (modo JSONB)
// Atributos JSONB (sub-campos dentro de _DATA)
modelo.DefineAtrib("telefono", "Teléfono", "VARCHAR(20)", "")
modelo.DefineAtrib("ciudad", "Ciudad", "VARCHAR(100)", "")
// Llave primaria
modelo.DefinePrimaryKey([]string{jdb.KEY})
// Índices
modelo.DefineIndex([]string{"EMAIL"})
// Campos requeridos (campo:mensaje_error)
modelo.DefineRequired([]string{"NOMBRE:El nombre es requerido", "EMAIL"})
// Crear tabla en base de datos (CREATE TABLE IF NOT EXISTS)
err := modelo.Init()
Triggers
modelo.Trigger(linq.BeforeInsert, func(m *linq.Model, old, new *et.Json, data et.Json) error {
new.Set("NOMBRE", strings.ToUpper(new.Str("NOMBRE")))
return nil
})
modelo.Trigger(linq.AfterInsert, func(m *linq.Model, old, new *et.Json, data et.Json) error {
event.Emit("usuario.creado", *new)
return nil
})
// Constantes: linq.BeforeInsert, linq.AfterInsert,
// linq.BeforeUpdate, linq.AfterUpdate,
// linq.BeforeDelete, linq.AfterDelete
Consultas CRUD
// INSERT
item, err := modelo.Insert(et.Json{
jdb.KEY: utility.UUID(),
"NOMBRE": "Juan Pérez",
"EMAIL": "juan@ejemplo.com",
}).One()
// UPDATE
item, err := modelo.Update(et.Json{"NOMBRE": "Juan Pablo"}).
Where(modelo.Col(jdb.KEY).Eq(id)).
One()
// UPSERT
item, err := modelo.Upsert(et.Json{
jdb.KEY: id,
"NOMBRE": "Juan",
}).One()
// DELETE
item, err := modelo.Delete().
Where(modelo.Col(jdb.KEY).Eq(id)).
One()
// SELECT múltiple
items, err := modelo.Select().
Where(modelo.Col("_STATE").Eq("0")).
OrderBy(modelo.Col("NOMBRE"), true).
All()
// SELECT paginado
items, err := modelo.Select().
Where(modelo.Col("_STATE").Eq("0")).
Page(1, 20).
List()
// SELECT uno
item, err := modelo.Select().
Where(modelo.Col(jdb.KEY).Eq(id)).
One()
Condiciones disponibles
col.Eq(val) // =
col.Neg(val) // !=
col.In(vals...) // IN (...)
col.Like(val) // ILIKE
col.More(val) // >
col.Less(val) // <
col.MoreEq(val) // >=
col.LessEq(val) // <=
col.Search(val) // @@ (búsqueda full-text)
Referencias entre modelos
// Llave foránea
modeloOrden.DefineForeignKey("USUARIO_ID", modeloUsuario.Col(jdb.KEY))
// Referencia (columna virtual que trae el nombre del otro modelo)
modeloOrden.DefineReference("USUARIO_ID", "USUARIO", jdb.KEY, modeloUsuario.Col("NOMBRE"), false)
💾 Cache (cache / mem)
Redis (cache)
import (
"time"
"github.com/celsiainternet/elvis/cache"
)
// Conectar (usa REDIS_HOST, REDIS_PASSWORD, REDIS_DB)
_, err := cache.Load()
defer cache.Close()
// Operaciones básicas
err = cache.Set("clave", "valor", 30*time.Minute)
valor, err := cache.Get("clave", "default")
_, err = cache.Delete("clave")
// Verificar conexión
ok := cache.HealthCheck()
Memoria (mem)
Cache in-process con TTL, inicializado automáticamente:
import (
"time"
"github.com/celsiainternet/elvis/mem"
)
mem.Set("clave", "valor", 5*time.Minute)
valor, err := mem.Get("clave", "default")
mem.Del("clave")
mem.Clear("prefijo") // elimina todas las claves que contienen "prefijo"
🔄 Eventos (event)
El sistema de eventos tiene dos modos: local (in-process) y distribuido (NATS).
Eventos locales (in-process)
import "github.com/celsiainternet/elvis/event"
// Registrar handler
event.On("usuario.creado", func(msg event.EvenMessage) {
fmt.Println("Usuario:", msg.Data.Str("nombre"))
})
// Emitir evento
event.Emit("usuario.creado", et.Json{"nombre": "Juan"})
Eventos distribuidos (NATS)
// Conectar a NATS (usa NATS_HOST, NATS_USER, NATS_PASSWORD)
conn, err := event.Load()
defer event.Close()
// Suscribirse
err = event.Subscribe("pedido.nuevo", func(msg event.EvenMessage) {
fmt.Println("Pedido:", msg.Data)
})
// Publicar
event.Publish("pedido.nuevo", et.Json{"pedido_id": "123"})
// Stack: handler persistente que se re-registra tras reconexión
event.Stack("canal/reset", func(msg event.EvenMessage) {
// re-sincronización
})
🔐 Autenticación y Autorización (claim / middleware)
Generar tokens JWT
import (
"time"
"github.com/celsiainternet/elvis/claim"
)
// Token básico (almacenado en Redis para soporte de logout)
token, err := claim.NewToken(userId, "mi-app", "Juan", "juan@e.com", "web", 24*time.Hour)
// Token con autorización (incluye projectId y profileTp)
token, err := claim.NewAuthorization(userId, "mi-app", "Juan", "juan@e.com", "web", projectId, profileTp, 8*time.Hour)
// Token efímero (vida corta, requiere tag descriptivo)
token, err := claim.NewEphemeralToken(userId, "mi-app", "Juan", "juan@e.com", "web", "descarga-reporte", 15*time.Minute)
La variable de entorno SECRET (default "1977") es la clave de firma JWT.
Validar y parsear tokens
// Parsear sin validar en Redis
c, err := claim.ParceToken(token)
// Validar (parsea + verifica que el token esté activo en Redis)
c, err := claim.ValidToken(token)
// Logout
err = claim.DeleteToken(app, device, userId)
err = claim.DeleteTokeByToken(token)
Leer datos del cliente desde el request
import "github.com/celsiainternet/elvis/claim"
func handler(w http.ResponseWriter, r *http.Request) {
clientId := claim.ClientId(r)
nombre := claim.ClientName(r)
username := claim.Username(r)
projectId := claim.ProjectId(r)
profileTp := claim.ProfileTp(r)
device := claim.Device(r)
tag := claim.Tag(r)
// O todo en un et.Json:
cliente := claim.GetClient(r)
}
Middleware HTTP
import (
"github.com/celsiainternet/elvis/middleware"
"github.com/go-chi/chi/v5"
)
r := chi.NewRouter()
r.Use(middleware.Cors)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestId)
r.Use(middleware.Telemetry)
// Ruta con autenticación
r.With(middleware.Autentication).Get("/perfil", handler)
// Ruta con autenticación + verificación de permisos
r.With(middleware.Autentication).With(middleware.Authorization).Get("/admin", handler)
// Ruta con token efímero
r.With(middleware.Ephemeral).Get("/descarga", handler)
🌐 HTTP Router y Respuestas (router / response)
Registro de rutas
Todas las rutas registradas se publican automáticamente al API Gateway vía NATS.
import (
"github.com/celsiainternet/elvis/router"
"github.com/go-chi/chi/v5"
)
r := chi.NewRouter()
packageName := "usuarios-service"
packagePath := "/api/v1/usuarios"
host := "http://localhost:3400"
// Pública
router.PublicRoute(r, "GET", "/", listar, packageName, packagePath, host)
router.PublicRoute(r, "POST", "/", crear, packageName, packagePath, host)
// Protegida (requiere JWT válido)
router.ProtectRoute(r, "GET", "/{id}", obtener, packageName, packagePath, host)
router.ProtectRoute(r, "PUT", "/{id}", actualizar, packageName, packagePath, host)
router.ProtectRoute(r, "DELETE", "/{id}", eliminar, packageName, packagePath, host)
// Con autenticación + verificación de permisos
router.AuthorizationRoute(r, "GET", "/admin", adminHandler, packageName, packagePath, host)
// Con token efímero
router.EphemeralRoute(r, "GET", "/descarga/{id}", descargar, packageName, packagePath, host)
// Con middlewares personalizados
router.With(r, "POST", "/upload", []func(http.Handler)http.Handler{miMiddleware}, upload, packageName, packagePath, host)
Helpers de respuesta
import (
"net/http"
"github.com/celsiainternet/elvis/response"
)
func handler(w http.ResponseWriter, r *http.Request) {
body, err := response.GetBody(r) // et.Json del body
params := response.GetQuery(r) // et.Json de query params
id := response.GetParam(r, "id") // URL param chi
// Respuestas
response.JSON(w, r, http.StatusOK, data) // {"ok": true, "result": data}
response.ITEM(w, r, http.StatusOK, item) // et.Item
response.ITEMS(w, r, http.StatusOK, items) // et.Items
response.HTTPError(w, r, http.StatusBadRequest, "msg") // {"ok": false, "result": {"message": "msg"}}
response.HTTPAlert(w, r, "alerta") // 400
response.Unauthorized(w, r) // 401
response.Forbidden(w, r) // 403
response.InternalServerError(w, r, err) // 500
// Streaming paginado (útil para exportaciones grandes)
response.Stream(w, r, 100, func(page, rows int) (et.Items, error) {
return modelo.Select().Page(page, rows).List()
})
}
🛡️ Resiliencia
Sistema de reintentos automáticos para operaciones que pueden fallar.
import (
"github.com/celsiainternet/elvis/et"
"github.com/celsiainternet/elvis/resilience"
)
// Registrar operación con reintentos
// Configurar con RESILIENCE_TOTAL_ATTEMPTS (default 3) y RESILIENCE_TIME_ATTEMPTS en segundos (default 30)
instance := resilience.Add(
"", // id (vacío = generado automáticamente)
"email-bienvenida", // tag
"Enviar email de bienvenida", // descripción
et.Json{"user_id": "123"}, // metadata
"growth", // equipo
"critical", // nivel
func(to, asunto string) error {
return enviarEmail(to, asunto)
},
"juan@ejemplo.com", // arg 1
"¡Bienvenido!", // arg 2
)
// Con parámetros personalizados
instance = resilience.AddCustom(
"",
"pago",
"Procesar pago",
5, // intentos totales
10*time.Second, // tiempo entre intentos
et.Json{},
"pagos", "high",
procesarPago, datoPago,
)
// Control de instancias
resilience.Stop(instance.Id)
resilience.Restart(instance.Id)
🔁 Workflows
Orquestación de procesos multi-paso con soporte a rollback y expresiones condicionales.
import (
"time"
"github.com/celsiainternet/elvis/et"
"github.com/celsiainternet/elvis/workflow"
)
// Definir el flujo
flow := workflow.New(
"onboarding-usuario",
"v1",
"Onboarding",
"Proceso completo de alta de usuario",
func(inst *workflow.Instance, ctx et.Json) (et.Json, error) {
return et.Json{"iniciado": true}, nil
},
false, // modo debug
"sistema", // equipo
)
// Resiliencia del flujo
flow.Resilence(3, 30*time.Second, "growth", "critical")
// Agregar pasos
flow.Step("CrearUsuario", "Crear usuario en BD", func(inst *workflow.Instance, ctx et.Json) (et.Json, error) {
return et.Json{"user_id": utility.UUID()}, nil
}, false) // false = continúa automáticamente
// Rollback del paso anterior
flow.Rollback(func(inst *workflow.Instance, ctx et.Json) (et.Json, error) {
return et.Json{"revertido": true}, nil
})
flow.Step("EnviarEmail", "Enviar email de bienvenida", func(inst *workflow.Instance, ctx et.Json) (et.Json, error) {
return et.Json{"email_enviado": true}, nil
}, true) // true = paso de parada (se detiene hasta reanudar)
// Ejecutar instancia
result, err := workflow.Run(
"", // instanceId (vacío = generado automáticamente)
"onboarding-usuario", // tag del flow
-1, // step (-1 = ejecutar siguiente paso)
et.Json{"env": "prod"}, // contexto
et.Json{"email": "juan@e.com"}, // datos de entrada
"sistema",
)
⚙️ Variables de Entorno
| Variable |
Paquete |
Default |
Descripción |
DB_DRIVER |
jdb |
— |
postgres, mysql u oracle |
DB_HOST |
jdb |
— |
Host de la base de datos |
DB_PORT |
jdb |
5432 |
Puerto de la base de datos |
DB_NAME |
jdb |
— |
Nombre de la base de datos |
DB_USER |
jdb |
— |
Usuario de la base de datos |
DB_PASSWORD |
jdb |
— |
Contraseña de la base de datos |
DB_APPLICATION_NAME |
jdb |
elvis |
Nombre de la aplicación en PostgreSQL |
USE_CORE |
jdb |
true |
Inicializar tablas core (series, records, recycling) |
REDIS_HOST |
cache |
— |
Host de Redis (ej. localhost:6379) |
REDIS_PASSWORD |
cache |
— |
Contraseña de Redis |
REDIS_DB |
cache |
0 |
Número de base de datos Redis |
NATS_HOST |
event |
— |
URL de conexión NATS |
NATS_USER |
event |
— |
Usuario NATS |
NATS_PASSWORD |
event |
— |
Contraseña NATS |
SECRET |
claim |
1977 |
Clave de firma JWT |
HOST |
jrpc / router |
localhost |
Host del servicio actual |
PORT |
servicio |
3400 |
Puerto HTTP |
RPC_HOST |
jrpc |
HOST |
Host para RPC entre servicios |
RPC_PORT |
jrpc |
4200 |
Puerto RPC |
AUTHORIZATION_METHOD |
router |
— |
Método RPC para verificar permisos |
RESILIENCE_TOTAL_ATTEMPTS |
resilience |
3 |
Intentos totales por operación |
RESILIENCE_TIME_ATTEMPTS |
resilience |
30 |
Segundos entre reintentos |
🔧 Comandos de Desarrollo
# Compilar el módulo
go build ./...
# Ejecutar tests
go test ./...
# Ejecutar un test específico con verbose
go test ./paquete/... -run NombreTest -v
# Formatear código y ejecutar CLI de scaffolding
gofmt -w . && go run ./cmd/create go
# CLI para operaciones de base de datos
gofmt -w . && go run ./cmd/jdb go
# Actualizar dependencias
go mod tidy
📁 Estructura del Proyecto
elvis/
├── cache/ # Cliente Redis (Set, Get, Delete, Pub/Sub)
├── claim/ # JWT: generación, validación, invalidación
├── cmd/
│ ├── create/ # CLI scaffolding de microservicios
│ └── jdb/ # CLI de operaciones de base de datos
├── config/ # Carga de configuración
├── console/ # Logging interno de bajo nivel
├── create/
│ ├── v1/ # Generador de proyectos v1
│ └── v2/ # Generador de proyectos v2
├── crontab/ # Tareas programadas (cron)
├── dt/ # Objetos de transferencia de datos
├── envar/ # Helpers de variables de entorno
├── et/ # Tipos centrales: Json, Item, Items, List, Any
├── event/ # Eventos local (emitter) y distribuido (NATS)
├── file/ # Manejo de archivos
├── health/ # Health check helpers
├── jdb/ # Abstracción de base de datos (Postgres/MySQL/Oracle)
├── jrpc/ # RPC entre servicios vía Redis
├── linq/ # ORM / query builder
├── logs/ # Logging estructurado
├── mem/ # Cache in-memory con TTL
├── middleware/ # Middleware HTTP chi (auth, cors, logger, etc.)
├── msg/ # Mensajes de error compartidos
├── race/ # Helpers de concurrencia
├── reg/ # Registro de IDs
├── resilience/ # Reintentos automáticos
├── response/ # Helpers de respuesta HTTP
├── router/ # Registro de rutas chi con API Gateway
├── service/ # Cliente HTTP entre servicios
├── stdrout/ # Salida estándar / terminal
├── strs/ # Utilidades de strings
├── timezone/ # Manejo de zonas horarias
├── utility/ # Utilidades generales (UUID, OTP, crypto, etc.)
└── workflow/ # Orquestación de flujos multi-paso
🔔 Eventos del Sistema
Eventos internos que emite la librería y a los que se puede suscribir:
Workflows
| Evento |
Descripción |
workflow:set |
Se creó o actualizó un flujo |
workflow:delete |
Se eliminó un flujo |
workflow:status |
Cambio de estado de una instancia |
workflow:awaiting |
Instancia en espera |
workflow:results |
Resultados disponibles |
Resiliencia
| Evento |
Descripción |
resilience:status |
Cambio de estado de una instancia |
resilience:stop |
Detener instancia por id |
resilience:restart |
Reiniciar instancia por id |
resilience:failed |
Instancia agotó todos los intentos |
Base de Datos (JDB)
| Evento |
Descripción |
sql:error |
Error en consulta SQL |
sql:query |
Consulta SQL ejecutada |
sql:definition |
DDL ejecutado |
sql:command |
Comando DML ejecutado |
API Gateway (Router)
| Canal |
Descripción |
apigateway/set/resolve |
Nueva ruta registrada |
apigateway/delete/resolve |
Ruta eliminada |
apigateway/reset |
Solicitud de re-sincronización de rutas |
apigateway/set/proxy |
Nuevo proxy registrado |
apigateway/delete/proxy |
Proxy eliminado |
📄 Licencia
Distribuido bajo la licencia MIT. Ver LICENSE para más detalles.