Skip to content

Commit 2c96914

Browse files
committed
Add a monitoring page to check DB latency
1 parent 746454c commit 2c96914

File tree

9 files changed

+450
-0
lines changed

9 files changed

+450
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.box.l10n.mojito.rest.monitoring;
2+
3+
import com.box.l10n.mojito.service.monitoring.DbMonitoringService;
4+
import com.box.l10n.mojito.service.monitoring.DbMonitoringService.DbMonitoringSnapshot;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RequestParam;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
@RestController
11+
@RequestMapping("/api/monitoring")
12+
public class DbMonitoringWS {
13+
14+
static final int DEFAULT_ITERATIONS = 5;
15+
16+
private final DbMonitoringService dbMonitoringService;
17+
18+
public DbMonitoringWS(DbMonitoringService dbMonitoringService) {
19+
this.dbMonitoringService = dbMonitoringService;
20+
}
21+
22+
@GetMapping("/db")
23+
public DbMonitoringSnapshot getDatabaseLatency(
24+
@RequestParam(name = "iterations", defaultValue = "" + DEFAULT_ITERATIONS) int iterations) {
25+
26+
int sanitizedIterations =
27+
Math.max(
28+
DbMonitoringService.MIN_ITERATIONS,
29+
Math.min(DbMonitoringService.MAX_ITERATIONS, iterations));
30+
31+
return dbMonitoringService.measureLatency(sanitizedIterations);
32+
}
33+
}

webapp/src/main/java/com/box/l10n/mojito/security/WebSecurityConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ static void setAuthorizationRequests(HttpSecurity http, List<String> extraPermit
164164
// user management is only allowed for ADMINs and PMs
165165
.requestMatchers("/api/users/**")
166166
.hasAnyRole("PM", "ADMIN")
167+
// monitoring endpoint reserved for admins
168+
.requestMatchers(HttpMethod.GET, "/api/monitoring/db")
169+
.hasRole("ADMIN")
167170
// Read-only access is OK for users
168171
.requestMatchers(HttpMethod.GET, "/api/textunits/**")
169172
.authenticated()
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.box.l10n.mojito.service.monitoring;
2+
3+
import com.box.l10n.mojito.service.repository.RepositoryService;
4+
import java.time.Instant;
5+
import java.util.ArrayList;
6+
import java.util.DoubleSummaryStatistics;
7+
import java.util.List;
8+
import java.util.concurrent.TimeUnit;
9+
import org.springframework.jdbc.core.JdbcTemplate;
10+
import org.springframework.stereotype.Service;
11+
12+
/**
13+
* Provides utilities to measure database latency by executing lightweight probes.
14+
*/
15+
@Service
16+
public class DbMonitoringService {
17+
18+
public static final int MIN_ITERATIONS = 1;
19+
public static final int MAX_ITERATIONS = 20;
20+
21+
private final JdbcTemplate jdbcTemplate;
22+
private final RepositoryService repositoryService;
23+
24+
public DbMonitoringService(JdbcTemplate jdbcTemplate, RepositoryService repositoryService) {
25+
this.jdbcTemplate = jdbcTemplate;
26+
this.repositoryService = repositoryService;
27+
}
28+
29+
public DbMonitoringSnapshot measureLatency(int requestedIterations) {
30+
int iterations = Math.max(MIN_ITERATIONS, Math.min(MAX_ITERATIONS, requestedIterations));
31+
32+
LatencySeries jdbcSeries = measureSeries(iterations, this::probeJdbc);
33+
LatencySeries hibernateSeries = measureSeries(iterations, this::probeHibernate);
34+
35+
return new DbMonitoringSnapshot(Instant.now(), iterations, jdbcSeries, hibernateSeries);
36+
}
37+
38+
private LatencySeries measureSeries(int iterations, Runnable probe) {
39+
List<LatencyMeasurement> measurements = new ArrayList<>(iterations);
40+
41+
for (int i = 0; i < iterations; i++) {
42+
long start = System.nanoTime();
43+
probe.run();
44+
long elapsedNanos = System.nanoTime() - start;
45+
double elapsedMillis = elapsedNanos / (double) TimeUnit.MILLISECONDS.toNanos(1);
46+
measurements.add(new LatencyMeasurement(i + 1, elapsedMillis));
47+
}
48+
49+
DoubleSummaryStatistics stats =
50+
measurements.stream().mapToDouble(LatencyMeasurement::getLatencyMs).summaryStatistics();
51+
52+
return new LatencySeries(measurements, stats.getMin(), stats.getMax(), stats.getAverage());
53+
}
54+
55+
private void probeJdbc() {
56+
jdbcTemplate.execute("SELECT 1");
57+
}
58+
59+
private void probeHibernate() {
60+
repositoryService.findRepositoriesIsNotDeletedOrderByName(null);
61+
}
62+
63+
public static class DbMonitoringSnapshot {
64+
final Instant timestamp;
65+
final int iterations;
66+
final LatencySeries jdbc;
67+
final LatencySeries hibernate;
68+
69+
public DbMonitoringSnapshot(
70+
Instant timestamp, int iterations, LatencySeries jdbc, LatencySeries hibernate) {
71+
this.timestamp = timestamp;
72+
this.iterations = iterations;
73+
this.jdbc = jdbc;
74+
this.hibernate = hibernate;
75+
}
76+
77+
public Instant getTimestamp() {
78+
return timestamp;
79+
}
80+
81+
public int getIterations() {
82+
return iterations;
83+
}
84+
85+
public LatencySeries getJdbc() {
86+
return jdbc;
87+
}
88+
89+
public LatencySeries getHibernate() {
90+
return hibernate;
91+
}
92+
}
93+
94+
public static class LatencySeries {
95+
final List<LatencyMeasurement> measurements;
96+
final double minLatencyMs;
97+
final double maxLatencyMs;
98+
final double averageLatencyMs;
99+
100+
public LatencySeries(
101+
List<LatencyMeasurement> measurements,
102+
double minLatencyMs,
103+
double maxLatencyMs,
104+
double averageLatencyMs) {
105+
this.measurements = List.copyOf(measurements);
106+
this.minLatencyMs = minLatencyMs;
107+
this.maxLatencyMs = maxLatencyMs;
108+
this.averageLatencyMs = averageLatencyMs;
109+
}
110+
111+
public List<LatencyMeasurement> getMeasurements() {
112+
return measurements;
113+
}
114+
115+
public double getMinLatencyMs() {
116+
return minLatencyMs;
117+
}
118+
119+
public double getMaxLatencyMs() {
120+
return maxLatencyMs;
121+
}
122+
123+
public double getAverageLatencyMs() {
124+
return averageLatencyMs;
125+
}
126+
}
127+
128+
public static class LatencyMeasurement {
129+
final int iteration;
130+
final double latencyMs;
131+
132+
LatencyMeasurement(int iteration, double latencyMs) {
133+
this.iteration = iteration;
134+
this.latencyMs = latencyMs;
135+
}
136+
137+
public int getIteration() {
138+
return iteration;
139+
}
140+
141+
public double getLatencyMs() {
142+
return latencyMs;
143+
}
144+
}
145+
}

webapp/src/main/resources/properties/en.properties

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ header.projectRequests=Project Requests
1313
# Label displayed in the header menu to open the settings page
1414
header.settings=Settings
1515

16+
# Label displayed in the header menu to open the monitoring page
17+
header.monitoring=Monitoring
18+
1619
# Label displayed in the header menu to open the screenshot page
1720
header.screenshots=Screenshots
1821

@@ -1087,3 +1090,25 @@ users.userInformationAlert=Administrators and Project Managers have write access
10871090

10881091

10891092
aiReviewModal.title=AI Review
1093+
1094+
# Monitoring page headings and labels
1095+
monitoring.latency.title=Database Latency
1096+
monitoring.latency.iterations=Iterations
1097+
monitoring.latency.iterations.help=Choose between {min} and {max} probes to average.
1098+
monitoring.latency.measure=Measure
1099+
monitoring.latency.loading=Measuring...
1100+
monitoring.latency.summary=Summary
1101+
monitoring.latency.lastRun=Last run
1102+
monitoring.latency.min=Min (ms)
1103+
monitoring.latency.max=Max (ms)
1104+
monitoring.latency.avg=Average (ms)
1105+
monitoring.latency.measurements=Measurements
1106+
monitoring.latency.iteration=Iteration
1107+
monitoring.latency.latency=Latency (ms)
1108+
monitoring.latency.noData=Run a measurement to see results.
1109+
monitoring.latency.error=Failed to measure latency.
1110+
monitoring.latency.forbidden=Only administrators can access this page.
1111+
monitoring.latency.overview=Overview
1112+
monitoring.latency.iterationsUsed=Iterations used
1113+
monitoring.latency.series.jdbc=Direct JDBC probe
1114+
monitoring.latency.series.hibernate=Hibernate repositories query

webapp/src/main/resources/public/js/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import ScreenshotDropzonePage from "./components/screenshots/ScreenshotDropzoneP
2020
import UserManagement from "./components/users/UserManagement";
2121
import Settings from "./components/settings/Settings";
2222
import BoxSettings from "./components/settings/BoxSettings";
23+
import DbLatencyMonitoring from "./components/settings/DbLatencyMonitoring";
2324
import WorkbenchActions from "./actions/workbench/WorkbenchActions";
2425
import RepositoryActions from "./actions/RepositoryActions";
2526
import ScreenshotsPageActions from "./actions/screenshots/ScreenshotsPageActions";
@@ -127,6 +128,7 @@ function startApp(messages) {
127128
<Route path="user-management" component={UserManagement}/>
128129
<Route path="box" component={BoxSettings}/>
129130
</Route>
131+
<Route path="monitoring" component={DbLatencyMonitoring}/>
130132
</Route>
131133
<Route path="auth/callback" component={AuthCallback}></Route>
132134
<Route path="login" component={Login}></Route>

webapp/src/main/resources/public/js/components/header/Header.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import BranchesPageActions from "../../actions/branches/BranchesPageActions";
2020
import {withAppConfig} from "../../utils/AppConfig";
2121
import { isMsalStateless } from '../../auth/AuthFlags';
2222
import TokenProvider from '../../auth/TokenProvider';
23+
import AuthorityService from "../../utils/AuthorityService";
2324

2425
class Header extends React.Component {
2526
state = {
@@ -140,6 +141,14 @@ class Header extends React.Component {
140141
</MenuItem>
141142
</LinkContainer>
142143

144+
{AuthorityService.isAdmin() && (
145+
<LinkContainer to="/monitoring">
146+
<MenuItem>
147+
<Glyphicon glyph="stats"/> <FormattedMessage id="header.monitoring" defaultMessage="Monitoring"/>
148+
</MenuItem>
149+
</LinkContainer>
150+
)}
151+
143152
<MenuItem divider/>
144153

145154
<MenuItem onSelect={this.openChangePasswordModal}>

0 commit comments

Comments
 (0)