Proyecto Base
La propuesta es conocer cómo funciona por dentro de una aplicación para entender la instrumentación. Si eres DevOps y estás pensando: "esto es problema de los devs", no te quito la razón. ¿Por qué no lo hicieron en un lenguaje que se pueda automatizar? :D
El problema es que necesitamos crear algo para generar los traces que necesitamos.
❯ tree
.
├── docker #Dockerfiles
│ ├── Dockerfile.auth
│ └── Dockerfile.todo
├── docker-compose.yml
├── package-lock.json
├── package.json
├── src
│ └── services
│ ├── auth-service.ts # Código del Auth
│ └── todo-service.ts # Código del TODOS
└── tsconfig.json
4 directories, 8 files
Analizando docker-compose.yaml tenemos esto. Ya estamos montando el volumen dentro de cada uno de los contenedores. El volumen es la propia carpeta del proyecto. Por eso no necesitamos hacer el COPY . . después dentro de los dockerfiles.
Las aplicaciones dentro corren en el puerto 8080 cada una de ellas, entonces vamos a poner 8080 para el auth y 8081 para el todos para accederlos externamente.
version: '3.8'
services:
todo:
build:
context: .
dockerfile: docker/Dockerfile.todo
volumes:
- .:/usr/src/app:rw
- /usr/src/app/node_modules
ports:
- 8081:8080
depends_on:
- redis
auth:
build:
context: .
dockerfile: docker/Dockerfile.auth
volumes:
- .:/usr/src/app:rw
- /usr/src/app/node_modules
ports:
- 8080:8080
redis:
image: redis:alpine
ports:
- '6380:6379'
# Mi máquina utiliza redis de forma personal entonces voy a usar externamente el 6380
Un redis, una aplicación auth y una todo. El dockerfile de las dos aplicaciones es el mismo cambiando solamente el CMD que iniciará auth o todos.
| auth dockerfile | todo dockerfile |
|---|---|
| FROM node:22-alpine | FROM node:22-alpine |
| WORKDIR /usr/src/app | WORKDIR /usr/src/app |
| COPY package*.json ./ | COPY package*.json ./ |
| RUN npm install | RUN npm install |
| EXPOSE 8080 | EXPOSE 8080 |
| CMD ["npm", "run", "auth"] | CMD ["npm", "run", "todo"] |
Nuestro proyecto será un proyecto NodeJS simple que se levanta solo iniciando con docker-compose up para que te pongas feliz.
Ahora vamos a partir para entender el auth-service.js.
// Importa el framework Express.js para crear aplicaciones web
import express from 'express';
// Crea una nueva instancia del aplicativo Express
const app = express();
// Define una ruta GET para el endpoint '/auth'
app.get('/auth', (req, res) => {
// Cuando se accede, retorna un objeto JSON con un username
// req: contiene informaciones de la petición HTTP
// res: usado para enviar la respuesta al cliente
res.json({username: 'David Puziol'})
})
// Inicia el servidor en el puerto 8080
app.listen(8080, () => {
// Callback ejecutado cuando el servidor está listo
// Imprime mensaje en la consola confirmando que está corriendo
console.log('Auth service is running on port 8080');
})
Y tenemos el todo-service.js que será nuestra aplicación. Estamos creando entonces la aplicación web que tiene una ruta para ser llamada en /todos que hace llamada a un servicio auth:8080/auth que definimos también y estará corriendo en otro contenedor llamado auth y también puebla el redis.
// Importa los paquetes necesarios
import express from 'express'; // Framework web
import axios from 'axios'; // Cliente HTTP para hacer peticiones
import IORedis from 'ioredis'; // Cliente Redis
// Crea una instancia de Express
const app = express();
// Configura conexión con Redis usando el hostname 'redis' (definido en docker-compose)
const redis = new IORedis({ host: 'redis' });
// Define ruta GET /todos
app.get('/todos', async (req, res) => {
// Busca informaciones del usuario del servicio de autenticación
const user = await axios.get('http://auth:8080/auth');
// Busca todas las llaves en Redis que comienzan con 'todo:'
const todoKeys = await redis.keys('todo:*');
// Array para almacenar los todos
const todos: any[] = [];
// Itera sobre cada llave encontrada
for (const key of todoKeys) {
// Busca el valor almacenado para cada llave
const todoItem = await redis.get(key);
if (todoItem) {
// Convierte la cadena JSON en objeto y añade al array
todos.push(JSON.parse(todoItem));
}
}
// Retorna los todos e informaciones del usuario
res.json({ todos, user: user.data });
});
// Inicia el servidor en el puerto 8080
app.listen(8080, () => {
console.log('Todo service is running on port 8080');
});
// Función para inicializar datos de ejemplo en Redis
async function init() {
// Crea 4 todos iniciales usando Promise.all para ejecución paralela
await Promise.all([
redis.set('todo:1', JSON.stringify({ name: 'Configurar OpenTelemetry' })),
redis.set('todo:2', JSON.stringify({ name: 'Implementar tracing' })),
redis.set('todo:3', JSON.stringify({ name: 'Añadir métricas' })),
redis.set('todo:4', JSON.stringify({ name: 'Configurar exporters' }))
]);
}
// Ejecuta la función de inicialización y trata posibles errores
init().catch(console.error);
No instrumentamos nada hasta ahora, ¿ok? Solo para que quede claro.
¿Vamos a levantar el ambiente todo?
# Da igual, docker o podman
podman compose up --build
Y tenemos esto aquí.
curl localhost:8080/auth
{"username":"David Prata"}
curl localhost:8081/todos
{"todos":[{"name":"Implementar tracing"},{"name":"Configurar exporters"},{"name":"Añadir métricas"},{"name":"Configurar OpenTelemetry"}],"user":{"username":"David Prata"}}
El proyecto está disponible en Este Repositorio. En la branch main tenemos el proyecto completo pero lo que vimos hasta ahora es la branch base y vamos creciendo a lo largo del camino.
git clone https://gitlab.com/davidpuziol/opentelemetry-project.git
git checkout base