backend/src/services/observability.service.js

/**
 * @file Servicio de observabilidad y monitoreo de la aplicación
 * @description Proporciona funcionalidades de logging, métricas Prometheus y instrumentación MongoDB
 * @requires prom-client - Client de Prometheus para métricas
 * @requires winston - Logger estructurado
 * @requires winston-daily-rotate-file - Plugin para rotación de logs
 */

const client = require('prom-client');
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const path = require('path');

/**
 * Configuración de Prometheus Metrics
 */
const register = new client.Registry();

// Métricas HTTP
const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register]
});

const httpRequestCount = new client.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register]
});

const httpErrorCount = new client.Counter({
  name: 'http_errors_total',
  help: 'Total number of HTTP errors',
  labelNames: ['method', 'route', 'error_type'],
  registers: [register]
});

// Métricas de autenticación
const authAttempts = new client.Counter({
  name: 'auth_attempts_total',
  help: 'Total authentication attempts',
  labelNames: ['endpoint', 'result'],
  registers: [register]
});

// Métricas de base de datos
const mongoQueryDuration = new client.Histogram({
  name: 'mongo_query_duration_seconds',
  help: 'Duration of MongoDB queries in seconds',
  labelNames: ['operation', 'collection'],
  registers: [register],
  buckets: [0.001, 0.01, 0.1, 0.5, 1, 2, 5]
});

const mongoQueryCount = new client.Counter({
  name: 'mongo_queries_total',
  help: 'Total number of MongoDB queries',
  labelNames: ['operation', 'collection', 'status'],
  registers: [register]
});

/**
 * Configuración de Winston Logger
 */
const logsDir = path.join(__dirname, '..', 'logs');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'mindcare-api' },
  transports: [
    // Console output en desarrollo
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.printf(({ timestamp, level, message, ...meta }) => {
          const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
          return `${timestamp} [${level}]: ${message} ${metaStr}`;
        })
      )
    })
  ]
});

// Agregar rotación de logs en producción
if (process.env.NODE_ENV === 'production') {
  logger.add(new DailyRotateFile({
    filename: path.join(logsDir, 'application-%DATE%.log'),
    datePattern: 'YYYY-MM-DD',
    maxSize: '20m',
    maxDays: '14d',
    format: winston.format.json()
  }));

  logger.add(new DailyRotateFile({
    filename: path.join(logsDir, 'error-%DATE%.log'),
    level: 'error',
    datePattern: 'YYYY-MM-DD',
    maxSize: '20m',
    maxDays: '30d',
    format: winston.format.json()
  }));
}

/**
 * Middleware para registrar métricas HTTP
 * @param {Object} req - Objeto de request de Express
 * @param {Object} res - Objeto de response de Express
 * @param {Function} next - Función next de Express
 */
const metricsMiddleware = (req, res, next) => {
  const start = Date.now();

  // Patrones de rutas que se deben ignorar
  const ignorePaths = ['/api/metrics', '/api/health', '/health'];
  if (ignorePaths.includes(req.path)) {
    return next();
  }

  // Interceptar el método send para capturar status code
  const originalSend = res.send;
  res.send = function(data) {
    const duration = (Date.now() - start) / 1000;
    const statusCode = res.statusCode;
    const route = req.route?.path || req.path;
    const method = req.method;

    httpRequestDuration.observe(
      { method, route, status_code: statusCode },
      duration
    );
    httpRequestCount.inc({
      method,
      route,
      status_code: statusCode
    });

    if (statusCode >= 400) {
      httpErrorCount.inc({
        method,
        route,
        error_type: statusCode >= 500 ? 'server_error' : 'client_error'
      });
    }

    logger.info('HTTP Request', {
      method,
      path: req.path,
      statusCode,
      duration: `${duration.toFixed(3)}s`,
      ip: req.ip
    });

    return originalSend.call(this, data);
  };

  next();
};

/**
 * Instrumentación para MongoDB con Mongoose
 * @param {Object} schema - Schema de Mongoose
 */
const instrumentMongoDB = (schema) => {
  schema.pre(/.*/, function(next) {
    this._startTime = Date.now();
    next();
  });

  schema.post(/.*/, function() {
    if (this._startTime) {
      const duration = (Date.now() - this._startTime) / 1000;
      const operation = this.op;
      const collection = this.model?.collection?.name || 'unknown';

      mongoQueryDuration.observe(
        { operation, collection },
        duration
      );

      mongoQueryCount.inc({
        operation,
        collection,
        status: 'success'
      });

      logger.debug('MongoDB Query', {
        operation,
        collection,
        duration: `${duration.toFixed(3)}s`
      });
    }
  });

  schema.post(/.*/, function(error, doc, next) {
    if (error) {
      const collection = this.model?.collection?.name || 'unknown';
      const operation = this.op;

      mongoQueryCount.inc({
        operation,
        collection,
        status: 'error'
      });

      logger.error('MongoDB Query Error', {
        operation,
        collection,
        error: error.message
      });

      next(error);
    } else {
      next();
    }
  });
};

/**
 * Registrar intento de autenticación
 * @param {string} endpoint - Endpoint de autenticación
 * @param {string} result - Resultado ('success' o 'failure')
 */
const logAuthAttempt = (endpoint, result) => {
  authAttempts.inc({ endpoint, result });
  logger.info('Authentication Attempt', { endpoint, result });
};

/**
 * Obtener métricas en formato Prometheus
 * @returns {Promise<string>} Métricas formateadas
 */
const getMetrics = async () => {
  return await register.metrics();
};

module.exports = {
  logger,
  metricsMiddleware,
  instrumentMongoDB,
  logAuthAttempt,
  getMetrics,
  register,
  // Exportar métricas individuales para acceso directo
  httpRequestDuration,
  httpRequestCount,
  httpErrorCount,
  authAttempts,
  mongoQueryDuration,
  mongoQueryCount
};