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/keysDELETE /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,
};
};