feat: add end-to-end observability (metrics, health, mobile RUM, dashboards, alerts)
- Backend: OTel metrics via OTLP -> Alloy -> Prometheus (OTEL_METRICS_EXPORTER=otlp) - New business metrics (src/metrics.js): searches, CIMA latency/errors, cache hits/misses, logins, rate-limits, pharmacy writes/links, push sent/failed, DB + Redis timings/errors, HTTP req count/duration, heartbeat - Backend health endpoints /healthz and /readyz - Mobile (Expo): Grafana Faro RUM via @grafana/faro-react-native - redis/postgres exporters in docker-compose + Prometheus scrape jobs - Grafana dashboards (backend, datastores, mobile RUM, overview) - Prometheus alert rules (farmafinder_*) -> existing Alertmanager (Telegram) - Design/spec saved to docs/superpowers/specs/
This commit is contained in:
@@ -1,9 +1,27 @@
|
||||
import axios from 'axios';
|
||||
import redisClient from './redis-client.js';
|
||||
import * as appMetrics from './src/metrics.js';
|
||||
|
||||
const CIMA_API_BASE_URL = 'https://cima.aemps.es/cima/rest';
|
||||
const CACHE_TTL = 3600;
|
||||
|
||||
/**
|
||||
* Wraps a CIMA API GET with duration + success/error metrics.
|
||||
*/
|
||||
async function trackedCimaGet(url, params) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const response = await axios.get(url, { params, timeout: 5000 });
|
||||
appMetrics.cimaRequestDuration.record(Date.now() - start);
|
||||
appMetrics.cimaRequestsTotal.add(1, { status: 'ok' });
|
||||
return response;
|
||||
} catch (err) {
|
||||
appMetrics.cimaRequestDuration.record(Date.now() - start);
|
||||
appMetrics.cimaRequestsTotal.add(1, { status: 'error' });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Separa los términos de dosis (ej: "1g", "1000 mg") del nombre del
|
||||
* medicamento, para poder buscar en CIMA solo con el nombre y luego
|
||||
@@ -69,23 +87,24 @@ export async function searchMedicines(query) {
|
||||
const cacheKey = `medicines:search:v3:${searchTerm}`;
|
||||
|
||||
try {
|
||||
appMetrics.medicineSearchesTotal.add(1);
|
||||
|
||||
// Intentar obtener del caché
|
||||
const cachedData = await redisClient.get(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
console.log(`📦 Cache hit for: ${searchTerm}`);
|
||||
appMetrics.cacheHitsTotal.add(1);
|
||||
return JSON.parse(cachedData);
|
||||
}
|
||||
|
||||
appMetrics.cacheMissesTotal.add(1);
|
||||
const parsed = parseSearchQuery(searchTerm);
|
||||
const apiSearchTerm = parsed.nameQuery || searchTerm;
|
||||
|
||||
console.log(`🌐 Fetching from CIMA API: ${apiSearchTerm}`);
|
||||
const response = await axios.get(`${CIMA_API_BASE_URL}/medicamentos`, {
|
||||
params: {
|
||||
nombre: apiSearchTerm
|
||||
},
|
||||
timeout: 5000
|
||||
const response = await trackedCimaGet(`${CIMA_API_BASE_URL}/medicamentos`, {
|
||||
nombre: apiSearchTerm,
|
||||
});
|
||||
|
||||
if (response.data && response.data.resultados) {
|
||||
@@ -143,20 +162,23 @@ export async function getMedicineDetails(nregistro) {
|
||||
const cacheKey = `medicine:${nregistro}`;
|
||||
|
||||
try {
|
||||
appMetrics.medicineSearchesTotal.add(1);
|
||||
|
||||
// Intentar obtener del caché
|
||||
const cachedData = await redisClient.get(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
console.log(`📦 Cache hit for medicine: ${nregistro}`);
|
||||
appMetrics.cacheHitsTotal.add(1);
|
||||
return JSON.parse(cachedData);
|
||||
}
|
||||
|
||||
appMetrics.cacheMissesTotal.add(1);
|
||||
|
||||
// Intentar obtener de la API de CIMA (endpoint individual puede no existir)
|
||||
console.log(`🌐 Fetching medicine details from CIMA: ${nregistro}`);
|
||||
try {
|
||||
const response = await axios.get(`${CIMA_API_BASE_URL}/medicamento/${nregistro}`, {
|
||||
timeout: 5000
|
||||
});
|
||||
const response = await trackedCimaGet(`${CIMA_API_BASE_URL}/medicamento/${nregistro}`);
|
||||
|
||||
if (response.data) {
|
||||
const med = response.data;
|
||||
@@ -185,9 +207,8 @@ export async function getMedicineDetails(nregistro) {
|
||||
}
|
||||
|
||||
// Fallback: buscar por nregistro usando el endpoint de búsqueda
|
||||
const searchResponse = await axios.get(`${CIMA_API_BASE_URL}/medicamentos`, {
|
||||
const searchResponse = await trackedCimaGet(`${CIMA_API_BASE_URL}/medicamentos`, {
|
||||
params: { nregistro },
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
if (searchResponse.data?.resultados?.length > 0) {
|
||||
@@ -233,7 +254,7 @@ export async function getMedicineDetails(nregistro) {
|
||||
|
||||
/**
|
||||
* Limpia el caché de búsquedas (útil para testing o mantenimiento)
|
||||
* @param {string} pattern - Patrón de claves a eliminar (ej: 'medicines:search:*')
|
||||
* @param {string} pattern - Patrón de claves a eliminar (ej: 'medicines:*')
|
||||
* @returns {Promise<number>} - Número de claves eliminadas
|
||||
*/
|
||||
export async function clearCache(pattern = 'medicines:*') {
|
||||
|
||||
Reference in New Issue
Block a user