Skip to content

Commit 3c29c0b

Browse files
fix: request stats (#875)
* fix: request stats * fix: refactor and comments * fix: per app test * fix: comments * fix: pr comments * fix: pr comments * fix: comment * Update src/main/java/io/supertokens/webserver/RequestStats.java Co-authored-by: Rishabh Poddar <[email protected]> * fix: pr comment --------- Co-authored-by: Rishabh Poddar <[email protected]>
1 parent 4fe17c2 commit 3c29c0b

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
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
66
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [7.0.10] - 2023-11-03
9+
10+
- Collects requests stats per app
11+
- Adds `/requests/stats` API to return requests stats for the last day
12+
813
## [7.0.9] - 2023-11-01
914

1015
- Tests `verified` in `loginMethods` for users with userId mapping

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 = "7.0.9"
22+
version = "7.0.10"
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
@@ -257,6 +257,8 @@ private void setupRoutes() {
257257
addAPI(new UnlinkAccountAPI(main));
258258
addAPI(new ConsumeResetPasswordAPI(main));
259259

260+
addAPI(new RequestStatsAPI(main));
261+
260262
StandardContext context = tomcatReference.getContext();
261263
Tomcat tomcat = tomcatReference.getTomcat();
262264

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

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

486491
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)