Caso Real: Orquestación de Servidores Minecraft
El Problema
Gestionar una flota de servidores Minecraft (Forge) en contenedores Docker crea una brecha de observabilidad:
- Los logs se escriben en
stdoutdentro del contenedor — son efímeros y no estructurados - La detección de crashes requiere hacer polling a los contenedores o ejecutar un daemon de monitoreo separado
- Enviar alertas a Discord requiere desplegar un bot (OAuth, permisos, gestión de tokens)
- Escalar a múltiples instancias Forge multiplica todos los problemas anteriores
La solución típica de homelab implica 3–5 herramientas separadas (Prometheus, Grafana, un bot de Discord, agregadores de logs). Esa es una cadena de dependencias que se rompe.
La Solución TinyMQ
Un sidecar recolector de logs ligero lee el stdout de cada servidor, parsea eventos y los publica en TinyMQ. La funcionalidad de Webhook nativo de TinyMQ entrega las alertas de crash directamente a la URL de Incoming Webhook de Discord — sin necesidad de bot de Discord.
Arquitectura
Paso 1: Registrar el Webhook de Discord
Discord ofrece URLs de Incoming Webhook nativas para los canales — no se necesita bot, solo una URL.
En Discord: Configuración del Canal → Integraciones → Webhooks → Nuevo Webhook → Copiar URL
Regístrala en TinyMQ:
curl -X POST http://localhost:7800/webhook/mc.crashes \
-H "Content-Type: application/json" \
-d '{"url": "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN"}'
Ahora cualquier mensaje publicado en mc.crashes se enviará mediante POST directamente al endpoint webhook de Discord.
Los webhooks de Discord esperan un payload JSON específico con un campo content (o embeds). El worker recolector de logs debe formatear el payload de TinyMQ de manera acorde antes de publicar en mc.crashes.
Paso 2: Sidecar Recolector de Logs (Go)
Este sidecar lee el archivo de log del servidor Minecraft (o el stream de log de Docker), clasifica los eventos y los publica en TinyMQ.
package main
import (
"bufio"
"encoding/json"
"log"
"os"
"strings"
"time"
"github.com/x-name15/tinymq/client"
)
type LogEvent struct {
ServerID string `json:"server_id"`
Level string `json:"level"` // "INFO", "WARN", "ERROR", "CRASH"
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
func classifyLine(line string) string {
upper := strings.ToUpper(line)
switch {
case strings.Contains(upper, "CRASH") || strings.Contains(upper, "FATAL"):
return "CRASH"
case strings.Contains(upper, "ERROR") || strings.Contains(upper, "EXCEPTION"):
return "ERROR"
case strings.Contains(upper, "WARN"):
return "WARN"
default:
return "INFO"
}
}
func main() {
serverID := os.Getenv("SERVER_ID") // e.g., "forge-survival-1"
logPath := os.Getenv("LOG_PATH") // e.g., "/logs/latest.log"
brokerURL := os.Getenv("TINYMQ_URL")
if brokerURL == "" {
brokerURL = "http://tinymq:7800"
}
mq := client.NewClient(brokerURL)
f, err := os.Open(logPath)
if err != nil {
log.Fatalf("Cannot open log file: %v", err)
}
defer f.Close()
// Seek to end — only new lines
f.Seek(0, 2)
log.Printf("[%s] Collector started. Tailing %s...\n", serverID, logPath)
scanner := bufio.NewScanner(f)
for {
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "" {
continue
}
event := LogEvent{
ServerID: serverID,
Level: classifyLine(line),
Message: line,
Timestamp: time.Now(),
}
payload, _ := json.Marshal(event)
// Publish all logs to general topic
mq.Publish("mc.logs", payload)
// Escalate crashes to dedicated topic (which has Discord webhook)
if event.Level == "CRASH" || event.Level == "ERROR" {
// Format for Discord
discordPayload, _ := json.Marshal(map[string]string{
"content": "🚨 **[" + serverID + "]** Server Event `" + event.Level + "`:\n```\n" + line + "\n```",
})
mq.Publish("mc.crashes", discordPayload)
}
}
// File hasn't grown — wait 500ms before next scan
time.Sleep(500 * time.Millisecond)
}
}
Paso 3: Docker Compose — Stack Completo
# docker-compose.yml
services:
tinymq:
image: ghcr.io/x-name15/tinymq:latest
ports:
- "7800:7800"
volumes:
- ./data:/root/data
restart: unless-stopped
# Minecraft Forge Server 1
mc-survival:
image: itzg/minecraft-server
environment:
- EULA=TRUE
- TYPE=FORGE
- VERSION=1.20.1
- MEMORY=4G
ports:
- "25565:25565"
volumes:
- ./servers/survival:/data
restart: unless-stopped
# Log Collector Sidecar for mc-survival
collector-survival:
build: ./collector
environment:
- SERVER_ID=forge-survival-1
- LOG_PATH=/logs/latest.log
- TINYMQ_URL=http://tinymq:7800
volumes:
- ./servers/survival/logs:/logs:ro # Read-only access to server logs
depends_on:
- tinymq
- mc-survival
restart: unless-stopped
# Minecraft Forge Server 2
mc-creative:
image: itzg/minecraft-server
environment:
- EULA=TRUE
- TYPE=FORGE
- VERSION=1.20.1
- MEMORY=2G
ports:
- "25566:25565"
volumes:
- ./servers/creative:/data
restart: unless-stopped
collector-creative:
build: ./collector
environment:
- SERVER_ID=forge-creative-1
- LOG_PATH=/logs/latest.log
- TINYMQ_URL=http://tinymq:7800
volumes:
- ./servers/creative/logs:/logs:ro
depends_on:
- tinymq
- mc-creative
restart: unless-stopped
Paso 4: Monitorear desde la CLI
# See all active queues — how many logs are pending?
tmq status
# Peek at the latest crash events without consuming them
tmq peek mc.crashes --limit=5
# Live-tail all log events in real-time
tmq tail mc.logs
# Check if any server crashed in the last hour
tmq sub mc.crashes --limit=20 --auto-ack=false --timeout=1s
Qué Obtienes
| Capacidad | Implementación |
|---|---|
| Stream de logs unificado | Todos los servidores publican en mc.logs |
| Detección de crashes | Pattern matching en el collector → publica en mc.crashes |
| Alertas en Discord | Webhook nativo de TinyMQ → Discord Incoming Webhook |
| Persistencia de logs | WAL garantiza que los logs sobrevivan a reinicios de TinyMQ |
| Fan-in multi-servidor | Todos los collectors publican al mismo topic |
| Sin bot requerido | La URL de Discord webhook es simplemente un endpoint HTTP |
| Observabilidad | /dashboard muestra conteos de mensajes y salud de los consumers |
Extendiendo el Patrón
Esta arquitectura se extiende de forma natural:
- Tracking de join/leave de jugadores: Parsear
[INFO] PlayerName joined the game→ publicar enmc.players - Monitoreo de rendimiento: Parsear avisos de TPS → publicar en
mc.performancecon TTL de 1h - Disparadores de backup: Publicar en
mc.maintenancecuando se detecta[INFO] Saving world→ disparar un worker de backup en S3 - Soporte multi-juego: Sustituir Minecraft por Valheim, Palworld o cualquier servidor que escriba logs en stdout