diff --git a/pom.xml b/pom.xml index 380347b..01110f9 100644 --- a/pom.xml +++ b/pom.xml @@ -112,8 +112,23 @@ httpclient5 5.3.1 + + org.springframework.boot + spring-boot-actuator-autoconfigure + - + + + com.influxdb + influxdb-spring + 7.0.0 + compile + + + com.influxdb + influxdb-client-java + 7.0.0 + diff --git a/src/main/java/xyz/mcutils/backend/common/Timer.java b/src/main/java/xyz/mcutils/backend/common/Timer.java new file mode 100644 index 0000000..c07a7d9 --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/common/Timer.java @@ -0,0 +1,19 @@ +package xyz.mcutils.backend.common; + +public class Timer { + + /** + * Schedules a task to run after a delay. + * + * @param runnable the task to run + * @param delay the delay before the task runs + */ + public static void scheduleRepeating(Runnable runnable, long delay, long period) { + new java.util.Timer().scheduleAtFixedRate(new java.util.TimerTask() { + @Override + public void run() { + runnable.run(); + } + }, delay, period); + } +} diff --git a/src/main/java/xyz/mcutils/backend/log/TransactionLogger.java b/src/main/java/xyz/mcutils/backend/log/TransactionLogger.java index e971d6a..641e6ee 100644 --- a/src/main/java/xyz/mcutils/backend/log/TransactionLogger.java +++ b/src/main/java/xyz/mcutils/backend/log/TransactionLogger.java @@ -3,6 +3,7 @@ package xyz.mcutils.backend.log; import jakarta.servlet.http.HttpServletRequest; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; @@ -12,6 +13,9 @@ import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import xyz.mcutils.backend.common.IPUtils; +import xyz.mcutils.backend.service.MetricService; +import xyz.mcutils.backend.service.metric.metrics.RequestsPerRouteMetric; +import xyz.mcutils.backend.service.metric.metrics.TotalRequestsMetric; import java.util.Arrays; import java.util.HashMap; @@ -21,6 +25,10 @@ import java.util.Map.Entry; @ControllerAdvice @Slf4j(topic = "Req Transaction") public class TransactionLogger implements ResponseBodyAdvice { + + @Autowired + private MetricService metricService; + @Override public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, @NonNull Class> selectedConverterType, @NonNull ServerHttpRequest rawRequest, @@ -43,6 +51,10 @@ public class TransactionLogger implements ResponseBodyAdvice { request.getRequestURI(), params )); + + // Increment the metric + ((TotalRequestsMetric) metricService.getMetric(TotalRequestsMetric.class)).increment(); + ((RequestsPerRouteMetric) metricService.getMetric(RequestsPerRouteMetric.class)).increment(request.getRequestURI()); return body; } diff --git a/src/main/java/xyz/mcutils/backend/repository/EndpointStatusRepository.java b/src/main/java/xyz/mcutils/backend/repository/EndpointStatusRepository.java index 2645841..8293455 100644 --- a/src/main/java/xyz/mcutils/backend/repository/EndpointStatusRepository.java +++ b/src/main/java/xyz/mcutils/backend/repository/EndpointStatusRepository.java @@ -1,6 +1,7 @@ package xyz.mcutils.backend.repository; import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; import xyz.mcutils.backend.model.cache.CachedEndpointStatus; /** @@ -8,4 +9,5 @@ import xyz.mcutils.backend.model.cache.CachedEndpointStatus; * * @author Braydon */ +@Repository public interface EndpointStatusRepository extends CrudRepository { } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/repository/MetricsRepository.java b/src/main/java/xyz/mcutils/backend/repository/MetricsRepository.java new file mode 100644 index 0000000..7f51ae7 --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/repository/MetricsRepository.java @@ -0,0 +1,11 @@ +package xyz.mcutils.backend.repository; + +import org.springframework.data.repository.CrudRepository; +import xyz.mcutils.backend.service.metric.Metric; + +/** + * A repository for {@link Metric}s. + * + * @author Braydon + */ +public interface MetricsRepository extends CrudRepository, String> { } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/repository/MinecraftServerCacheRepository.java b/src/main/java/xyz/mcutils/backend/repository/MinecraftServerCacheRepository.java index 2fc8500..074cffa 100644 --- a/src/main/java/xyz/mcutils/backend/repository/MinecraftServerCacheRepository.java +++ b/src/main/java/xyz/mcutils/backend/repository/MinecraftServerCacheRepository.java @@ -1,6 +1,7 @@ package xyz.mcutils.backend.repository; import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; import xyz.mcutils.backend.model.cache.CachedMinecraftServer; /** @@ -8,4 +9,5 @@ import xyz.mcutils.backend.model.cache.CachedMinecraftServer; * * @author Braydon */ +@Repository public interface MinecraftServerCacheRepository extends CrudRepository { } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/repository/PlayerCacheRepository.java b/src/main/java/xyz/mcutils/backend/repository/PlayerCacheRepository.java index e28d11b..2b44f67 100644 --- a/src/main/java/xyz/mcutils/backend/repository/PlayerCacheRepository.java +++ b/src/main/java/xyz/mcutils/backend/repository/PlayerCacheRepository.java @@ -1,6 +1,7 @@ package xyz.mcutils.backend.repository; import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; import xyz.mcutils.backend.model.cache.CachedPlayer; import java.util.UUID; @@ -10,4 +11,5 @@ import java.util.UUID; * * @author Braydon */ +@Repository public interface PlayerCacheRepository extends CrudRepository { } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/repository/PlayerNameCacheRepository.java b/src/main/java/xyz/mcutils/backend/repository/PlayerNameCacheRepository.java index 1ff899f..d2a9399 100644 --- a/src/main/java/xyz/mcutils/backend/repository/PlayerNameCacheRepository.java +++ b/src/main/java/xyz/mcutils/backend/repository/PlayerNameCacheRepository.java @@ -1,6 +1,7 @@ package xyz.mcutils.backend.repository; import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; import xyz.mcutils.backend.model.cache.CachedPlayerName; /** @@ -12,4 +13,5 @@ import xyz.mcutils.backend.model.cache.CachedPlayerName; * * @author Braydon */ +@Repository public interface PlayerNameCacheRepository extends CrudRepository { } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/repository/PlayerSkinPartCacheRepository.java b/src/main/java/xyz/mcutils/backend/repository/PlayerSkinPartCacheRepository.java index a2dbb0a..dcbde91 100644 --- a/src/main/java/xyz/mcutils/backend/repository/PlayerSkinPartCacheRepository.java +++ b/src/main/java/xyz/mcutils/backend/repository/PlayerSkinPartCacheRepository.java @@ -1,6 +1,7 @@ package xyz.mcutils.backend.repository; import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; import xyz.mcutils.backend.model.cache.CachedPlayerSkinPart; /** @@ -10,4 +11,5 @@ import xyz.mcutils.backend.model.cache.CachedPlayerSkinPart; * player skin part by it's id. *

*/ +@Repository public interface PlayerSkinPartCacheRepository extends CrudRepository { } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/service/MetricService.java b/src/main/java/xyz/mcutils/backend/service/MetricService.java new file mode 100644 index 0000000..3c8b231 --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/service/MetricService.java @@ -0,0 +1,121 @@ +package xyz.mcutils.backend.service; + +import com.influxdb.client.WriteApiBlocking; +import com.influxdb.spring.influx.InfluxDB2AutoConfiguration; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import xyz.mcutils.backend.common.Timer; +import xyz.mcutils.backend.repository.MetricsRepository; +import xyz.mcutils.backend.service.metric.Metric; +import xyz.mcutils.backend.service.metric.metrics.RequestsPerRouteMetric; +import xyz.mcutils.backend.service.metric.metrics.TotalRequestsMetric; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Service @Log4j2 +public class MetricService { + /** + * The metrics that are registered. + */ + private final Map, Metric> metrics = new HashMap<>(); + + /** + * The interval in which the metrics are saved. + */ + private final long saveInterval = TimeUnit.MINUTES.toMillis(1); + + /** + * The interval in which the metrics are written to InfluxDB. + */ + private final long writeInfluxInterval = TimeUnit.SECONDS.toMillis(30); + + private final WriteApiBlocking influxWriteApi; + private final MetricsRepository metricsRepository; + + @Autowired + public MetricService(InfluxDB2AutoConfiguration influxAutoConfiguration, MetricsRepository metricsRepository) { + this.influxWriteApi = influxAutoConfiguration.influxDBClient().getWriteApiBlocking(); + this.metricsRepository = metricsRepository; + + // Register the metrics + registerMetric(new TotalRequestsMetric()); + registerMetric(new RequestsPerRouteMetric()); + + // Load the metrics from Redis + loadMetrics(); + + Timer.scheduleRepeating(this::saveMetrics, saveInterval, saveInterval); + Timer.scheduleRepeating(this::writeToInflux, writeInfluxInterval, writeInfluxInterval); + } + + /** + * Register a metric. + * + * @param metric the metric to register + */ + public void registerMetric(Metric metric) { + if (metrics.containsKey(metric.getClass())) { + throw new IllegalArgumentException("A metric with the class " + metric.getClass().getName() + " is already registered"); + } + metrics.put(metric.getClass(), metric); + } + + /** + * Get a metric by its class. + * + * @param clazz the class of the metric + * @return the metric + * @throws IllegalArgumentException if there is no metric with the class registered + */ + public Metric getMetric(Class clazz) throws IllegalArgumentException { + if (!metrics.containsKey(clazz)) { + throw new IllegalArgumentException("No metric with the class " + clazz.getName() + " is registered"); + } + return metrics.get(clazz); + } + + /** + * Load all metrics from Redis. + */ + public void loadMetrics() { + log.info("Loading metrics"); + for (Metric metric : metricsRepository.findAll()) { + metrics.put(metric.getClass(), metric); + } + log.info("Loaded {} metrics", metrics.size()); + } + + /** + * Save all metrics to Redis. + */ + private void saveMetrics() { + log.info("Saving metrics to Redis"); + for (Metric metric : metrics.values()) { + saveMetric(metric); + } + log.info("Saved {} metrics", metrics.size()); + } + + /** + * Save a metric to Redis. + * + * @param metric the metric to save + */ + private void saveMetric(Metric metric) { + metricsRepository.save(metric); // Save the metric to the repository + } + + /** + * Push all metrics to InfluxDB. + */ + private void writeToInflux() { + log.info("Writing metrics to InfluxDB"); + for (Metric metric : metrics.values()) { + influxWriteApi.writePoint(metric.toPoint()); + } + log.info("Wrote {} metrics", metrics.size()); + } +} diff --git a/src/main/java/xyz/mcutils/backend/service/metric/Metric.java b/src/main/java/xyz/mcutils/backend/service/metric/Metric.java new file mode 100644 index 0000000..7a5ed34 --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/service/metric/Metric.java @@ -0,0 +1,32 @@ +package xyz.mcutils.backend.service.metric; + +import com.influxdb.annotations.Measurement; +import com.influxdb.client.write.Point; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@AllArgsConstructor +@Getter @Setter +@RedisHash(value = "metric") +@Measurement(name = "metric") +public abstract class Metric { + /** + * The id of the metric. + */ + @Id private String id; + + /** + * The value of the metric. + */ + private T value; + + /** + * Gets this point as a {@link Point}. + * + * @return the point + */ + public abstract Point toPoint(); +} diff --git a/src/main/java/xyz/mcutils/backend/service/metric/impl/IntegerMetric.java b/src/main/java/xyz/mcutils/backend/service/metric/impl/IntegerMetric.java new file mode 100644 index 0000000..d8c3a2b --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/service/metric/impl/IntegerMetric.java @@ -0,0 +1,49 @@ +package xyz.mcutils.backend.service.metric.impl; + +import com.influxdb.client.write.Point; +import xyz.mcutils.backend.service.metric.Metric; + +public class IntegerMetric extends Metric { + + public IntegerMetric(String id) { + super(id, 0); + } + + /** + * Increment the value of this metric. + * + * @param amount the amount to increment by + */ + public void increment(int amount) { + setValue(getValue() + amount); + } + + /** + * Increment the value of this metric by 1. + */ + public void increment() { + increment(1); + } + + /** + * Decrement the value of this metric. + * + * @param amount the amount to decrement by + */ + public void decrement(int amount) { + setValue(getValue() - amount); + } + + /** + * Decrement the value of this metric by 1. + */ + public void decrement() { + decrement(1); + } + + @Override + public Point toPoint() { + return Point.measurement(getId()) + .addField("value", getValue()); + } +} diff --git a/src/main/java/xyz/mcutils/backend/service/metric/impl/MapMetric.java b/src/main/java/xyz/mcutils/backend/service/metric/impl/MapMetric.java new file mode 100644 index 0000000..1f52a8a --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/service/metric/impl/MapMetric.java @@ -0,0 +1,23 @@ +package xyz.mcutils.backend.service.metric.impl; + +import com.influxdb.client.write.Point; +import xyz.mcutils.backend.service.metric.Metric; + +import java.util.HashMap; +import java.util.Map; + +public class MapMetric extends Metric> { + + public MapMetric(String id) { + super(id, new HashMap<>()); + } + + @Override + public Point toPoint() { + Point point = Point.measurement(getId()); + for (Map.Entry entry : getValue().entrySet()) { + point.addField(entry.getKey().toString(), entry.getValue().toString()); + } + return point; + } +} diff --git a/src/main/java/xyz/mcutils/backend/service/metric/metrics/RequestsPerRouteMetric.java b/src/main/java/xyz/mcutils/backend/service/metric/metrics/RequestsPerRouteMetric.java new file mode 100644 index 0000000..867d411 --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/service/metric/metrics/RequestsPerRouteMetric.java @@ -0,0 +1,19 @@ +package xyz.mcutils.backend.service.metric.metrics; + +import xyz.mcutils.backend.service.metric.impl.MapMetric; + +public class RequestsPerRouteMetric extends MapMetric { + + public RequestsPerRouteMetric() { + super("requests_per_route"); + } + + /** + * Increment the value for this route. + * + * @param route the route to increment + */ + public void increment(String route) { + getValue().put(route, getValue().getOrDefault(route, 0) + 1); + } +} diff --git a/src/main/java/xyz/mcutils/backend/service/metric/metrics/TotalRequestsMetric.java b/src/main/java/xyz/mcutils/backend/service/metric/metrics/TotalRequestsMetric.java new file mode 100644 index 0000000..8c1f087 --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/service/metric/metrics/TotalRequestsMetric.java @@ -0,0 +1,10 @@ +package xyz.mcutils.backend.service.metric.metrics; + +import xyz.mcutils.backend.service.metric.impl.IntegerMetric; + +public class TotalRequestsMetric extends IntegerMetric { + + public TotalRequestsMetric() { + super("total_requests"); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d2ce0dd..09a8b87 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,20 +4,36 @@ server: servlet: context-path: / -# The public URL of the application -public-url: http://localhost:80 - # Spring Configuration spring: + # Don't include null properties in JSON + jackson: + default-property-inclusion: non_null data: # Redis - This is used for caching redis: - host: "localhost" + host: "127.0.0.1" port: 6379 - database: 0 + database: 1 auth: "" # Leave blank for no auth - # Don't serialize null values - jackson: - default-property-inclusion: non_null +# Disable default metrics +management: + endpoints: + web: + exposure: + exclude: + - "*" + influx: + metrics: + export: + enabled: false +# InfluxDB Configuration +influx: + url: http://localhost + token: token + org: org + bucket: bucket + +public-url: http://localhost \ No newline at end of file