Skip to content

Commit b3585da

Browse files
authored
fix: requests stats (#876)
1 parent 21ea25a commit b3585da

File tree

7 files changed

+506
-1
lines changed

7 files changed

+506
-1
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [unreleased]
99

10+
## [6.0.16] - 2023-11-03
11+
12+
- Collects requests stats per app
13+
- Adds `/requests/stats` API to return requests stats for the last day
14+
1015
## [6.0.15] - 2023-10-18
1116

1217
- Fixes issue with cron tasks that run per app and tenant

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" }
1919
// }
2020
//}
2121

22-
version = "6.0.15"
22+
version = "6.0.16"
2323

2424

2525
repositories {
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
3+
*
4+
* This software is licensed under the Apache License, Version 2.0 (the
5+
* "License") as published by the Apache Software Foundation.
6+
*
7+
* You may not use this file except in compliance with the License. You may
8+
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package io.supertokens.webserver;
18+
19+
import com.google.gson.JsonArray;
20+
import com.google.gson.JsonObject;
21+
import com.google.gson.JsonPrimitive;
22+
import io.supertokens.Main;
23+
import io.supertokens.ResourceDistributor;
24+
import io.supertokens.multitenancy.Multitenancy;
25+
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
26+
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
27+
28+
public class RequestStats extends ResourceDistributor.SingletonResource {
29+
public static final String RESOURCE_KEY = "io.supertokens.webserver.RequestStats";
30+
31+
private final int MAX_MINUTES = 24 * 60;
32+
33+
private long currentMinute; // current minute since epoch
34+
private final int[] currentMinuteRequestCounts; // array of 60 items representing number of requests at each second in the current minute
35+
36+
// The 2 arrays below contains stats for a day for every minute
37+
// the array is stored in such a way that array[currentMinute % MAX_MINUTES] contains the stats for a day ago
38+
// until array[(currentMinute - 1) % MAX_MINUTES] which contains the stats for the last minute, circling around
39+
// from end of array to the beginning
40+
// for e.g. if currentMinute % MAX_MINUTES = 250,
41+
// then array[250] contains stats for now - 1440 minutes
42+
// array[251] contains stats for now - 1439 minutes
43+
// ...
44+
// array[1439] contains stats for now - 1191 minutes
45+
// array[0] contains stats for now - 1190 minutes
46+
// array[1] contains stats for now - 1189 minutes
47+
// ...
48+
// array[249] contains stats for now - 1 minute
49+
private final double[] averageRequestsPerSecond;
50+
private final int[] peakRequestsPerSecond;
51+
52+
private RequestStats() {
53+
currentMinute = System.currentTimeMillis() / 60000;
54+
currentMinuteRequestCounts = new int[60];
55+
56+
averageRequestsPerSecond = new double[MAX_MINUTES];
57+
peakRequestsPerSecond = new int[MAX_MINUTES];
58+
for (int i = 0; i < MAX_MINUTES; i++) {
59+
averageRequestsPerSecond[i] = -1;
60+
peakRequestsPerSecond[i] = -1;
61+
}
62+
}
63+
64+
private void checkAndUpdateMinute(long currentSecond) {
65+
if (currentSecond / 60 == currentMinute) {
66+
return; // stats update not required
67+
}
68+
69+
int sum = 0;
70+
int max = 0;
71+
for (int i = 0; i < 60; i++) {
72+
sum += currentMinuteRequestCounts[i];
73+
max = Math.max(max, currentMinuteRequestCounts[i]);
74+
}
75+
76+
averageRequestsPerSecond[(int) (currentMinute % MAX_MINUTES)] = sum / 60.0;
77+
peakRequestsPerSecond[(int) (currentMinute % MAX_MINUTES)] = max;
78+
79+
// fill zeros for passed minutes
80+
for (long i = currentMinute + 1; i < currentSecond / 60; i++) {
81+
averageRequestsPerSecond[(int) (i % MAX_MINUTES)] = 0;
82+
peakRequestsPerSecond[(int) (i % MAX_MINUTES)] = 0;
83+
}
84+
85+
currentMinute = currentSecond / 60;
86+
for (int i = 0; i < 60; i++) {
87+
currentMinuteRequestCounts[i] = 0;
88+
}
89+
}
90+
91+
private void updateCounts(long currentSecond) {
92+
currentMinuteRequestCounts[(int) (currentSecond % 60)]++;
93+
}
94+
95+
public static RequestStats getInstance(Main main, AppIdentifier appIdentifier) throws TenantOrAppNotFoundException {
96+
try {
97+
return (RequestStats) main.getResourceDistributor()
98+
.getResource(appIdentifier.getAsPublicTenantIdentifier(), RESOURCE_KEY);
99+
} catch (TenantOrAppNotFoundException e) {
100+
// appIdentifier parameter is coming from the API request and hence we need to check if the app exists
101+
// before creating a resource for it, otherwise someone could fill up memory by making requests for apps
102+
// that don't exist.
103+
// The other resources are created during init or while refreshing tenants from the db, so we don't need
104+
// this kind of pattern for those resources.
105+
if (Multitenancy.getTenantInfo(main, appIdentifier.getAsPublicTenantIdentifier()) == null) {
106+
throw e;
107+
}
108+
return (RequestStats) main.getResourceDistributor()
109+
.setResource(appIdentifier.getAsPublicTenantIdentifier(), RESOURCE_KEY, new RequestStats());
110+
}
111+
}
112+
113+
public void updateRequestStats() {
114+
this.updateRequestStats(true);
115+
}
116+
117+
synchronized private void updateRequestStats(boolean updateCounts) {
118+
long now = System.currentTimeMillis() / 1000;
119+
this.checkAndUpdateMinute(now);
120+
if (updateCounts) { this.updateCounts(now); }
121+
}
122+
123+
public JsonObject getStats() {
124+
this.updateRequestStats(false);
125+
126+
JsonArray avgRps = new JsonArray();
127+
JsonArray peakRps = new JsonArray();
128+
129+
long atMinute = System.currentTimeMillis() / 60000;
130+
131+
int offset = (int) (atMinute % MAX_MINUTES);
132+
for (int i = 0; i < MAX_MINUTES; i++) {
133+
avgRps.add(new JsonPrimitive(this.averageRequestsPerSecond[(i + offset) % MAX_MINUTES]));
134+
peakRps.add(new JsonPrimitive(this.peakRequestsPerSecond[(i + offset) % MAX_MINUTES]));
135+
}
136+
137+
JsonObject result = new JsonObject();
138+
result.addProperty("atMinute", atMinute);
139+
result.add("averageRequestsPerSecond", avgRps);
140+
result.add("peakRequestsPerSecond", peakRps);
141+
return result;
142+
}
143+
}

src/main/java/io/supertokens/webserver/Webserver.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ private void setupRoutes() {
250250
addAPI(new AssociateUserToTenantAPI(main));
251251
addAPI(new DisassociateUserFromTenant(main));
252252

253+
addAPI(new RequestStatsAPI(main));
254+
253255
StandardContext context = tomcatReference.getContext();
254256
Tomcat tomcat = tomcatReference.getTomcat();
255257

src/main/java/io/supertokens/webserver/WebserverAPI.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,11 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
480480
}
481481
Logging.info(main, tenantIdentifier, "API ended: " + req.getRequestURI() + ". Method: " + req.getMethod(),
482482
false);
483+
try {
484+
RequestStats.getInstance(main, tenantIdentifier.toAppIdentifier()).updateRequestStats();
485+
} catch (TenantOrAppNotFoundException e) {
486+
// Ignore the error as we would have already sent the response for tenantNotFound
487+
}
483488
}
484489

485490
protected String getRIDFromRequest(HttpServletRequest req) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved.
3+
*
4+
* This software is licensed under the Apache License, Version 2.0 (the
5+
* "License") as published by the Apache Software Foundation.
6+
*
7+
* You may not use this file except in compliance with the License. You may
8+
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package io.supertokens.webserver.api.core;
18+
19+
import com.google.gson.JsonObject;
20+
import io.supertokens.Main;
21+
import io.supertokens.cliOptions.CLIOptions;
22+
import io.supertokens.multitenancy.exception.BadPermissionException;
23+
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
24+
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
25+
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
26+
import io.supertokens.webserver.InputParser;
27+
import io.supertokens.webserver.RequestStats;
28+
import io.supertokens.webserver.WebserverAPI;
29+
import jakarta.servlet.ServletException;
30+
import jakarta.servlet.http.HttpServletRequest;
31+
import jakarta.servlet.http.HttpServletResponse;
32+
33+
import java.io.File;
34+
import java.io.IOException;
35+
36+
public class RequestStatsAPI extends WebserverAPI {
37+
private static final long serialVersionUID = -4641988458637882374L;
38+
39+
public RequestStatsAPI(Main main) {
40+
super(main, "");
41+
}
42+
43+
@Override
44+
public String getPath() {
45+
return "/requests/stats";
46+
}
47+
48+
@Override
49+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
50+
// API is app specific
51+
try {
52+
AppIdentifier appIdentifier = getAppIdentifierWithStorageFromRequestAndEnforcePublicTenant(req);
53+
JsonObject stats = RequestStats.getInstance(main, appIdentifier).getStats();
54+
stats.addProperty("status", "OK");
55+
super.sendJsonResponse(200, stats, resp);
56+
57+
} catch (BadPermissionException | TenantOrAppNotFoundException e) {
58+
throw new ServletException(e);
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)