Skip to main content

Overview

Monitoring the MND system ensures reliability, tracks API quota usage, and helps identify issues before they affect users.

Health Check Endpoint

The backend exposes a health check endpoint for uptime monitoring.

Endpoint

GET /api/health

Response

{
  "status": "ok",
  "uptime": 12345.67,
  "timestamp": "2026-03-05T10:30:00.000Z"
}

Implementation

In src/api/routesController.ts:
export function healthCheck(req: Request, res: Response) {
  res.json({
    status: 'ok',
    uptime: process.uptime(),
    timestamp: new Date().toISOString()
  });
}

Monitoring Services

Use these services to ping /api/health periodically: UptimeRobot (Free tier):
  • Monitor up to 50 endpoints
  • 5-minute interval checks
  • Email/SMS alerts
Setup:
  1. Go to uptimerobot.com
  2. Add New Monitor
  3. Type: HTTP(s)
  4. URL: https://yourdomain.com/api/health
  5. Interval: 5 minutes
  6. Add alert contacts
Better Uptime:
  • 3-minute interval
  • Incident management
  • Status page
Pingdom:
  • 1-minute interval
  • Performance monitoring
  • Real user monitoring

API Usage Tracking

The Distance Matrix client includes built-in usage tracking.

Usage Statistics

Access stats via the singleton instance:
import { distanceMatrixClient } from './infra/distanceMatrixClient';

const stats = distanceMatrixClient.getStats();
console.log(stats);
Output:
{
  monthlyCount: 45,
  dailyCount: 12,
  lastReset: '2026-03-05',
  cacheHits: 234,
  cacheMisses: 45
}
distanceMatrixClient.printStats();
Console output:
📊 Distance Matrix Usage:
  Monthly: 45/700
  Daily: 12/50
  Cache hits: 234
  Cache misses: 45
  Cache size: 156 entries

Usage Limits

Configured in src/infra/distanceMatrixClient.ts:
private readonly MONTHLY_LIMIT = 700;
private readonly DAILY_LIMIT = 50;
private readonly CACHE_TTL_DAYS = 7;

Quota Warnings

The client logs warnings when approaching limits:
if (this.usageStats.monthlyCount >= this.MONTHLY_LIMIT) {
  console.warn(`⚠️  Monthly limit reached (${this.MONTHLY_LIMIT})`);
  return {
    ok: false,
    errorMessage: 'Monthly API quota exceeded'
  };
}

Distance Matrix Quota Management

Google Distance Matrix API has strict quota limits.

Free Tier Limits

  • $200 free credit/month = ~700 requests
  • After that: $5-10 per 1000 requests

Quota Monitoring Endpoint

Create an endpoint to expose quota stats:
// src/api/routesController.ts
export function getQuotaStats(req: Request, res: Response) {
  const stats = distanceMatrixClient.getStats();
  
  res.json({
    quota: {
      monthly: {
        used: stats.monthlyCount,
        limit: 700,
        remaining: 700 - stats.monthlyCount,
        percentage: Math.round((stats.monthlyCount / 700) * 100)
      },
      daily: {
        used: stats.dailyCount,
        limit: 50,
        remaining: 50 - stats.dailyCount
      },
      cache: {
        hits: stats.cacheHits,
        misses: stats.cacheMisses,
        hitRate: stats.cacheHits / (stats.cacheHits + stats.cacheMisses)
      }
    },
    lastReset: stats.lastReset
  });
}

// Add to server.ts
app.get('/api/quota', getQuotaStats);
Response:
{
  "quota": {
    "monthly": {
      "used": 45,
      "limit": 700,
      "remaining": 655,
      "percentage": 6
    },
    "daily": {
      "used": 12,
      "limit": 50,
      "remaining": 38
    },
    "cache": {
      "hits": 234,
      "misses": 45,
      "hitRate": 0.84
    }
  },
  "lastReset": "2026-03-05"
}

Quota Alerts

Set up alerts when quota exceeds thresholds:
function checkQuotaAlerts() {
  const stats = distanceMatrixClient.getStats();
  
  const monthlyUsage = (stats.monthlyCount / 700) * 100;
  
  if (monthlyUsage >= 90) {
    sendAlert('CRITICAL: 90% of monthly Distance Matrix quota used!');
  } else if (monthlyUsage >= 75) {
    sendAlert('WARNING: 75% of monthly Distance Matrix quota used');
  }
}

// Run daily
setInterval(checkQuotaAlerts, 24 * 60 * 60 * 1000);

Caching Strategy

The client caches results for 7 days to minimize API calls. Cache configuration:
private cache: Map<string, CacheEntry> = new Map();
private readonly CACHE_TTL_DAYS = 7;

private isCacheValid(entry: CacheEntry): boolean {
  const ageMs = Date.now() - entry.timestamp;
  const ageDays = ageMs / (1000 * 60 * 60 * 24);
  return ageDays < this.CACHE_TTL_DAYS;
}
Cache key format:
const cacheKey = `${originNodeId}|${destNodeId}|${mode}`;
Pre-warm cache to reduce API calls:
const commonRoutes = [
  ['TILAGOR', 'CAMPUS', 'driving'],
  ['CAMPUS', 'TILAGOR', 'driving'],
  ['NAIORPUL', 'CAMPUS', 'driving'],
  // ...
];

await distanceMatrixClient.prewarmCache(commonRoutes);

Error Logging

Request Logging

The server logs all requests:
app.use((req: Request, res: Response, next: NextFunction) => {
  console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
  next();
});
Output:
2026-03-05T10:30:15.123Z GET /api/health
2026-03-05T10:30:20.456Z GET /api/routes?from=TILAGOR&to=CAMPUS&time=08:30
2026-03-05T10:30:25.789Z POST /api/auth/send-link

Error Handler

Global error handler catches unhandled errors:
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error('Unhandled error:', err);
  res.status(500).json({
    error: 'Internal server error',
    message: err.message
  });
});

Structured Logging

For production, use a logging library like Winston:
npm install winston
import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

// Usage
logger.info('Server started', { port: PORT });
logger.error('Distance Matrix API error', { error: err.message });

Performance Metrics

Request Duration

Track API response times:
app.use((req: Request, res: Response, next: NextFunction) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`);
  });
  
  next();
});

Route Planning Performance

Log route calculation times:
export async function planRoute(req: Request, res: Response) {
  const start = Date.now();
  
  // ... route planning logic ...
  
  const duration = Date.now() - start;
  console.log(`Route planning: ${from}${to} took ${duration}ms`);
  
  res.json({ ...result, computeTimeMs: duration });
}

PM2 Monitoring

PM2 provides built-in monitoring.

Real-time Monitoring

pm2 monit
Shows:
  • CPU usage
  • Memory usage
  • Logs (stdout/stderr)

Process List

pm2 list
Output:
┌─────┬──────────────┬─────────┬─────────┬─────────┬──────────┐
│ id  │ name         │ mode    │ ↺       │ status  │ cpu      │
├─────┼──────────────┼─────────┼─────────┼─────────┼──────────┤
│ 0   │ mnd-backend  │ fork    │ 0       │ online  │ 0.3%     │
└─────┴──────────────┴─────────┴─────────┴─────────┴──────────┘

PM2 Plus (Cloud Monitoring)

Free tier:
  • Real-time monitoring
  • Exception tracking
  • Custom metrics
Setup:
pm2 link <secret_key> <public_key>
Get keys from pm2.io

Log Management

View logs:
pm2 logs mnd-backend
Log rotation: Install PM2 log rotate:
pm2 install pm2-logrotate
Configure:
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 30
pm2 set pm2-logrotate:compress true

Database Monitoring

The backend uses JSON file-based storage.

Monitor Data Files

Track file sizes:
du -h src/data/*.json
Output:
4.0K src/data/nodes.json
60K src/data/edges.json
24K src/data/routes.json

User Data

Monitor user database size:
du -h src/data/users.json
If growing too large, consider migrating to a real database (SQLite, PostgreSQL).

Alerting

Email Alerts

Send email alerts for critical events:
import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: process.env.EMAIL_SMTP_HOST,
  port: Number(process.env.EMAIL_SMTP_PORT),
  auth: {
    user: process.env.EMAIL_SMTP_USER,
    pass: process.env.EMAIL_SMTP_PASSWORD
  }
});

async function sendAlert(message: string) {
  await transporter.sendMail({
    from: process.env.EMAIL_FROM,
    to: 'admin@yourdomain.com',
    subject: 'MND Alert',
    text: message
  });
}

// Usage
if (monthlyQuota >= 90) {
  await sendAlert('Distance Matrix quota at 90%!');
}

Slack Alerts

npm install @slack/webhook
import { IncomingWebhook } from '@slack/webhook';

const webhook = new IncomingWebhook(process.env.SLACK_WEBHOOK_URL);

async function sendSlackAlert(message: string) {
  await webhook.send({
    text: message,
    username: 'MND Monitor',
    icon_emoji: ':bus:'
  });
}

Metrics Dashboard

Create a simple metrics endpoint:
app.get('/api/metrics', (req: Request, res: Response) => {
  const stats = distanceMatrixClient.getStats();
  
  res.json({
    server: {
      uptime: process.uptime(),
      memory: process.memoryUsage(),
      nodeVersion: process.version
    },
    distanceMatrix: {
      monthlyUsed: stats.monthlyCount,
      monthlyLimit: 700,
      dailyUsed: stats.dailyCount,
      cacheHitRate: stats.cacheHits / (stats.cacheHits + stats.cacheMisses)
    },
    graph: {
      nodes: graph.getNodes().length,
      edges: graph.getEdges().length
    }
  });
});
Response:
{
  "server": {
    "uptime": 12345.67,
    "memory": {
      "rss": 52428800,
      "heapTotal": 20971520,
      "heapUsed": 15728640
    },
    "nodeVersion": "v18.16.0"
  },
  "distanceMatrix": {
    "monthlyUsed": 45,
    "monthlyLimit": 700,
    "dailyUsed": 12,
    "cacheHitRate": 0.84
  },
  "graph": {
    "nodes": 19,
    "edges": 542
  }
}

Monitoring Checklist

  • Health check endpoint configured
  • Uptime monitoring service added (UptimeRobot, etc.)
  • Distance Matrix quota tracking enabled
  • Quota alerts configured (75%, 90%)
  • Request logging enabled
  • Error logging configured
  • PM2 monitoring active
  • Log rotation configured
  • Performance metrics tracked
  • Alert notifications configured (email/Slack)
  • Metrics dashboard accessible

Next Steps