135 lines
5.8 KiB
JavaScript
135 lines
5.8 KiB
JavaScript
"use strict";
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
};
|
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
};
|
|
var UsageService_1;
|
|
var _a;
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.UsageService = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
const typeorm_2 = require("typeorm");
|
|
const usage_entity_1 = require("./usage.entity");
|
|
const redis_service_1 = require("../common/redis.service");
|
|
let UsageService = UsageService_1 = class UsageService {
|
|
usageRepository;
|
|
redisService;
|
|
logger = new common_1.Logger(UsageService_1.name);
|
|
constructor(usageRepository, redisService) {
|
|
this.usageRepository = usageRepository;
|
|
this.redisService = redisService;
|
|
}
|
|
async recordRequest(data) {
|
|
this.usageRepository.save(data).catch((err) => {
|
|
this.logger.error(`Failed to save usage log to DB: ${err.message}`);
|
|
});
|
|
const monthKey = this.getMonthlyKey(data.tenantId);
|
|
try {
|
|
await this.redisService.getClient().incr(monthKey);
|
|
}
|
|
catch (err) {
|
|
this.logger.error(`Failed to increment Redis usage counter: ${err.message}`);
|
|
}
|
|
}
|
|
async getMonthlyUsage(tenantId) {
|
|
const key = this.getMonthlyKey(tenantId);
|
|
const val = await this.redisService.getClient().get(key);
|
|
return val ? parseInt(val, 10) : 0;
|
|
}
|
|
async checkQuota(tenantId, limit) {
|
|
const used = await this.getMonthlyUsage(tenantId);
|
|
return {
|
|
allowed: used < limit,
|
|
used,
|
|
};
|
|
}
|
|
async checkRateLimit(tenantId, limit) {
|
|
const key = this.getRateLimitKey(tenantId);
|
|
try {
|
|
const client = this.redisService.getClient();
|
|
const usedRaw = await client.get(key);
|
|
const used = usedRaw ? parseInt(usedRaw, 10) : 0;
|
|
if (used >= limit) {
|
|
return { allowed: false, used };
|
|
}
|
|
const multi = client.multi();
|
|
multi.incr(key);
|
|
if (!usedRaw) {
|
|
multi.expire(key, 60);
|
|
}
|
|
await multi.exec();
|
|
return { allowed: true, used: used + 1 };
|
|
}
|
|
catch (err) {
|
|
this.logger.error(`Rate limit check failed for ${tenantId}: ${err.message}`);
|
|
return { allowed: true, used: 0 };
|
|
}
|
|
}
|
|
async getUsageHistory(tenantId, days = 30) {
|
|
return this.usageRepository
|
|
.createQueryBuilder('usage')
|
|
.select("DATE_TRUNC('day', usage.createdAt)", 'date')
|
|
.addSelect('COUNT(*)', 'count')
|
|
.where('usage.tenantId = :tenantId', { tenantId })
|
|
.andWhere("usage.createdAt >= NOW() - (:days || ' days')::INTERVAL", { days: days.toString() })
|
|
.groupBy("DATE_TRUNC('day', usage.createdAt)")
|
|
.orderBy('date', 'ASC')
|
|
.getRawMany();
|
|
}
|
|
async getUsageSummary(tenantId, plan = 'FREE') {
|
|
const monthlyUsage = await this.getMonthlyUsage(tenantId);
|
|
const QUOTA_LIMITS = {
|
|
'FREE': 8000,
|
|
'PRO': 50000,
|
|
'ENTERPRISE': 1000000
|
|
};
|
|
const monthlyLimit = QUOTA_LIMITS[plan] || 8000;
|
|
const stats = await this.usageRepository
|
|
.createQueryBuilder('usage')
|
|
.select('COUNT(*)', 'totalToday')
|
|
.addSelect('AVG(usage.responseTimeMs)', 'avgLatency')
|
|
.addSelect('COUNT(CASE WHEN usage.statusCode >= 200 AND usage.statusCode < 300 THEN 1 END)', 'successCount')
|
|
.where('usage.tenantId = :tenantId', { tenantId })
|
|
.andWhere('usage.createdAt >= CURRENT_DATE')
|
|
.getRawOne();
|
|
const totalToday = parseInt(stats.totalToday || '0', 10);
|
|
const avgLatency = Math.round(parseFloat(stats.avgLatency || '0'));
|
|
const successRate = totalToday > 0
|
|
? Math.round((parseInt(stats.successCount || '0', 10) / totalToday) * 100)
|
|
: 100;
|
|
return {
|
|
monthlyUsage,
|
|
monthlyLimit,
|
|
totalToday,
|
|
avgLatency,
|
|
successRate,
|
|
lastUpdated: new Date()
|
|
};
|
|
}
|
|
getMonthlyKey(tenantId) {
|
|
const now = new Date();
|
|
const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
return `usage:${tenantId}:${yearMonth}`;
|
|
}
|
|
getRateLimitKey(tenantId) {
|
|
const now = new Date();
|
|
const window = `${now.getFullYear()}${now.getMonth()}${now.getDate()}${now.getHours()}${now.getMinutes()}`;
|
|
return `ratelimit:${tenantId}:${window}`;
|
|
}
|
|
};
|
|
exports.UsageService = UsageService;
|
|
exports.UsageService = UsageService = UsageService_1 = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__param(0, (0, typeorm_1.InjectRepository)(usage_entity_1.UsageLog)),
|
|
__metadata("design:paramtypes", [typeof (_a = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _a : Object, redis_service_1.RedisService])
|
|
], UsageService);
|
|
//# sourceMappingURL=usage.service.js.map
|