Skip to content

Nuxt Render Cache предоставляет полный REST API для управления кешем. API позволяет мониторить состояние кеша, управлять ключами, очищать данные по тегам и получать статистику.

Аутентификация

Все API endpoints защищены токеном аутентификации. Токен должен передаваться в заголовке x-render-cache-api.

bash
# Установка токена в переменную окружения
export RENDER_CACHE_API_TOKEN="your-super-secret-token"

# Или установка в nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    renderCacheApiToken: process.env.RENDER_CACHE_API_TOKEN
  }
})

Базовые принципы

Формат запросов

bash
# Все запросы к API должны содержать токен
curl -H "x-render-cache-api: your-token" \
     http://localhost:3000/api/render-cache/keys

Формат ответов

json
{
  "success": true,
  "data": "...",
  "error": null
}

Обработка ошибок

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request parameters"
  }
}

Управление кешем

GET /api/render-cache/keys

Получить список всех ключей кеша и связанных тегов.

Запрос

bash
curl -H "x-render-cache-api: your-token" \
     http://localhost:3000/api/render-cache/keys

Ответ

json
{
  "keys": ["page:home", "page:about", "component:header", "api:products:list"],
  "tags": ["page", "component", "api", "home", "about", "header", "products"],
  "count": 4,
  "success": true
}

Примеры использования

typescript
// Получение списка ключей
const response = await $fetch('/api/render-cache/keys', {
  headers: { 'x-render-cache-api': token },
});

console.log(`Всего ключей: ${response.count}`);
console.log('Теги:', response.tags);
bash
# Использование с jq для красивого вывода
curl -H "x-render-cache-api: token" \
     http://localhost:3000/api/render-cache/keys | jq '.'

DELETE /api/render-cache/keys

Удалить ключи кеша по тегам.

Запрос с query параметрами

bash
curl -X DELETE \
     -H "x-render-cache-api: your-token" \
     "http://localhost:3000/api/render-cache/keys?tags=page,home"

Запрос с телом

bash
curl -X DELETE \
     -H "x-render-cache-api: your-token" \
     -H "Content-Type: application/json" \
     -d '{"tags": ["page", "home"]}' \
     http://localhost:3000/api/render-cache/keys

Ответ

json
{
  "tags": ["page", "home"],
  "deletedCount": 3,
  "success": true
}

Примеры использования

typescript
// Удаление по тегам через query параметры
const response = await $fetch('/api/render-cache/keys?tags=user,profile', {
  method: 'DELETE',
  headers: { 'x-render-cache-api': token },
});

// Удаление по тегам через тело запроса
const response = await $fetch('/api/render-cache/keys', {
  method: 'DELETE',
  headers: {
    'x-render-cache-api': token,
    'Content-Type': 'application/json',
  },
  body: { tags: ['product', 'catalog'] },
});

console.log(`Удалено ${response.deletedCount} ключей`);
bash
# Очистка кеша для определенной функциональности
curl -X DELETE \
     -H "x-render-cache-api: token" \
     "http://localhost:3000/api/render-cache/keys?tags=blog,posts"

# Массовое удаление нескольких типов контента
curl -X DELETE \
     -H "x-render-cache-api: token" \
     -H "Content-Type: application/json" \
     -d '{"tags": ["user", "profile", "settings"]}' \
     http://localhost:3000/api/render-cache/keys

DELETE /api/render-cache/clear

Очистить весь кеш полностью.

Запрос

bash
curl -X DELETE \
     -H "x-render-cache-api: your-token" \
     http://localhost:3000/api/render-cache/clear

Ответ

json
{
  "cleared": true,
  "deletedCount": 25,
  "success": true
}

Примеры использования

typescript
// Полная очистка кеша
const response = await $fetch('/api/render-cache/clear', {
  method: 'DELETE',
  headers: { 'x-render-cache-api': token },
});

console.log(`Очищено ${response.deletedCount} ключей`);
bash
# Полная очистка кеша для тестирования
curl -X DELETE \
     -H "x-render-cache-api: token" \
     http://localhost:3000/api/render-cache/clear

# Проверка очистки
curl -H "x-render-cache-api: token" \
     http://localhost:3000/api/render-cache/keys | jq '.count'

API ключи

GET /api/render-cache/keys/[key]

Получить информацию о конкретном ключе кеша.

Запрос

bash
curl -H "x-render-cache-api: your-token" \
     http://localhost:3000/api/render-cache/keys/page:home

Ответ (ключ найден)

json
{
  "key": "page:home",
  "data": "<div><h1>Главная страница</h1>...</div>",
  "timestamp": 1703123456789,
  "tags": ["page", "home", "public"],
  "exists": true,
  "success": true
}

Ответ (ключ не найден)

json
{
  "key": "page:nonexistent",
  "exists": false,
  "success": true
}

DELETE /api/render-cache/keys/[key]

Удалить конкретный ключ из кеша.

Запрос

bash
curl -X DELETE \
     -H "x-render-cache-api: your-token" \
     http://localhost:3000/api/render-cache/keys/page:home

Ответ

json
{
  "key": "page:home",
  "deleted": true,
  "success": true
}

Статистика

GET /api/render-cache/stats

Получить статистику Redis и кеша.

Запрос

bash
curl -H "x-render-cache-api: your-token" \
     http://localhost:3000/api/render-cache/stats

Ответ

json
{
  "totalKeys": 15,
  "redisInfo": {
    "redis_version": "7.0.0",
    "connected_clients": "3",
    "used_memory": "2048",
    "total_connections_received": "150",
    "uptime_in_seconds": "3600",
    "keyspace_hits": "1250",
    "keyspace_misses": "45"
  },
  "success": true
}

Примеры использования

typescript
// Получение статистики
const stats = await $fetch('/api/render-cache/stats', {
  headers: { 'x-render-cache-api': token },
});

console.log(`Всего ключей: ${stats.totalKeys}`);
console.log(`Используемая память: ${stats.redisInfo.used_memory} bytes`);
console.log(`Попаданий в кеш: ${stats.redisInfo.keyspace_hits}`);
console.log(`Промахов кеша: ${stats.redisInfo.keyspace_misses}`);
bash
# Мониторинг состояния Redis
curl -H "x-render-cache-api: token" \
     http://localhost:3000/api/render-cache/stats | jq '.redisInfo'

# Проверка количества ключей
curl -H "x-render-cache-api: token" \
     http://localhost:3000/api/render-cache/stats | jq '.totalKeys'

Расширенные примеры

Скрипт для мониторинга

typescript
interface CacheStats {
  totalKeys: number;
  redisInfo: Record<string, string>;
}

class CacheMonitor {
  private readonly apiUrl: string;
  private readonly token: string;

  constructor(apiUrl: string, token: string) {
    this.apiUrl = apiUrl;
    this.token = token;
  }

  async getStats(): Promise<CacheStats> {
    const response = await $fetch('/api/render-cache/stats', {
      baseURL: this.apiUrl,
      headers: { 'x-render-cache-api': this.token },
    });
    return response;
  }

  async getKeys(): Promise<string[]> {
    const response = await $fetch('/api/render-cache/keys', {
      baseURL: this.apiUrl,
      headers: { 'x-render-cache-api': this.token },
    });
    return response.keys;
  }

  async clearCache(): Promise<number> {
    const response = await $fetch('/api/render-cache/clear', {
      baseURL: this.apiUrl,
      method: 'DELETE',
      headers: { 'x-render-cache-api': this.token },
    });
    return response.deletedCount;
  }

  async invalidateByTags(tags: string[]): Promise<number> {
    const response = await $fetch('/api/render-cache/keys', {
      baseURL: this.apiUrl,
      method: 'DELETE',
      headers: { 'x-render-cache-api': this.token },
      body: { tags },
    });
    return response.deletedCount;
  }

  // Мониторинг производительности
  async getPerformanceMetrics() {
    const stats = await this.getStats();
    const keys = await this.getKeys();

    const hitRate = this.calculateHitRate(stats.redisInfo);
    const memoryUsage = parseInt(stats.redisInfo.used_memory || '0');

    return {
      totalKeys: stats.totalKeys,
      hitRate: `${hitRate.toFixed(2)}%`,
      memoryUsage: `${(memoryUsage / 1024 / 1024).toFixed(2)} MB`,
      connectedClients: stats.redisInfo.connected_clients,
      uptime: this.formatUptime(stats.redisInfo.uptime_in_seconds),
    };
  }

  private calculateHitRate(redisInfo: Record<string, string>): number {
    const hits = parseInt(redisInfo.keyspace_hits || '0');
    const misses = parseInt(redisInfo.keyspace_misses || '0');
    const total = hits + misses;

    return total > 0 ? (hits / total) * 100 : 0;
  }

  private formatUptime(seconds: string): string {
    const uptime = parseInt(seconds || '0');
    const hours = Math.floor(uptime / 3600);
    const minutes = Math.floor((uptime % 3600) / 60);

    return `${hours}h ${minutes}m`;
  }
}

// Использование
const monitor = new CacheMonitor('http://localhost:3000', 'your-token');

const metrics = await monitor.getPerformanceMetrics();
console.log('Cache Performance Metrics:', metrics);

// Очистка устаревшего контента
await monitor.invalidateByTags(['old', 'deprecated']);

Автоматизация инвалидации

typescript
export default defineEventHandler(async (event) => {
  const { tags, keys, pattern } = await readBody(event);

  // Валидация токена
  const authToken = getHeader(event, 'x-render-cache-api');
  if (authToken !== process.env.RENDER_CACHE_API_TOKEN) {
    throw createError({
      statusCode: 403,
      statusMessage: 'Forbidden: Invalid token',
    });
  }

  try {
    const cache = useCache();
    let totalDeleted = 0;

    // Инвалидация по тегам
    if (tags && Array.isArray(tags)) {
      totalDeleted += await cache.deleteByTags(tags);
    }

    // Инвалидация конкретных ключей
    if (keys && Array.isArray(keys)) {
      for (const key of keys) {
        totalDeleted += await cache.deleteKey(key);
      }
    }

    // Инвалидация по паттерну (расширенная функциональность)
    if (pattern) {
      const allKeys = await cache.getAllKeys();
      const matchingKeys = allKeys.filter((key) => key.includes(pattern));

      for (const key of matchingKeys) {
        totalDeleted += await cache.deleteKey(key);
      }
    }

    return {
      success: true,
      deletedCount: totalDeleted,
      message: `Successfully invalidated ${totalDeleted} cache entries`,
    };
  } catch (error) {
    console.error('Cache invalidation error:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Internal Server Error: Failed to invalidate cache',
    });
  }
});

Скрипт для резервного копирования

typescript
interface CacheEntry {
  key: string;
  data: string;
  timestamp: number;
  tags: string[];
}

class CacheBackup {
  private readonly apiUrl: string;
  private readonly token: string;

  constructor(apiUrl: string, token: string) {
    this.apiUrl = apiUrl;
    this.token = token;
  }

  async createBackup(): Promise<CacheEntry[]> {
    const keys = await this.getAllKeys();

    const backup: CacheEntry[] = [];

    for (const key of keys) {
      try {
        const entry = await this.getKeyData(key);
        if (entry) {
          backup.push({
            key,
            data: entry.data,
            timestamp: entry.timestamp,
            tags: entry.tags,
          });
        }
      } catch (error) {
        console.warn(`Failed to backup key ${key}:`, error);
      }
    }

    return backup;
  }

  async restoreBackup(backup: CacheEntry[]): Promise<void> {
    const cache = useCache();

    for (const entry of backup) {
      try {
        await cache.set(
          entry.key,
          {
            data: entry.data,
            timestamp: entry.timestamp,
            tags: entry.tags,
          },
          3600
        ); // Восстанавливаем с TTL 1 час
      } catch (error) {
        console.warn(`Failed to restore key ${entry.key}:`, error);
      }
    }
  }

  private async getAllKeys(): Promise<string[]> {
    const response = await $fetch('/api/render-cache/keys', {
      baseURL: this.apiUrl,
      headers: { 'x-render-cache-api': this.token },
    });
    return response.keys;
  }

  private async getKeyData(key: string) {
    const response = await $fetch(`/api/render-cache/keys/${key}`, {
      baseURL: this.apiUrl,
      headers: { 'x-render-cache-api': this.token },
    });
    return response.exists ? response : null;
  }
}

// Использование
const backupManager = new CacheBackup('http://localhost:3000', 'your-token');

// Создание резервной копии
const backup = await backupManager.createBackup();
console.log(`Created backup of ${backup.length} cache entries`);

// Сохранение в файл
await writeFile('cache-backup.json', JSON.stringify(backup, null, 2));

// Восстановление из файла
const backupData = JSON.parse(await readFile('cache-backup.json', 'utf-8'));
await backupManager.restoreBackup(backupData);
console.log('Cache backup restored');

Безопасность

Защита от несанкционированного доступа

typescript
export default defineEventHandler((event) => {
  // Проверяем что это запрос к API кеша
  const url = getRequestURL(event);
  if (!url.pathname.startsWith('/api/render-cache/')) {
    return;
  }

  // Проверяем токен
  const authToken = getHeader(event, 'x-render-cache-api');
  if (!authToken) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Unauthorized: Missing x-render-cache-api header',
    });
  }

  // Проверяем валидность токена
  if (authToken !== process.env.RENDER_CACHE_API_TOKEN) {
    throw createError({
      statusCode: 403,
      statusMessage: 'Forbidden: Invalid token',
    });
  }

  // Rate limiting
  const clientIP = getClientIP(event);
  const rateLimitKey = `ratelimit:api:${clientIP}`;

  // Проверяем количество запросов (не более 100 в минуту)
  const requestCount = await useCache().get(rateLimitKey);
  if (requestCount && parseInt(requestCount.data) > 100) {
    throw createError({
      statusCode: 429,
      statusMessage: 'Too Many Requests: Rate limit exceeded',
    });
  }

  // Увеличиваем счетчик
  const newCount = (requestCount ? parseInt(requestCount.data) : 0) + 1;
  await useCache().set(rateLimitKey, { data: newCount.toString() }, 60);
});

Валидация входных данных

typescript
export const validateCacheTags = (tags: any): string[] => {
  if (!Array.isArray(tags)) {
    throw new Error('Tags must be an array');
  }

  if (tags.length === 0) {
    throw new Error('At least one tag is required');
  }

  if (tags.length > 10) {
    throw new Error('Maximum 10 tags allowed');
  }

  const validTags = tags.filter((tag: any) => {
    if (typeof tag !== 'string') {
      return false;
    }

    if (tag.length === 0 || tag.length > 50) {
      return false;
    }

    // Проверяем на допустимые символы
    if (!/^[a-zA-Z0-9:_-]+$/.test(tag)) {
      return false;
    }

    return true;
  });

  if (validTags.length !== tags.length) {
    throw new Error('Some tags contain invalid characters or are too long');
  }

  return validTags;
};

export const validateCacheKey = (key: any): string => {
  if (typeof key !== 'string') {
    throw new Error('Key must be a string');
  }

  if (key.length === 0) {
    throw new Error('Key cannot be empty');
  }

  if (key.length > 200) {
    throw new Error('Key is too long (max 200 characters)');
  }

  // Проверяем на допустимые символы
  if (!/^[a-zA-Z0-9:_-]+$/.test(key)) {
    throw new Error('Key contains invalid characters');
  }

  return key;
};

Мониторинг и алертинг

Система мониторинга

typescript
export default defineEventHandler(async (event) => {
  // Проверяем токен
  const authToken = getHeader(event, 'x-render-cache-api');
  if (authToken !== process.env.RENDER_CACHE_API_TOKEN) {
    throw createError({
      statusCode: 403,
      statusMessage: 'Forbidden',
    });
  }

  try {
    const cache = useCache();
    const stats = await cache.getStats();
    const keys = await cache.getAllKeys();

    // Проверяем здоровье системы
    const health = {
      redis: {
        connected: true,
        version: stats.redisInfo.redis_version,
        uptime: stats.redisInfo.uptime_in_seconds,
        memory: {
          used: parseInt(stats.redisInfo.used_memory || '0'),
          peak: parseInt(stats.redisInfo.used_memory_peak || '0'),
        },
        connections: {
          current: parseInt(stats.redisInfo.connected_clients || '0'),
          total: parseInt(stats.redisInfo.total_connections_received || '0'),
        },
      },
      cache: {
        totalKeys: keys.length,
        hitRate: calculateHitRate(stats.redisInfo),
        memoryUsage: parseInt(stats.redisInfo.used_memory || '0'),
      },
    };

    // Проверяем пороговые значения
    const alerts = [];

    if (health.cache.hitRate < 80) {
      alerts.push({
        level: 'warning',
        message: `Low cache hit rate: ${health.cache.hitRate.toFixed(2)}%`,
      });
    }

    if (health.redis.memory.used > 500 * 1024 * 1024) {
      // 500MB
      alerts.push({
        level: 'critical',
        message: `High memory usage: ${(
          health.redis.memory.used /
          1024 /
          1024
        ).toFixed(2)} MB`,
      });
    }

    return {
      success: true,
      health,
      alerts,
      timestamp: Date.now(),
    };
  } catch (error) {
    console.error('Health check failed:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Health check failed',
    });
  }
});

function calculateHitRate(redisInfo: Record<string, string>): number {
  const hits = parseInt(redisInfo.keyspace_hits || '0');
  const misses = parseInt(redisInfo.keyspace_misses || '0');
  const total = hits + misses;

  return total > 0 ? (hits / total) * 100 : 0;
}

Скрипт для регулярного мониторинга

bash
#!/bin/bash

API_URL="http://localhost:3000"
TOKEN="your-token"
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"

# Функция для отправки алертов в Slack
send_alert() {
    local level=$1
    local message=$2

    curl -X POST -H 'Content-type: application/json' \
         --data "{\"text\":\"Cache Alert [$level]: $message\"}" \
         $SLACK_WEBHOOK
}

# Основной цикл мониторинга
while true; do
    # Получаем статистику
    response=$(curl -s -H "x-render-cache-api: $TOKEN" $API_URL/api/render-cache/stats)

    if [ $? -ne 0 ]; then
        send_alert "CRITICAL" "Cannot connect to cache API"
        sleep 60
        continue
    fi

    # Проверяем основные метрики
    total_keys=$(echo $response | jq '.totalKeys')
    used_memory=$(echo $response | jq '.redisInfo.used_memory')

    # Конвертируем память в MB
    memory_mb=$((used_memory / 1024 / 1024))

    echo "$(date): Keys: $total_keys, Memory: ${memory_mb}MB"

    # Проверяем пороговые значения
    if [ $memory_mb -gt 800 ]; then
        send_alert "CRITICAL" "Cache memory usage is ${memory_mb}MB (threshold: 800MB)"
    elif [ $memory_mb -gt 600 ]; then
        send_alert "WARNING" "Cache memory usage is ${memory_mb}MB (threshold: 600MB)"
    fi

    if [ $total_keys -gt 10000 ]; then
        send_alert "WARNING" "Too many cache keys: $total_keys (threshold: 10000)"
    fi

    sleep 300  # Проверяем каждые 5 минут
done

Лучшие практики

1. Регулярная очистка

typescript
// Автоматическая очистка устаревших данных
export const useCacheMaintenance = () => {
  const cache = useCache();

  const cleanupExpiredEntries = async () => {
    const keys = await cache.getAllKeys();
    let cleaned = 0;

    for (const key of keys) {
      // Проверяем TTL 1 час для обслуживания
      if (await cache.expired(key, 3600)) {
        await cache.deleteKey(key);
        cleaned++;
      }
    }

    console.log(`Cleaned ${cleaned} expired entries`);
    return cleaned;
  };

  const cleanupByPattern = async (pattern: string, maxAge: number) => {
    const keys = await cache.getAllKeys();
    const matchingKeys = keys.filter((key) => key.includes(pattern));
    let cleaned = 0;

    for (const key of matchingKeys) {
      if (await cache.expired(key, maxAge)) {
        await cache.deleteKey(key);
        cleaned++;
      }
    }

    console.log(`Cleaned ${cleaned} entries matching pattern "${pattern}"`);
    return cleaned;
  };

  return {
    cleanupExpiredEntries,
    cleanupByPattern,
  };
};

2. Мониторинг производительности

typescript
// Мониторинг hit rate и других метрик
export const useCacheMetrics = () => {
  const cache = useCache();

  const getDetailedStats = async () => {
    const stats = await cache.getStats();
    const keys = await cache.getAllKeys();

    // Анализ распределения тегов
    const tagDistribution: Record<string, number> = {};
    const keyAges: number[] = [];

    for (const key of keys.slice(0, 100)) {
      // Анализируем первые 100 ключей
      const entry = await cache.get(key);
      if (entry) {
        // Считаем теги
        entry.tags.forEach((tag) => {
          tagDistribution[tag] = (tagDistribution[tag] || 0) + 1;
        });

        // Считаем возраст
        const age = Date.now() - entry.timestamp;
        keyAges.push(age);
      }
    }

    // Вычисляем средний возраст
    const avgAge =
      keyAges.length > 0
        ? keyAges.reduce((a, b) => a + b, 0) / keyAges.length
        : 0;

    return {
      basic: stats,
      tagDistribution,
      averageKeyAge: avgAge,
      keySampleSize: Math.min(keys.length, 100),
      recommendations: generateRecommendations(stats, tagDistribution, avgAge),
    };
  };

  const generateRecommendations = (
    stats: any,
    tagDistribution: Record<string, number>,
    avgAge: number
  ) => {
    const recommendations = [];

    // Рекомендации по hit rate
    const hitRate = calculateHitRate(stats.redisInfo);
    if (hitRate < 80) {
      recommendations.push(
        'Consider increasing TTL values to improve hit rate'
      );
    }

    // Рекомендации по памяти
    const memoryUsage = parseInt(stats.redisInfo.used_memory || '0');
    if (memoryUsage > 500 * 1024 * 1024) {
      recommendations.push(
        'High memory usage detected, consider cleanup or Redis configuration'
      );
    }

    // Рекомендации по возрасту ключей
    if (avgAge > 3600000) {
      // 1 час
      recommendations.push(
        'Keys are getting old, consider reducing TTL values'
      );
    }

    return recommendations;
  };

  return {
    getDetailedStats,
  };
};

3. Автоматизация операций

typescript
// Планировщик автоматических операций с кешем
export const useCacheScheduler = () => {
  const maintenance = useCacheMaintenance();
  const metrics = useCacheMetrics();

  const scheduleMaintenance = () => {
    // Очистка каждый час
    setInterval(async () => {
      try {
        const cleaned = await maintenance.cleanupExpiredEntries();
        if (cleaned > 0) {
          console.log(`Scheduled cleanup: removed ${cleaned} expired entries`);
        }
      } catch (error) {
        console.error('Scheduled cleanup failed:', error);
      }
    }, 60 * 60 * 1000); // Каждый час

    // Детальная статистика каждый день
    setInterval(async () => {
      try {
        const stats = await metrics.getDetailedStats();
        console.log('Daily cache statistics:', stats);

        // Отправка статистики в систему мониторинга
        await sendMetricsToMonitoring(stats);
      } catch (error) {
        console.error('Daily statistics failed:', error);
      }
    }, 24 * 60 * 60 * 1000); // Каждый день
  };

  const sendMetricsToMonitoring = async (stats: any) => {
    // Отправка в систему мониторинга (Datadog, New Relic, etc.)
    // Реализация зависит от используемой системы мониторинга
    console.log('Metrics sent to monitoring system');
  };

  return {
    scheduleMaintenance,
  };
};

Создано с ❤️ для Nuxt сообщества