OhMyDitzzy commited on
Commit
46eb81c
·
1 Parent(s): 67d5038

feat: add upstash/redis for stats database

Browse files
Files changed (2) hide show
  1. package.json +1 -0
  2. src/server/lib/stats-tracker.ts +54 -38
package.json CHANGED
@@ -36,6 +36,7 @@
36
  "@radix-ui/react-separator": "^1.1.8",
37
  "@radix-ui/react-slot": "^1.2.4",
38
  "@radix-ui/react-tabs": "^1.1.13",
 
39
  "axios": "^1.13.2",
40
  "class-variance-authority": "^0.7.1",
41
  "clsx": "^2.1.1",
 
36
  "@radix-ui/react-separator": "^1.1.8",
37
  "@radix-ui/react-slot": "^1.2.4",
38
  "@radix-ui/react-tabs": "^1.1.13",
39
+ "@upstash/redis": "^1.36.1",
40
  "axios": "^1.13.2",
41
  "class-variance-authority": "^0.7.1",
42
  "clsx": "^2.1.1",
src/server/lib/stats-tracker.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { promises as fs } from 'fs';
2
- import { join } from 'path';
3
 
4
  interface EndpointStats {
5
  totalRequests: number;
@@ -18,10 +17,6 @@ interface IPFailureTracking {
18
  resetTime: number;
19
  }
20
 
21
- interface DailyVisitors {
22
- [date: string]: Set<string>;
23
- }
24
-
25
  interface GlobalStats {
26
  totalRequests: number;
27
  totalSuccess: number;
@@ -47,11 +42,11 @@ class StatsTracker {
47
  private ipFailures: Map<string, IPFailureTracking>;
48
  private readonly MAX_FAILS_PER_IP = 1;
49
  private readonly FAIL_WINDOW_MS = 12 * 60 * 60 * 1000;
50
- private readonly STATS_FILE_PATH: string;
51
  private saveTimeout: NodeJS.Timeout | null = null;
 
52
 
53
- constructor(statsFilePath?: string) {
54
- this.STATS_FILE_PATH = statsFilePath || join(process.cwd(), 'stats-data.json');
55
  this.stats = {
56
  totalRequests: 0,
57
  totalSuccess: 0,
@@ -62,6 +57,16 @@ class StatsTracker {
62
  visitorsByDay: new Map(),
63
  };
64
  this.ipFailures = new Map();
 
 
 
 
 
 
 
 
 
 
65
 
66
  setInterval(() => {
67
  const now = Date.now();
@@ -74,41 +79,50 @@ class StatsTracker {
74
  }
75
 
76
  async loadStats(): Promise<void> {
 
 
 
 
 
77
  try {
78
- const data = await fs.readFile(this.STATS_FILE_PATH, 'utf-8');
79
- const parsed: SerializedStats = JSON.parse(data);
80
-
81
- this.stats.totalRequests = parsed.totalRequests || 0;
82
- this.stats.totalSuccess = parsed.totalSuccess || 0;
83
- this.stats.totalFailed = parsed.totalFailed || 0;
84
- this.stats.uniqueVisitors = new Set(parsed.uniqueVisitors || []);
85
- this.stats.startTime = parsed.startTime || Date.now();
 
 
 
 
86
  this.stats.endpoints = new Map();
87
 
88
- if (parsed.endpoints) {
89
- Object.entries(parsed.endpoints).forEach(([endpoint, stats]) => {
90
  this.stats.endpoints.set(endpoint, stats);
91
  });
92
  }
93
 
94
  this.stats.visitorsByDay = new Map();
95
- if (parsed.visitorsByDay) {
96
- Object.entries(parsed.visitorsByDay).forEach(([date, ips]) => {
97
  this.stats.visitorsByDay.set(date, new Set(ips));
98
  });
99
  }
100
 
101
- console.log('Stats loaded successfully from', this.STATS_FILE_PATH);
102
- } catch (error: any) {
103
- if (error.code === 'ENOENT') {
104
- console.log('No existing stats file found, starting fresh');
105
- } else {
106
- console.error('Error loading stats:', error);
107
- }
108
  }
109
  }
110
 
111
  private async saveStats(): Promise<void> {
 
 
 
 
112
  try {
113
  const serialized: SerializedStats = {
114
  totalRequests: this.stats.totalRequests,
@@ -128,13 +142,13 @@ class StatsTracker {
128
  serialized.visitorsByDay[date] = Array.from(ips);
129
  });
130
 
131
- await fs.writeFile(
132
- this.STATS_FILE_PATH,
133
- JSON.stringify(serialized, null, 2),
134
- 'utf-8'
135
- );
136
  } catch (error) {
137
- console.error('Error saving stats:', error);
138
  }
139
  }
140
 
@@ -142,7 +156,7 @@ class StatsTracker {
142
  if (this.saveTimeout) {
143
  clearTimeout(this.saveTimeout);
144
  }
145
-
146
  this.saveTimeout = setTimeout(() => {
147
  this.saveStats();
148
  }, 5000);
@@ -212,7 +226,7 @@ class StatsTracker {
212
  } else if (statusCode >= 500) {
213
  endpointStats.failedRequests++;
214
  }
215
-
216
  this.scheduleSave();
217
 
218
  return true;
@@ -239,6 +253,7 @@ class StatsTracker {
239
  ? `${uptimeDays}d ${uptimeHours % 24}h`
240
  : `${uptimeHours}h`,
241
  },
 
242
  };
243
  }
244
 
@@ -301,13 +316,14 @@ class StatsTracker {
301
  clearTimeout(this.saveTimeout);
302
  }
303
  await this.saveStats();
 
304
  }
305
  }
306
 
307
  let statsTracker: StatsTracker;
308
 
309
- export async function initStatsTracker(statsFilePath?: string) {
310
- statsTracker = new StatsTracker(statsFilePath);
311
  await statsTracker.loadStats();
312
  return statsTracker;
313
  }
 
1
+ import { Redis } from '@upstash/redis';
 
2
 
3
  interface EndpointStats {
4
  totalRequests: number;
 
17
  resetTime: number;
18
  }
19
 
 
 
 
 
20
  interface GlobalStats {
21
  totalRequests: number;
22
  totalSuccess: number;
 
42
  private ipFailures: Map<string, IPFailureTracking>;
43
  private readonly MAX_FAILS_PER_IP = 1;
44
  private readonly FAIL_WINDOW_MS = 12 * 60 * 60 * 1000;
45
+ private redis: Redis | null = null;
46
  private saveTimeout: NodeJS.Timeout | null = null;
47
+ private readonly REDIS_KEY = 'api-stats:global';
48
 
49
+ constructor() {
 
50
  this.stats = {
51
  totalRequests: 0,
52
  totalSuccess: 0,
 
57
  visitorsByDay: new Map(),
58
  };
59
  this.ipFailures = new Map();
60
+
61
+ if (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) {
62
+ this.redis = new Redis({
63
+ url: process.env.UPSTASH_REDIS_REST_URL,
64
+ token: process.env.UPSTASH_REDIS_REST_TOKEN,
65
+ });
66
+ console.log('Redis initialized for persistent stats');
67
+ } else {
68
+ console.warn('Redis not configured - stats will be in-memory only');
69
+ }
70
 
71
  setInterval(() => {
72
  const now = Date.now();
 
79
  }
80
 
81
  async loadStats(): Promise<void> {
82
+ if (!this.redis) {
83
+ console.log('No Redis configured, starting with fresh stats');
84
+ return;
85
+ }
86
+
87
  try {
88
+ const data = await this.redis.get<SerializedStats>(this.REDIS_KEY);
89
+
90
+ if (!data) {
91
+ console.log('No existing stats found in Redis, starting fresh');
92
+ return;
93
+ }
94
+
95
+ this.stats.totalRequests = data.totalRequests || 0;
96
+ this.stats.totalSuccess = data.totalSuccess || 0;
97
+ this.stats.totalFailed = data.totalFailed || 0;
98
+ this.stats.uniqueVisitors = new Set(data.uniqueVisitors || []);
99
+ this.stats.startTime = data.startTime || Date.now();
100
  this.stats.endpoints = new Map();
101
 
102
+ if (data.endpoints) {
103
+ Object.entries(data.endpoints).forEach(([endpoint, stats]) => {
104
  this.stats.endpoints.set(endpoint, stats);
105
  });
106
  }
107
 
108
  this.stats.visitorsByDay = new Map();
109
+ if (data.visitorsByDay) {
110
+ Object.entries(data.visitorsByDay).forEach(([date, ips]) => {
111
  this.stats.visitorsByDay.set(date, new Set(ips));
112
  });
113
  }
114
 
115
+ console.log(`Stats loaded from Redis: ${this.stats.totalRequests} total requests`);
116
+ } catch (error) {
117
+ console.error('Error loading stats from Redis:', error);
 
 
 
 
118
  }
119
  }
120
 
121
  private async saveStats(): Promise<void> {
122
+ if (!this.redis) {
123
+ return;
124
+ }
125
+
126
  try {
127
  const serialized: SerializedStats = {
128
  totalRequests: this.stats.totalRequests,
 
142
  serialized.visitorsByDay[date] = Array.from(ips);
143
  });
144
 
145
+ await this.redis.set(this.REDIS_KEY, serialized);
146
+
147
+ // We can use this for auto clean up
148
+ // However, this will be considered later.
149
+ // await this.redis.expire(this.REDIS_KEY, 90 * 24 * 60 * 60);
150
  } catch (error) {
151
+ console.error('Error saving stats to Redis:', error);
152
  }
153
  }
154
 
 
156
  if (this.saveTimeout) {
157
  clearTimeout(this.saveTimeout);
158
  }
159
+
160
  this.saveTimeout = setTimeout(() => {
161
  this.saveStats();
162
  }, 5000);
 
226
  } else if (statusCode >= 500) {
227
  endpointStats.failedRequests++;
228
  }
229
+
230
  this.scheduleSave();
231
 
232
  return true;
 
253
  ? `${uptimeDays}d ${uptimeHours % 24}h`
254
  : `${uptimeHours}h`,
255
  },
256
+ persistenceEnabled: this.redis !== null,
257
  };
258
  }
259
 
 
316
  clearTimeout(this.saveTimeout);
317
  }
318
  await this.saveStats();
319
+ console.log('Stats saved on shutdown');
320
  }
321
  }
322
 
323
  let statsTracker: StatsTracker;
324
 
325
+ export async function initStatsTracker() {
326
+ statsTracker = new StatsTracker();
327
  await statsTracker.loadStats();
328
  return statsTracker;
329
  }