Metrics
Some checks failed
Deploy API / deploy (ubuntu-latest, 2.44.0) (push) Failing after 8s

This commit is contained in:
Braydon 2024-09-12 18:25:32 -04:00
parent 56705af255
commit 6b33ac7330
12 changed files with 480 additions and 2 deletions

@ -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();
}

@ -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);

@ -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.
*/

@ -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