Skip to main content

Construyendo Métricas Personalizadas

Vamos entonces a construir una métrica personalizada. El código estará disponible en la rama personal_metrics.

Una vez definido nuestro instrumentador vemos que estamos usando un retorno que entrega el meter instanciado

  // Inicia el SDK - comienza a recoger telemetría
sdk.start();
return { meter, meterProvider } // <<<<<

En el código de todo-service.ts podemos aprovechar esas instancias.

// start('todo-service')
const { meter } = start('todo-service');

Y a partir de esto podemos empezar. Solo para aclarar, app.use() en Express sirve como un sistema de intercepción y procesamiento de peticiones antes de que lleguen a las rutas finales. Se usa mucho para interceptar las peticiones HTTP, procesar o modificar una petición antes de que llegue a la ruta final, ejecutar acciones comunes para múltiples rutas, añadir una capa de procesamiento, etc.

Vamos a instanciar tiempo de entrada antes de iniciar la petición y otro al final y restar para tener el tiempo de procesamiento.

// Creando una métrica del tipo Histogram con el nombre latency-call
const latencyCall = meter.createHistogram('latency_call', {
description: 'Latency of /todos endpoint calls in milliseconds'
});

app.use((req, res, next) => {
// Captura el tiempo de inicio de la petición
const startTime = Date.now();

// Añade un listener para cuando la petición termine
req.on('end', () => {
// Captura el tiempo de fin de la petición
const endTime = Date.now();

// Registra las métricas de la llamada
latencyCall.record(endTime - startTime, {
route: req.route?.path, // Ruta que fue accedida
status: res.statusCode, // Código de estado de la respuesta
method: req.method // Método HTTP usado (GET, POST, etc)
})
})
// Continúa el procesamiento de la petición
next();
})

Solo con esto ya será posible tener una medida del tiempo de procesamiento de cada petición.

Pero vamos a crear también un contador para el número de veces que este endpoint fue accedido.

const requestCounter = meter.createCounter('todos_requests_total', {
description: 'Total number of requests to /todos endpoint'
});

Y vamos a incrementar cada vez que entre en /todos.

//... Código anterior...
app.get('/todos', async (req, res) => {
requestCounter.add(1); //Incrementando
const user = await axios.get('http://auth:8080/auth');
//... Resto del código

Para simular vamos a pasar dos parámetros. Si fail es pasado entonces vamos a devolver 500 y si slow es pasado vamos a hacer dormir 1 segundo.

Entonces tenemos nuestro código completo.

import start from './instrumentation';
// start('todo-service')
const { meter } = start('todo-service');

import express from 'express';
import axios from 'axios';
import IORedis from 'ioredis';

const app = express();
const redis = new IORedis({ host: 'redis' });

// Nuestras dos métricas
const latencyCall = meter.createHistogram('latency_call', {
description: 'Latency of /todos endpoint calls in milliseconds'
});

const requestCounter = meter.createCounter('todos_requests_total', {
description: 'Total number of requests to /todos endpoint'
});

//Siempre será ejecutado antes y al final para coger el tiempo.
app.use((req,res,next)=>{
const startTime = Date.now();
req.on('end',()=>{
const endTime = Date.now();
latencyCall.record(endTime-startTime,{
route: req.route?.path,
status: res.statusCode,
method: req.method
})
})
next();
})

// Definimos una función sleep solo para ser usada en el endpoint todos.
const sleep = (time:number)=>{return new Promise((resolve)=>{setTimeout(resolve,time)})};

app.get('/todos', async (req, res) => {
requestCounter.add(1); // Incrementamos el contador
const user = await axios.get('http://auth:8080/auth');
const todoKeys = await redis.keys('todo:*');
const todos: any[] = [];

for (const key of todoKeys) {
const todoItem = await redis.get(key);
if (todoItem) {
todos.push(JSON.parse(todoItem));
}
}
// Si tiene la query slow duerme 1 segundo
if(req.query['slow']){
await sleep(1000);
}

// Si tiene la query fail para todo y devuelve 500
if(req.query['fail']){
await console.error('Really Bad error!');
res.sendStatus(500);
return
}

res.json({ todos, user: user.data });
});

app.listen(8080, () => {
console.log('Todo service is running on port 8080');
});

// Inicializa algunos todos para test
async function init() {
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' }))
]);
}

init().catch(console.error);

Vamos a ejecutar.

docker compose up --build

Y hacer unas peticiones


curl http://localhost:8081/todos
{"todos":[{"name":"Añadir métricas"},{"name":"Configurar OpenTelemetry"},{"name":"Implementar tracing"},{"name":"Configurar exporters"}],"user":{"username":"David Prata"}}

curl http://localhost:8081/todos
{"todos":[{"name":"Añadir métricas"},{"name":"Configurar OpenTelemetry"},{"name":"Implementar tracing"},{"name":"Configurar exporters"}],"user":{"username":"David Prata"}}

curl http://localhost:8081/todos
{"todos":[{"name":"Añadir métricas"},{"name":"Configurar OpenTelemetry"},{"name":"Implementar tracing"},{"name":"Configurar exporters"}],"user":{"username":"David Prata"}}

curl http://localhost:8081/todos
{"todos":[{"name":"Añadir métricas"},{"name":"Configurar OpenTelemetry"},{"name":"Implementar tracing"},{"name":"Configurar exporters"}],"user":{"username":"David Prata"}}

curl http://localhost:8081/todos\?slow
{"todos":[{"name":"Añadir métricas"},{"name":"Configurar OpenTelemetry"},{"name":"Implementar tracing"},{"name":"Configurar exporters"}],"user":{"username":"David Prata"}}

curl http://localhost:8081/todos\?slow\=1
{"todos":[{"name":"Añadir métricas"},{"name":"Configurar OpenTelemetry"},{"name":"Implementar tracing"},
{"name":"Configurarexporters"}],"user":{"username":"David Prata"}}

curl http://localhost:8081/todos\?fail
{"todos":[{"name":"Añadir métricas"},{"name":"Configurar OpenTelemetry"},{"name":"Implementar tracing"},{"name":"Configurar exporters"}],"user":{"username":"David Prata"}}

curl http://localhost:8081/todos\?fail\=1
Internal Server Error

Hicimos 7 peticiones entonces tenemos en Prometheus localhost:9090

alt text

¿Y para ver la latencia?

alt text