Saltar al contenido principal

🧪 Proyectos Prácticos

Proyectos completos para practicar todo lo aprendido. Cada uno combina Bash, curl, jq, JSON/YAML y APIs.

#ProyectoQué aprenderásDificultad
01System Info ReporterVariables, funciones, JSON, jq🟢 Fácil
02Pokédex CLIcurl, APIs, jq, cache🟢 Fácil
03API Health Monitorcurl, loops, condicionales, JSON🟢 Fácil
04Config ManagerJSON/YAML, jq, validación🟢 Fácil
05Deploy ScriptFunciones, health checks, simulación🟢 Fácil
06Log Analyzergrep, awk, sed, sort, uniq, JSON output🟡 Medio

Proyecto 01 — System Info Reporter

Genera un reporte del sistema en JSON y texto con alertas.

#!/bin/bash
set -euo pipefail

TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

get_os_info() {
if [[ "$(uname)" == "Darwin" ]]; then
os="macOS $(sw_vers -productVersion 2>/dev/null || echo 'unknown')"
elif [[ -f /etc/os-release ]]; then
os=$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)
else
os=$(uname -s)
fi
jq -n --arg os "$os" --arg kernel "$(uname -r)" --arg arch "$(uname -m)" \
'{os: $os, kernel: $kernel, arch: $arch}'
}

get_memory_info() {
if [[ "$(uname)" == "Darwin" ]]; then
total_mb=$(( $(sysctl -n hw.memsize) / 1048576 ))
else
total_mb=$(free -m | awk '/Mem:/{print $2}')
fi
echo "{\"total_mb\": $total_mb}"
}

get_network_info() {
local ip_public
ip_public=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null || echo "N/A")
jq -n --arg ip "$ip_public" '{public_ip: $ip}'
}

# Generar reporte JSON
os_info=$(get_os_info)
mem_info=$(get_memory_info)
net_info=$(get_network_info)

jq -n --arg ts "$TIMESTAMP" --arg host "$(hostname)" \
--argjson os "$os_info" --argjson mem "$mem_info" --argjson net "$net_info" \
'{timestamp: $ts, hostname: $host, os: $os, memory: $mem, network: $net}'

Proyecto 02 — Pokédex CLI

Consume la PokéAPI para mostrar info de Pokémon desde la terminal con cache.

#!/bin/bash
set -euo pipefail

POKE_API="https://pokeapi.co/api/v2"
CACHE_DIR="/tmp/pokedex-cache"
mkdir -p "$CACHE_DIR"

cached_get() {
local url="$1"
local cache_file="$CACHE_DIR/$(echo "$url" | md5sum 2>/dev/null | cut -d' ' -f1)"
if [[ -f "$cache_file" ]]; then
local age=$(( $(date +%s) - $(stat -c%Y "$cache_file" 2>/dev/null || stat -f%m "$cache_file") ))
[[ $age -lt 3600 ]] && { cat "$cache_file"; return 0; }
fi
curl -s --max-time 10 "$url" | tee "$cache_file"
}

pokemon_info() {
local data=$(cached_get "$POKE_API/pokemon/${1,,}")
echo "$data" | jq '{
id: .id, nombre: .name,
tipos: [.types[].type.name],
stats: [.stats[] | {(.stat.name): .base_stat}] | add,
total: [.stats[].base_stat] | add,
habilidades: [.abilities[].ability.name]
}'
}

pokemon_vs() {
local d1=$(cached_get "$POKE_API/pokemon/${1,,}")
local d2=$(cached_get "$POKE_API/pokemon/${2,,}")
echo "=== ${1} vs ${2} ==="
for stat in hp attack defense speed; do
v1=$(echo "$d1" | jq ".stats[] | select(.stat.name==\"$stat\") | .base_stat")
v2=$(echo "$d2" | jq ".stats[] | select(.stat.name==\"$stat\") | .base_stat")
printf " %-18s %3s vs %-3s %s\n" "$stat" "$v1" "$v2" \
"$([[ $v1 -gt $v2 ]] && echo "◀" || ([[ $v2 -gt $v1 ]] && echo "▶" || echo "="))"
done
}

case "${1:-help}" in
info) pokemon_info "${2:?Falta nombre}" ;;
vs) pokemon_vs "${2:?Falta P1}" "${3:?Falta P2}" ;;
*) echo "Uso: $0 {info|vs} <pokemon> [pokemon2]" ;;
esac

Proyecto 03 — API Health Monitor

Monitorea múltiples endpoints y genera reportes.

#!/bin/bash
set -euo pipefail

ENDPOINTS=(
"PokéAPI|https://pokeapi.co/api/v2/pokemon/1|200"
"GitHub|https://api.github.com|200"
"httpbin|https://httpbin.org/get|200"
)

echo "══════════════════════════════════════"
echo " API Health Monitor — $(date '+%H:%M:%S')"
echo "══════════════════════════════════════"

for endpoint in "${ENDPOINTS[@]}"; do
IFS='|' read -r name url expected <<< "$endpoint"

result=$(curl -s -o /dev/null \
-w "%{http_code}|%{time_total}|%{time_starttransfer}" \
--max-time 15 "$url" 2>&1) || true

IFS='|' read -r code total ttfb <<< "$result"
ms=$(echo "$total * 1000" | bc | cut -d. -f1)

if [[ "$code" == "$expected" ]]; then
printf " ✅ %-20s HTTP %-4s %5sms\n" "$name" "$code" "$ms"
elif [[ "$code" == "000" ]]; then
printf " ❌ %-20s DOWN\n" "$name"
else
printf " ⚠️ %-20s HTTP %-4s (esperado: %s)\n" "$name" "$code" "$expected"
fi
done

Proyecto 04 — Config Manager

Gestiona configuraciones de apps en JSON con validación.

#!/bin/bash
set -euo pipefail

init_config() {
cat > "${1:-mi-app}.json" << 'EOF'
{
"app": {"name": "mi-app", "version": "1.0.0", "environment": "development"},
"server": {"host": "0.0.0.0", "port": 8000, "workers": 4},
"database": {"host": "localhost", "port": 5432, "name": "myapp_db"}
}
EOF
echo "✅ Config creada: ${1:-mi-app}.json"
}

read_config() {
local file="$1" key="${2:-}"
[[ -z "$key" ]] && jq '.' "$file" || jq ".$key" "$file"
}

validate_config() {
if jq '.' "$1" > /dev/null 2>&1; then
echo "✅ JSON válido: $1"
for field in "app.name" "app.version" "server.port"; do
val=$(jq -r ".$field // empty" "$1")
[[ -n "$val" ]] && echo " ✅ $field = $val" || echo " ❌ $field — faltante"
done
else
echo "❌ JSON inválido: $1"
fi
}

case "${1:-help}" in
init) init_config "$2" ;;
read) read_config "$2" "${3:-}" ;;
validate) validate_config "$2" ;;
*) echo "Uso: $0 {init|read|validate} <archivo> [clave]" ;;
esac

Proyecto 05 — Deploy Script (simulación)

#!/bin/bash
set -euo pipefail

APP="${1:?Uso: $0 <app> <version>}"
VERSION="${2:?Falta version}"

log() { echo "[$(date '+%H:%M:%S')] $*"; }

log "🚀 Iniciando deploy de $APP v$VERSION"

# Paso 1: Verificar servicio actual
log "Verificando servicio actual..."
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "https://httpbin.org/get")
[[ "$code" == "200" ]] && log "✅ Servicio actual OK" || log "⚠️ Servicio actual: HTTP $code"

# Paso 2: Simular deploy
log "Desplegando $APP v$VERSION..."
sleep 2
log "✅ Deploy completado"

# Paso 3: Health check post-deploy
log "Verificando nuevo deploy..."
for i in {1..5}; do
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "https://httpbin.org/get")
[[ "$code" == "200" ]] && { log "✅ Health check OK"; break; }
log "⏳ Esperando... ($i/5)"
sleep 3
done

log "🎉 Deploy de $APP v$VERSION finalizado"

Proyecto 06 — Log Analyzer

Analiza logs de acceso (estilo Nginx/Apache) y genera reportes en texto y JSON.

#!/bin/bash
set -euo pipefail

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'

# ── Generar logs de ejemplo ──
generate_sample_logs() {
local file="${1:-/tmp/access.log}"
local count="${2:-200}"

local ips=("192.168.1.10" "192.168.1.25" "10.0.0.5" "172.16.0.100" "8.8.8.8"
"1.1.1.1" "192.168.1.50" "10.0.0.15" "203.0.113.42" "198.51.100.7")
local methods=("GET" "GET" "GET" "GET" "POST" "POST" "PUT" "DELETE" "GET" "GET")
local paths=("/api/v1/pokemon" "/api/v1/pokemon/25" "/health" "/api/v1/users"
"/api/v1/pokemon/999" "/admin/login" "/api/v2/pokemon" "/favicon.ico")
local codes=("200" "200" "200" "201" "200" "204" "404" "500" "401" "301")
local agents=("Mozilla/5.0" "curl/8.1.0" "python-requests/2.31" "PostmanRuntime/7.32")

echo "" > "$file"
for ((i=0; i<count; i++)); do
local ip="${ips[$((RANDOM % ${#ips[@]}))]}"
local method="${methods[$((RANDOM % ${#methods[@]}))]}"
local path="${paths[$((RANDOM % ${#paths[@]}))]}"
local code="${codes[$((RANDOM % ${#codes[@]}))]}"
local agent="${agents[$((RANDOM % ${#agents[@]}))]}"
local size=$((RANDOM % 5000 + 100))
local day=$((RANDOM % 28 + 1))
local hour=$((RANDOM % 24))
local min=$((RANDOM % 60))
local date_str
date_str=$(printf "%02d/Feb/2026:%02d:%02d:00" "$day" "$hour" "$min")
printf '%s - - [%s +0000] "%s %s HTTP/1.1" %s %d "%s"\n' \
"$ip" "$date_str" "$method" "$path" "$code" "$size" "$agent" >> "$file"
done
echo -e "${GREEN}✅ Generados $count logs en $file${NC}"
}

# ── Análisis ──
analyze_overview() {
local file="$1"
local total lines_200 lines_4xx lines_5xx
total=$(wc -l < "$file" | xargs)
lines_200=$(grep -c '" 2[0-9][0-9] ' "$file" || echo 0)
lines_4xx=$(grep -c '" 4[0-9][0-9] ' "$file" || echo 0)
lines_5xx=$(grep -c '" 5[0-9][0-9] ' "$file" || echo 0)

echo -e "${BLUE}═══ Resumen General ═══${NC}"
echo " Total peticiones: $total"
echo -e " ${GREEN}2xx (éxito): $lines_200${NC}"
echo -e " ${YELLOW}4xx (cliente): $lines_4xx${NC}"
echo -e " ${RED}5xx (servidor): $lines_5xx${NC}"

local error_rate=0
[[ $total -gt 0 ]] && error_rate=$((lines_5xx * 100 / total))
if [[ $error_rate -gt 5 ]]; then
echo -e " ${RED}🚨 Error rate: ${error_rate}% (>5%)${NC}"
else
echo -e " ${GREEN}✅ Error rate: ${error_rate}%${NC}"
fi
}

analyze_top_ips() {
local file="$1" limit="${2:-10}"
echo -e "\n${BLUE}═══ Top $limit IPs ═══${NC}"
awk '{print $1}' "$file" | sort | uniq -c | sort -rn | head -"$limit" | \
awk '{printf " %-6s %s\n", $1, $2}'
}

analyze_top_paths() {
local file="$1" limit="${2:-10}"
echo -e "\n${BLUE}═══ Top $limit Endpoints ═══${NC}"
awk -F'"' '{print $2}' "$file" | awk '{print $2}' | sort | uniq -c | sort -rn | head -"$limit" | \
awk '{printf " %-6s %s\n", $1, $2}'
}

analyze_status_codes() {
local file="$1"
echo -e "\n${BLUE}═══ Status Codes ═══${NC}"
awk '{print $9}' "$file" | sort | uniq -c | sort -rn | while read -r count code; do
local color="$NC"
case "$code" in
2*) color="$GREEN" ;; 3*) color="$BLUE" ;;
4*) color="$YELLOW" ;; 5*) color="$RED" ;;
esac
printf " ${color}%-6s HTTP %s${NC}\n" "$count" "$code"
done
}

analyze_per_hour() {
local file="$1"
echo -e "\n${BLUE}═══ Peticiones por Hora ═══${NC}"
awk -F'[: ]' '{print $5}' "$file" | sort | uniq -c | sort -k2n | while read -r count hour; do
local bar=""
local bar_len=$((count / 2))
for ((j=0; j<bar_len && j<40; j++)); do bar+="█"; done
printf " %02d:00 %-6s %s\n" "$hour" "$count" "$bar"
done
}

# ── Reporte JSON ──
generate_json_report() {
local file="$1" output="${2:-/tmp/log-report.json}"
local total lines_2xx lines_4xx lines_5xx
total=$(wc -l < "$file" | xargs)
lines_2xx=$(grep -c '" 2[0-9][0-9] ' "$file" || echo 0)
lines_4xx=$(grep -c '" 4[0-9][0-9] ' "$file" || echo 0)
lines_5xx=$(grep -c '" 5[0-9][0-9] ' "$file" || echo 0)

local top_ips top_paths status_codes
top_ips=$(awk '{print $1}' "$file" | sort | uniq -c | sort -rn | head -5 | \
awk '{printf "{\"ip\":\"%s\",\"count\":%s}\n", $2, $1}' | jq -s '.')
top_paths=$(awk -F'"' '{print $2}' "$file" | awk '{print $2}' | sort | uniq -c | sort -rn | head -5 | \
awk '{printf "{\"path\":\"%s\",\"count\":%s}\n", $2, $1}' | jq -s '.')
status_codes=$(awk '{print $9}' "$file" | sort | uniq -c | sort -rn | \
awk '{printf "{\"code\":%s,\"count\":%s}\n", $2, $1}' | jq -s '.')

jq -n \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg file "$file" \
--argjson total "$total" --argjson ok "$lines_2xx" \
--argjson client_err "$lines_4xx" --argjson server_err "$lines_5xx" \
--argjson top_ips "$top_ips" --argjson top_paths "$top_paths" \
--argjson status "$status_codes" \
'{report: {generated_at: $ts, source_file: $file,
summary: {total_requests: $total, successful_2xx: $ok,
client_errors_4xx: $client_err, server_errors_5xx: $server_err,
error_rate_percent: (if $total > 0 then ($server_err * 100 / $total) else 0 end)},
top_ips: $top_ips, top_paths: $top_paths, status_codes: $status}}' > "$output"
echo -e "\n${GREEN}Reporte JSON: $output${NC}"
}

# ── Main ──
case "${1:-help}" in
generate) generate_sample_logs "${2:-/tmp/access.log}" "${3:-200}" ;;
analyze)
LOG_FILE="${2:?Falta archivo de log}"
[[ ! -f "$LOG_FILE" ]] && { echo -e "${RED}❌ Archivo no encontrado: $LOG_FILE${NC}"; exit 1; }
analyze_overview "$LOG_FILE"
analyze_status_codes "$LOG_FILE"
analyze_top_ips "$LOG_FILE"
analyze_top_paths "$LOG_FILE"
analyze_per_hour "$LOG_FILE"
;;
top-ips) analyze_top_ips "${2:?Falta archivo}" "${3:-10}" ;;
top-paths) analyze_top_paths "${2:?Falta archivo}" "${3:-10}" ;;
hourly) analyze_per_hour "${2:?Falta archivo}" ;;
json) generate_json_report "${2:?Falta archivo}" "${3:-/tmp/log-report.json}" ;;
*)
echo "Log Analyzer v1.0"
echo " $0 generate [archivo] [cantidad] Generar logs de ejemplo"
echo " $0 analyze <archivo> Análisis completo"
echo " $0 top-ips <archivo> [n] Top IPs"
echo " $0 top-paths <archivo> [n] Top endpoints"
echo " $0 hourly <archivo> Tráfico por hora"
echo " $0 json <archivo> [salida.json] Reporte JSON"
;;
esac