This commit is contained in:
parent
56705af255
commit
6b33ac7330
14
API/pom.xml
14
API/pom.xml
@ -105,6 +105,20 @@
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Error Reporting & Metrics -->
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
|
||||
<version>8.0.0-alpha.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.questdb</groupId>
|
||||
<artifactId>questdb</artifactId>
|
||||
<version>8.1.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.konghq</groupId>
|
||||
<artifactId>unirest-java-core</artifactId>
|
||||
|
@ -0,0 +1,16 @@
|
||||
package me.braydon.tether.common;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
public interface IOperatingSystemMXBean {
|
||||
/**
|
||||
* Get the system's CPU usage for the entire system.
|
||||
* If the value is 0.0, all cores are idle. If the
|
||||
* value is 1.0, all cores were 100% utilized. If
|
||||
* the value is negative, the usage is not available.
|
||||
*
|
||||
* @return the system cpu load
|
||||
*/
|
||||
double getSystemCpuLoad();
|
||||
}
|
44
API/src/main/java/me/braydon/tether/common/IPUtils.java
Normal file
44
API/src/main/java/me/braydon/tether/common/IPUtils.java
Normal file
@ -0,0 +1,44 @@
|
||||
package me.braydon.tether.common;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@UtilityClass
|
||||
public final class IPUtils {
|
||||
private static final String[] IP_HEADERS = new String[] {
|
||||
"CF-Connecting-IP",
|
||||
"X-Forwarded-For"
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the real IP from the given request.
|
||||
*
|
||||
* @param request the request
|
||||
* @return the real IP
|
||||
*/
|
||||
@NonNull
|
||||
public static String getRealIp(@NonNull HttpServletRequest request) {
|
||||
String ip = request.getRemoteAddr();
|
||||
for (String headerName : IP_HEADERS) {
|
||||
String header = request.getHeader(headerName);
|
||||
if (header == null) {
|
||||
continue;
|
||||
}
|
||||
if (!header.contains(",")) { // Handle single IP
|
||||
ip = header;
|
||||
break;
|
||||
}
|
||||
// Handle multiple IPs
|
||||
String[] ips = header.split(",");
|
||||
for (String ipHeader : ips) {
|
||||
ip = ipHeader;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
@ -5,6 +5,9 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.braydon.tether.common.IPUtils;
|
||||
import me.braydon.tether.metric.impl.RequestsMetric;
|
||||
import me.braydon.tether.service.MetricsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@ -24,11 +27,21 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
@ControllerAdvice
|
||||
@Slf4j(topic = "Req/Res Transaction")
|
||||
public class RequestLogger implements ResponseBodyAdvice<Object> {
|
||||
/**
|
||||
* The metrics service to use.
|
||||
*/
|
||||
@NonNull private final MetricsService metricsService;
|
||||
|
||||
@Autowired
|
||||
public RequestLogger(@NonNull MetricsService metricsService) {
|
||||
this.metricsService = metricsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
@ -36,6 +49,11 @@ public class RequestLogger implements ResponseBodyAdvice<Object> {
|
||||
HttpServletRequest request = ((ServletServerHttpRequest) rawRequest).getServletRequest();
|
||||
HttpServletResponse response = ((ServletServerHttpResponse) rawResponse).getServletResponse();
|
||||
|
||||
// Metrics
|
||||
if (metricsService.isEnabled()) {
|
||||
RequestsMetric.incrementCodeCount(response.getStatus());
|
||||
}
|
||||
|
||||
// Get the request ip ip
|
||||
String ip = IPUtils.getRealIp(request);
|
||||
|
||||
|
41
API/src/main/java/me/braydon/tether/metric/Metric.java
Normal file
41
API/src/main/java/me/braydon/tether/metric/Metric.java
Normal file
@ -0,0 +1,41 @@
|
||||
package me.braydon.tether.metric;
|
||||
|
||||
import io.questdb.client.Sender;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import me.braydon.tether.common.EnvironmentUtils;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@RequiredArgsConstructor @Setter @Getter
|
||||
public abstract class Metric {
|
||||
/**
|
||||
* The interval (in millis) at which
|
||||
* this metric should be tracked.
|
||||
* <p>
|
||||
* If the interval is -1, it will not
|
||||
* be automatically tracked.
|
||||
* </p>
|
||||
*/
|
||||
private final long interval;
|
||||
|
||||
/**
|
||||
* The unix time of when this metric was last tracked.
|
||||
*/
|
||||
private long lastTrack;
|
||||
|
||||
@NonNull
|
||||
public static Sender applyEnvColumn(@NonNull Sender sender) {
|
||||
return sender.stringColumn("env", EnvironmentUtils.isProduction() ? "production" : "staging");
|
||||
}
|
||||
|
||||
/**
|
||||
* Track this metric.
|
||||
*
|
||||
* @param sender the sender to use
|
||||
*/
|
||||
public abstract void track(@NonNull Sender sender);
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package me.braydon.tether.metric.impl;
|
||||
|
||||
import io.questdb.client.Sender;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import me.braydon.tether.common.IOperatingSystemMXBean;
|
||||
import me.braydon.tether.metric.Metric;
|
||||
|
||||
import javax.management.JMX;
|
||||
import javax.management.ObjectName;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* This metric is responsible for tracking
|
||||
* the statistics of this application.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Log4j2(topic = "App Statistics Collector")
|
||||
public final class AppStatisticsMetric extends Metric {
|
||||
/**
|
||||
* The JMX to use for getting the system's CPU load, if available.
|
||||
*/
|
||||
private IOperatingSystemMXBean jmx;
|
||||
|
||||
public AppStatisticsMetric() {
|
||||
super(TimeUnit.SECONDS.toMillis(10L));
|
||||
try {
|
||||
jmx = JMX.newMXBeanProxy(
|
||||
ManagementFactory.getPlatformMBeanServer(),
|
||||
ObjectName.getInstance("java.lang:type=OperatingSystem"),
|
||||
IOperatingSystemMXBean.class
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
log.error("OperatingSystemMXBean is not supported by the system, the system CPU usage won't be collected", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track this metric.
|
||||
*
|
||||
* @param sender the sender to use
|
||||
*/
|
||||
@Override
|
||||
public void track(@NonNull Sender sender) {
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
applyEnvColumn(sender.table("app-statistics")
|
||||
.doubleColumn("systemCpu", jmx.getSystemCpuLoad() * 100D)
|
||||
.longColumn("usedMemory", (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024))
|
||||
.longColumn("freeMemory", runtime.freeMemory() / (1024 * 1024))
|
||||
.longColumn("totalMemory", runtime.maxMemory() / (1024 * 1024))
|
||||
.longColumn("uptime", ManagementFactory.getRuntimeMXBean().getUptime()))
|
||||
.atNow();
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package me.braydon.tether.metric.impl;
|
||||
|
||||
import io.questdb.client.Sender;
|
||||
import lombok.NonNull;
|
||||
import me.braydon.tether.metric.Metric;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
public final class RequestsMetric extends Metric {
|
||||
private static final Map<Integer, Integer> codeCounts = new HashMap<>();
|
||||
|
||||
public RequestsMetric() {
|
||||
super(TimeUnit.SECONDS.toMillis(10L));
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the request
|
||||
* count for the given code.
|
||||
*
|
||||
* @param code the request code
|
||||
*/
|
||||
public static void incrementCodeCount(int code) {
|
||||
codeCounts.merge(code, 1, Integer::sum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track this metric.
|
||||
*
|
||||
* @param sender the sender to use
|
||||
*/
|
||||
@Override
|
||||
public void track(@NonNull Sender sender) {
|
||||
if (codeCounts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sender = applyEnvColumn(sender.table("requests"));
|
||||
for (Map.Entry<Integer, Integer> entry : codeCounts.entrySet()) {
|
||||
sender.longColumn(String.valueOf(entry.getKey()), entry.getValue());
|
||||
}
|
||||
sender.atNow();
|
||||
codeCounts.clear();
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package me.braydon.tether.metric.impl;
|
||||
|
||||
import io.questdb.client.Sender;
|
||||
import lombok.NonNull;
|
||||
import me.braydon.tether.metric.Metric;
|
||||
import me.braydon.tether.service.DiscordService;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
public final class TrackedUsersMetric extends Metric {
|
||||
/**
|
||||
* The amount of recently watched users.
|
||||
*/
|
||||
private static int recentlyWatchedUsers;
|
||||
|
||||
/**
|
||||
* The Discord service to use.
|
||||
*/
|
||||
@NonNull private final DiscordService discordService;
|
||||
|
||||
public TrackedUsersMetric(@NonNull DiscordService discordService) {
|
||||
super(TimeUnit.SECONDS.toMillis(10L));
|
||||
this.discordService = discordService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the amount of recently watched users.
|
||||
*/
|
||||
public static void incrementRecentlyWatchedUsers() {
|
||||
recentlyWatchedUsers++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track this metric.
|
||||
*
|
||||
* @param sender the sender to use
|
||||
*/
|
||||
@Override
|
||||
public void track(@NonNull Sender sender) {
|
||||
applyEnvColumn(sender.table("tracked-users")
|
||||
.longColumn("current", discordService.getTrackedUsers())
|
||||
.longColumn("recent", recentlyWatchedUsers))
|
||||
.atNow();
|
||||
recentlyWatchedUsers = 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package me.braydon.tether.metric.impl;
|
||||
|
||||
import io.questdb.client.Sender;
|
||||
import lombok.NonNull;
|
||||
import me.braydon.tether.metric.Metric;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
public final class UserLookupTimingsMetric extends Metric {
|
||||
private static Sender sender;
|
||||
|
||||
public UserLookupTimingsMetric(@NonNull Sender sender) {
|
||||
super(-1);
|
||||
UserLookupTimingsMetric.sender = sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track this metric.
|
||||
*
|
||||
* @param timings the timings
|
||||
*/
|
||||
public static void track(long timings) {
|
||||
applyEnvColumn(sender.table("timings")
|
||||
.stringColumn("type", "user-lookup")
|
||||
.longColumn("value", timings))
|
||||
.atNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Track this metric.
|
||||
*
|
||||
* @param sender the sender to use
|
||||
*/
|
||||
@Override
|
||||
public void track(@NonNull Sender sender) {}
|
||||
}
|
@ -14,6 +14,8 @@ import lombok.extern.log4j.Log4j2;
|
||||
import me.braydon.tether.exception.impl.BadRequestException;
|
||||
import me.braydon.tether.exception.impl.ResourceNotFoundException;
|
||||
import me.braydon.tether.exception.impl.ServiceUnavailableException;
|
||||
import me.braydon.tether.metric.impl.TrackedUsersMetric;
|
||||
import me.braydon.tether.metric.impl.UserLookupTimingsMetric;
|
||||
import me.braydon.tether.model.response.DiscordUserResponse;
|
||||
import me.braydon.tether.model.user.CachedDiscordUser;
|
||||
import me.braydon.tether.model.user.DiscordUser;
|
||||
@ -23,9 +25,12 @@ import net.dv8tion.jda.api.entities.Activity;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.SelfUser;
|
||||
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
|
||||
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -40,7 +45,7 @@ import java.util.concurrent.TimeUnit;
|
||||
*/
|
||||
@Service
|
||||
@Log4j2(topic = "Discord")
|
||||
public final class DiscordService {
|
||||
public final class DiscordService extends ListenerAdapter {
|
||||
/**
|
||||
* A cache of users retrieved from Discord.
|
||||
*/
|
||||
@ -48,12 +53,24 @@ public final class DiscordService {
|
||||
.expireAfterWrite(3L, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* The token to use for the bot.
|
||||
*/
|
||||
@Value("${discord.bot-token}")
|
||||
private String botToken;
|
||||
|
||||
/**
|
||||
* The token to use for the user account.
|
||||
*/
|
||||
@Value("${discord.user-account-token}")
|
||||
private String userAccountToken;
|
||||
|
||||
/**
|
||||
* Are metrics enabled?
|
||||
*/
|
||||
@Value("${questdb.enabled}")
|
||||
private boolean metricsEnabled;
|
||||
|
||||
/**
|
||||
* The current instance of the Discord bot.
|
||||
*/
|
||||
@ -64,6 +81,13 @@ public final class DiscordService {
|
||||
connectBot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildMemberJoin(@NotNull GuildMemberJoinEvent event) {
|
||||
if (metricsEnabled) {
|
||||
TrackedUsersMetric.incrementRecentlyWatchedUsers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Discord user by their snowflake.
|
||||
*
|
||||
@ -103,7 +127,11 @@ public final class DiscordService {
|
||||
CachedDiscordUser cachedUser = cachedUsers.getIfPresent(snowflake);
|
||||
boolean fromCache = cachedUser != null;
|
||||
if (cachedUser == null) { // No cache, retrieve fresh data
|
||||
long before = System.currentTimeMillis();
|
||||
cachedUser = new CachedDiscordUser(getUser(snowflake, member != null), System.currentTimeMillis());
|
||||
if (metricsEnabled) {
|
||||
UserLookupTimingsMetric.track(System.currentTimeMillis() - before);
|
||||
}
|
||||
cachedUsers.put(snowflake, cachedUser);
|
||||
}
|
||||
|
||||
@ -121,6 +149,20 @@ public final class DiscordService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the amount of users
|
||||
* the bot is tracking.
|
||||
*
|
||||
* @return the tracked user count
|
||||
*/
|
||||
public int getTrackedUsers() {
|
||||
int tracked = 0;
|
||||
for (Guild guild : jda.getGuilds()) {
|
||||
tracked += guild.getMemberCount();
|
||||
}
|
||||
return tracked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the bot to the Discord API.
|
||||
*/
|
||||
|
102
API/src/main/java/me/braydon/tether/service/MetricsService.java
Normal file
102
API/src/main/java/me/braydon/tether/service/MetricsService.java
Normal file
@ -0,0 +1,102 @@
|
||||
package me.braydon.tether.service;
|
||||
|
||||
import io.questdb.client.Sender;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import me.braydon.tether.metric.Metric;
|
||||
import me.braydon.tether.metric.impl.AppStatisticsMetric;
|
||||
import me.braydon.tether.metric.impl.RequestsMetric;
|
||||
import me.braydon.tether.metric.impl.TrackedUsersMetric;
|
||||
import me.braydon.tether.metric.impl.UserLookupTimingsMetric;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@Service @Log4j2(topic = "Metrics")
|
||||
public final class MetricsService {
|
||||
/**
|
||||
* The Discord service some metrics use.
|
||||
*/
|
||||
@NonNull private final DiscordService discordService;
|
||||
|
||||
/**
|
||||
* The metrics to automatically track.
|
||||
*/
|
||||
@NonNull private final List<Metric> metrics = new LinkedList<>();
|
||||
|
||||
/**
|
||||
* Are metrics enabled?
|
||||
*/
|
||||
@Value("${questdb.enabled}")
|
||||
@Getter private boolean enabled;
|
||||
|
||||
/**
|
||||
* The URI to the metrics server.
|
||||
*/
|
||||
@Value("${questdb.uri}")
|
||||
private String dbUrl;
|
||||
|
||||
/**
|
||||
* The sender client.
|
||||
*/
|
||||
private Sender sender;
|
||||
|
||||
@Autowired
|
||||
public MetricsService(@NonNull DiscordService discordService) {
|
||||
this.discordService = discordService;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void onInitialize() {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
// Initialize the sender, and schedule a task to start tracking
|
||||
sender = Sender.fromConfig(dbUrl);
|
||||
log.info("Sender Configured!");
|
||||
|
||||
// Register metrics
|
||||
metrics.add(new AppStatisticsMetric());
|
||||
metrics.add(new TrackedUsersMetric(discordService));
|
||||
metrics.add(new RequestsMetric());
|
||||
new UserLookupTimingsMetric(sender);
|
||||
log.info("Tracking {} metrics...", metrics.size());
|
||||
|
||||
new Timer().scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
trackMetrics();
|
||||
}
|
||||
}, 1000L, 1000L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track the registered metrics.
|
||||
*/
|
||||
private void trackMetrics() {
|
||||
int points = 0;
|
||||
for (Metric metric : metrics) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (metric.getInterval() == -1L || (now - metric.getLastTrack()) < metric.getInterval()) {
|
||||
continue;
|
||||
}
|
||||
metric.setLastTrack(now);
|
||||
metric.track(sender);
|
||||
points++;
|
||||
}
|
||||
// Record the amount of points recorded a second
|
||||
Metric.applyEnvColumn(sender.table("data-points")
|
||||
.longColumn("value", points))
|
||||
.atNow();
|
||||
}
|
||||
}
|
@ -8,6 +8,12 @@ logging:
|
||||
file:
|
||||
path: "./logs"
|
||||
|
||||
# Sentry Configuration
|
||||
sentry:
|
||||
dsn: "CHANGE_ME"
|
||||
tracesSampleRate: 1.0
|
||||
environment: "development"
|
||||
|
||||
# Discord Configuration
|
||||
discord:
|
||||
# The user account token also for general API calls
|
||||
@ -16,6 +22,11 @@ discord:
|
||||
# The bot token for realtime API calls (online status, activities, etc)
|
||||
bot-token: "CHANGE_ME"
|
||||
|
||||
# QuestDB Configuration (Metrics)
|
||||
questdb:
|
||||
enabled: false
|
||||
uri: "http::addr=localhost:9000;username=tether;password=p4$$w0rd;auto_flush_interval=5000;"
|
||||
|
||||
# Spring Configuration
|
||||
spring:
|
||||
# Ignore
|
||||
|
Loading…
x
Reference in New Issue
Block a user