Added server uptime (#2012)

* Wrote a query and utility for getting server uptime
* Added current_uptime to json endpoints
* Load current uptime on the website
* Moved nukkit repo to nukkit module
* Added equals and hashcode to QueryStatement and QueryAPIQuery
* Remove dependency on codemc repository

Affects issues:
- Close #1845
This commit is contained in:
Risto Lahtela 2021-07-18 10:43:38 +03:00 committed by GitHub
parent 5e53303415
commit 84fa3ad4cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 230 additions and 5 deletions

View File

@ -74,7 +74,7 @@ subprojects {
nukkitVersion = "1.0-SNAPSHOT"
bungeeVersion = "1.16-R0.4"
velocityVersion = "3.0.0-SNAPSHOT"
redisBungeeVersion = "0.6.3"
redisBungeeVersion = "0.3.8-SNAPSHOT"
commonsTextVersion = "1.9"
commonsCompressVersion = "1.21"
@ -98,9 +98,6 @@ subprojects {
maven { url = "https://oss.sonatype.org/content/repositories/snapshots" } // BungeeCord
maven { url = "https://repo.md-5.net/content/repositories/snapshots/" } // RedisBungee
maven { url = "https://repo.velocitypowered.com/snapshots/" } // Velocity
maven { url = "https://repo.opencollab.dev/maven-snapshots/" } // Nukkit snapshot
maven { url = "https://repo.opencollab.dev/maven-releases/" } // Nukkit release
maven { url = "https://repo.codemc.org/repository/maven-public" } // bStats
maven { url = "https://repo.playeranalytics.net/releases" } // Plan
}

View File

@ -55,6 +55,8 @@ import javax.inject.Singleton;
@Singleton
public class PlanSystem implements SubSystem {
private static final long SERVER_ENABLE_TIME = System.currentTimeMillis();
private boolean enabled = false;
private final PlanFiles files;
@ -298,4 +300,8 @@ public class PlanSystem implements SubSystem {
public ErrorLogger getErrorLogger() {
return errorLogger;
}
public static long getServerEnableTime() {
return SERVER_ENABLE_TIME;
}
}

View File

@ -25,6 +25,7 @@ import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.json.graphs.Graphs;
import com.djrapitops.plan.extension.implementation.results.ExtensionTabData;
import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionServerTableDataQuery;
import com.djrapitops.plan.gathering.ServerUptimeCalculator;
import com.djrapitops.plan.gathering.cache.SessionCache;
import com.djrapitops.plan.gathering.domain.*;
import com.djrapitops.plan.identification.Server;
@ -34,6 +35,7 @@ import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
import com.djrapitops.plan.settings.config.paths.TimeSettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.GenericLang;
import com.djrapitops.plan.settings.locale.lang.HtmlLang;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
@ -62,6 +64,7 @@ public class JSONFactory {
private final Locale locale;
private final DBSystem dbSystem;
private final ServerInfo serverInfo;
private final ServerUptimeCalculator serverUptimeCalculator;
private final Graphs graphs;
private final Formatters formatters;
@ -71,6 +74,7 @@ public class JSONFactory {
Locale locale,
DBSystem dbSystem,
ServerInfo serverInfo,
ServerUptimeCalculator serverUptimeCalculator,
Graphs graphs,
Formatters formatters
) {
@ -78,6 +82,7 @@ public class JSONFactory {
this.locale = locale;
this.dbSystem = dbSystem;
this.serverInfo = serverInfo;
this.serverUptimeCalculator = serverUptimeCalculator;
this.graphs = graphs;
this.formatters = formatters;
}
@ -220,6 +225,8 @@ public class JSONFactory {
server.put("avg_tps", averageTPS != -1 ? decimals.apply(averageTPS) : locale.get(HtmlLang.UNIT_NO_DATA).toString());
server.put("low_tps_spikes", tpsWeek.lowTpsSpikeCount(config.get(DisplaySettings.GRAPH_TPS_THRESHOLD_MED)));
server.put("downtime", timeAmount.apply(tpsWeek.serverDownTime()));
server.put("current_uptime", serverUptimeCalculator.getServerUptimeMillis(serverUUID).map(timeAmount)
.orElse(locale.getString(GenericLang.UNAVAILABLE)));
Optional<TPS> online = tpsWeek.getLast();
server.put("online", online.map(point -> point.getDate() >= now - TimeUnit.MINUTES.toMillis(3L) ? point.getPlayers() : "Possibly offline")

View File

@ -22,6 +22,7 @@ import com.djrapitops.plan.delivery.domain.mutators.TPSMutator;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.gathering.ServerSensor;
import com.djrapitops.plan.gathering.ServerUptimeCalculator;
import com.djrapitops.plan.gathering.domain.TPS;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
@ -65,6 +66,7 @@ public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<Strin
private final Formatter<Long> timeAmount;
private final Formatter<Double> decimals;
private final Formatter<Double> percentage;
private final ServerUptimeCalculator serverUptimeCalculator;
private final Formatter<DateHolder> year;
@Inject
@ -74,6 +76,7 @@ public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<Strin
DBSystem dbSystem,
ServerInfo serverInfo,
ServerSensor<?> serverSensor,
ServerUptimeCalculator serverUptimeCalculator,
Formatters formatters
) {
this.config = config;
@ -81,6 +84,7 @@ public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<Strin
this.dbSystem = dbSystem;
this.serverInfo = serverInfo;
this.serverSensor = serverSensor;
this.serverUptimeCalculator = serverUptimeCalculator;
year = formatters.year();
day = formatters.dayLong();
@ -148,6 +152,8 @@ public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<Strin
numbers.put("player_kills", db.query(KillQueries.playerKillCount(0L, now, serverUUID)));
numbers.put("mob_kills", db.query(KillQueries.mobKillCount(0L, now, serverUUID)));
numbers.put("deaths", db.query(KillQueries.deathCount(0L, now, serverUUID)));
numbers.put("current_uptime", serverUptimeCalculator.getServerUptimeMillis(serverUUID).map(timeAmount)
.orElse(locale.getString(GenericLang.UNAVAILABLE)));
return numbers;
}

View File

@ -22,10 +22,13 @@ import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.json.Trend;
import com.djrapitops.plan.gathering.ServerSensor;
import com.djrapitops.plan.gathering.ServerUptimeCalculator;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.TimeSettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.GenericLang;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.analysis.NetworkActivityIndexQueries;
@ -50,24 +53,30 @@ public class NetworkOverviewJSONCreator implements NetworkTabJSONCreator<Map<Str
private final Formatter<Long> day;
private final PlanConfig config;
private final Locale locale;
private final DBSystem dbSystem;
private final ServerInfo serverInfo;
private final ServerSensor<?> serverSensor;
private final Formatter<Long> timeAmount;
private final ServerUptimeCalculator serverUptimeCalculator;
private final Formatter<DateHolder> year;
@Inject
public NetworkOverviewJSONCreator(
PlanConfig config,
Locale locale,
DBSystem dbSystem,
ServerInfo serverInfo,
ServerSensor<?> serverSensor,
ServerUptimeCalculator serverUptimeCalculator,
Formatters formatters
) {
this.config = config;
this.locale = locale;
this.dbSystem = dbSystem;
this.serverInfo = serverInfo;
this.serverSensor = serverSensor;
this.serverUptimeCalculator = serverUptimeCalculator;
year = formatters.year();
day = formatters.dayLong();
@ -127,6 +136,8 @@ public class NetworkOverviewJSONCreator implements NetworkTabJSONCreator<Map<Str
Long sessionCount = db.query(SessionQueries.sessionCount(0L, now));
numbers.put("sessions", sessionCount);
numbers.put("session_length_avg", sessionCount != 0 ? timeAmount.apply(totalPlaytime / sessionCount) : "-");
numbers.put("current_uptime", serverUptimeCalculator.getServerUptimeMillis(serverUUID).map(timeAmount)
.orElse(locale.getString(GenericLang.UNAVAILABLE)));
return numbers;
}

View File

@ -0,0 +1,66 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.gathering;
import com.djrapitops.plan.PlanSystem;
import com.djrapitops.plan.gathering.domain.TPS;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.objects.TPSQueries;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Singleton
public class ServerUptimeCalculator {
private final ServerInfo serverInfo;
private final DBSystem dbSystem;
@Inject
public ServerUptimeCalculator(ServerInfo serverInfo, DBSystem dbSystem) {
this.serverInfo = serverInfo;
this.dbSystem = dbSystem;
}
public Optional<Long> getServerUptimeMillis(ServerUUID serverUUID) {
if (serverUUID == null) throw new IllegalArgumentException("'serverUUID' can't be null");
if (serverUUID.equals(serverInfo.getServerUUID())) {
return Optional.of(System.currentTimeMillis() - PlanSystem.getServerEnableTime());
} else {
long dataGapThreshold = TimeUnit.MINUTES.toMillis(3);
Database database = dbSystem.getDatabase();
Optional<Long> latestDataDate = database.query(TPSQueries.fetchLatestTPSEntryForServer(serverUUID)).map(TPS::getDate);
Optional<Long> dataBlockStartDate = database.query(TPSQueries.fetchLatestServerStartTime(serverUUID, dataGapThreshold));
if (!latestDataDate.isPresent() || !dataBlockStartDate.isPresent()) {
return Optional.empty();
}
if (System.currentTimeMillis() - latestDataDate.get() > dataGapThreshold) {
return Optional.empty();
}
return Optional.of(System.currentTimeMillis() - dataBlockStartDate.get());
}
}
}

View File

@ -223,6 +223,7 @@ public enum HtmlLang implements Lang {
NO_KILLS("No Kills"),
LABEL_MAX_FREE_DISK("Max Free Disk"),
LABEL_MIN_FREE_DISK("Min Free Disk"),
LABEL_CURRENT_UPTIME("Current Uptime"),
LOGIN_LOGIN("Login"),
LOGIN_LOGOUT("Logout"),

View File

@ -23,6 +23,7 @@ import com.djrapitops.plan.storage.database.SQLDB;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Objects;
public class QueryAPIQuery<T> implements Query<T> {
@ -57,4 +58,24 @@ public class QueryAPIQuery<T> implements Query<T> {
throw DBOpException.forCause(sql, e);
}
}
@Override
public String toString() {
return "QueryAPIQuery{" +
"sql='" + sql + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QueryAPIQuery<?> that = (QueryAPIQuery<?>) o;
return Objects.equals(sql, that.sql);
}
@Override
public int hashCode() {
return Objects.hash(sql);
}
}

View File

@ -23,6 +23,7 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Objects;
/**
* SQL query that closes proper elements.
@ -88,4 +89,17 @@ public abstract class QueryStatement<T> implements Query<T> {
public String toString() {
return "Query (" + sql + ')';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QueryStatement<?> that = (QueryStatement<?>) o;
return Objects.equals(getSql(), that.getSql());
}
@Override
public int hashCode() {
return Objects.hash(getSql());
}
}

View File

@ -452,7 +452,6 @@ public class TPSQueries {
AND + DATE + ">=?" +
AND + DATE + "<=?" +
ORDER_BY + DATE;
System.out.println(sql);
return new QueryStatement<Map<Integer, List<TPS>>>(sql, 50000) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
@ -472,4 +471,54 @@ public class TPSQueries {
}
};
}
public static Query<Optional<Long>> fetchLatestServerStartTime(ServerUUID serverUUID, long dataGapThreshold) {
String selectPreviousRowNumber = SELECT +
"-1+ROW_NUMBER() over (ORDER BY " + DATE + ") AS previous_rn, " +
DATE + " AS d1" +
FROM + TABLE_NAME +
WHERE + SERVER_ID + '=' + ServerTable.STATEMENT_SELECT_SERVER_ID +
ORDER_BY + DATE + " DESC";
String selectRowNumber = SELECT +
"ROW_NUMBER() over (ORDER BY " + DATE + ") AS rn, " +
DATE + " AS previous_date" +
FROM + TABLE_NAME +
WHERE + SERVER_ID + '=' + ServerTable.STATEMENT_SELECT_SERVER_ID +
ORDER_BY + DATE + " DESC";
String selectFirstEntryDate = SELECT + "MIN(" + DATE + ") as start_time" +
FROM + TABLE_NAME +
WHERE + SERVER_ID + '=' + ServerTable.STATEMENT_SELECT_SERVER_ID;
// Finds the start time since difference between d1 and previous date is a gap,
// so d1 is always first entry after a gap in the data. MAX finds the latest.
// Union ensures if there are no gaps to use the first date recorded.
String selectStartTime = SELECT +
"MAX(d1) AS start_time" +
FROM + "(" + selectPreviousRowNumber + ") t1" +
INNER_JOIN +
"(" + selectRowNumber + ") t2 ON t1.previous_rn=t2.rn" +
WHERE + "d1 - previous_date > ?" +
UNION + selectFirstEntryDate;
return new QueryStatement<Optional<Long>>(selectStartTime) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, serverUUID.toString());
statement.setString(2, serverUUID.toString());
statement.setLong(3, dataGapThreshold);
statement.setString(4, serverUUID.toString());
}
@Override
public Optional<Long> processResults(ResultSet set) throws SQLException {
long startTime = 0;
while (set.next()) {
long gotStartTime = set.getLong("start_time");
if (!set.wasNull()) {
startTime = Math.max(startTime, gotStartTime);
}
}
return startTime != 0 ? Optional.of(startTime) : Optional.empty();
}
};
}
}

View File

@ -69,6 +69,7 @@ function loadNetworkOverviewValues(json, error) {
data = json.numbers;
element = tab.querySelector('#data_numbers');
element.querySelector('#data_current_uptime').innerText = data.current_uptime;
element.querySelector('#data_total').innerText = data.total_players;
element.querySelector('#data_regular').innerText = data.regular_players;
element.querySelector('#data_online').innerText = data.online_players;
@ -325,6 +326,7 @@ function onViewserver(i, servers) {
quickView.querySelector('#data_avg_tps').innerText = server.avg_tps;
quickView.querySelector('#data_low_tps_spikes').innerText = server.low_tps_spikes;
quickView.querySelector('#data_downtime').innerText = server.downtime;
quickView.querySelector('#data_current_uptime').innerText = server.current_uptime;
}, 0);
}
}

View File

@ -74,6 +74,7 @@ function loadserverOverviewValues(json, error) {
data = json.numbers;
element = tab.querySelector('#data_numbers');
element.querySelector('#data_current_uptime').innerText = data.current_uptime;
element.querySelector('#data_total').innerText = data.total_players;
element.querySelector('#data_regular').innerText = data.regular_players;
element.querySelector('#data_online').innerText = data.online_players;

View File

@ -251,6 +251,9 @@
Some data requires Plan to be installed on game servers.
</div>
<div class="card-body" id="data_numbers">
<p><i class="col-light-green fa fa-fw fa-power-off"></i> Current Uptime<span
class="float-end" id="data_current_uptime"></span></p>
<hr>
<p><i class="col-black fa fa-fw fa-users"></i> Total Players<span
class="float-end"><b id="data_total"></b></span></p>
<p><i class="col-lime fa fa-fw fa-users"></i> Regular Players<span
@ -379,6 +382,8 @@
<span class="data_server_name"></span> as Numbers</h6>
</div>
<div class="card-body" id="data_quick_view">
<p><i class="col-light-green fa fa-fw fa-power-off"></i> Current Uptime<span
class="float-end" id="data_current_uptime"></span></p>
<p><i class="col-blue fa fa-fw fa-chart-line"></i> Last Peak: <span
id="data_last_peak_date"></span><span class="float-end"><b
id="data_last_peak_players"></b> Players</span>

View File

@ -220,6 +220,9 @@
Server as Numbers</h6>
</div>
<div class="card-body" id="data_numbers">
<p><i class="col-light-green fa fa-fw fa-power-off"></i> Current Uptime<span
class="float-end" id="data_current_uptime"></span></p>
<hr>
<p><i class="col-black fa fa-fw fa-users"></i> Total Players<span
class="float-end"><b id="data_total"></b></span></p>
<p><i class="col-lime fa fa-fw fa-users"></i> Regular Players<span

View File

@ -18,6 +18,7 @@ package com.djrapitops.plan.storage.database.queries;
import com.djrapitops.plan.delivery.domain.DateObj;
import com.djrapitops.plan.gathering.domain.TPS;
import com.djrapitops.plan.gathering.domain.builders.TPSBuilder;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.storage.database.DatabaseTestPreparer;
import com.djrapitops.plan.storage.database.queries.objects.TPSQueries;
@ -30,6 +31,8 @@ import utilities.RandomData;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -69,4 +72,32 @@ public interface TPSQueriesTest extends DatabaseTestPreparer {
int actual = db().query(TPSQueries.fetchAllTimePeakPlayerCount(serverUUID())).map(DateObj::getValue).orElse(-1);
assertEquals(expected, actual, () -> "Wrong return value. " + Lists.map(tpsData, TPS::getPlayers).toString());
}
@Test
default void serverStartDateIsFetched() {
List<TPS> tpsData = RandomData.randomTPS();
TPS stored = tpsData.get(0);
TPS stored2 = TPSBuilder.get().date(stored.getDate() + TimeUnit.MINUTES.toMillis(1L)).toTPS();
db().executeTransaction(new TPSStoreTransaction(serverUUID(), stored));
db().executeTransaction(new TPSStoreTransaction(serverUUID(), stored2));
Optional<Long> result = db().query(TPSQueries.fetchLatestServerStartTime(serverUUID(), TimeUnit.MINUTES.toMillis(3)));
assertTrue(result.isPresent());
assertEquals(stored.getDate(), result.get());
}
@Test
default void serverStartDateIsCorrect() {
List<TPS> tpsData = RandomData.randomTPS();
TPS stored = tpsData.get(0);
TPS stored2 = TPSBuilder.get().date(stored.getDate() + TimeUnit.MINUTES.toMillis(4L)).toTPS();
TPS stored3 = TPSBuilder.get().date(stored.getDate() + TimeUnit.MINUTES.toMillis(5L)).toTPS();
db().executeTransaction(new TPSStoreTransaction(serverUUID(), stored));
db().executeTransaction(new TPSStoreTransaction(serverUUID(), stored2));
db().executeTransaction(new TPSStoreTransaction(serverUUID(), stored3));
Optional<Long> result = db().query(TPSQueries.fetchLatestServerStartTime(serverUUID(), TimeUnit.MINUTES.toMillis(3)));
assertTrue(result.isPresent());
assertEquals(stored2.getDate(), result.get());
}
}

View File

@ -1,3 +1,8 @@
repositories {
maven { url = "https://repo.opencollab.dev/maven-snapshots/" } // Nukkit snapshot
maven { url = "https://repo.opencollab.dev/maven-releases/" } // Nukkit release
}
dependencies {
compileOnly project(":common")
implementation project(path: ":common", configuration: 'shadow')