add basic metrics impl

This commit is contained in:
Lee 2024-04-14 09:34:10 +01:00
parent fc640fe1a0
commit e9da32775f
16 changed files with 346 additions and 9 deletions

17
pom.xml

@ -112,8 +112,23 @@
<artifactId>httpclient5</artifactId> <artifactId>httpclient5</artifactId>
<version>5.3.1</version> <version>5.3.1</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
</dependency>
<!-- InfluxDB Metrics -->
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-spring</artifactId>
<version>7.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-client-java</artifactId>
<version>7.0.0</version>
</dependency>
<!-- DNS Lookup --> <!-- DNS Lookup -->
<dependency> <dependency>

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

@ -3,6 +3,7 @@ package xyz.mcutils.backend.log;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; 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.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import xyz.mcutils.backend.common.IPUtils; 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.Arrays;
import java.util.HashMap; import java.util.HashMap;
@ -21,6 +25,10 @@ import java.util.Map.Entry;
@ControllerAdvice @ControllerAdvice
@Slf4j(topic = "Req Transaction") @Slf4j(topic = "Req Transaction")
public class TransactionLogger implements ResponseBodyAdvice<Object> { public class TransactionLogger implements ResponseBodyAdvice<Object> {
@Autowired
private MetricService metricService;
@Override @Override
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType,
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest rawRequest, @NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest rawRequest,
@ -43,6 +51,10 @@ public class TransactionLogger implements ResponseBodyAdvice<Object> {
request.getRequestURI(), request.getRequestURI(),
params params
)); ));
// Increment the metric
((TotalRequestsMetric) metricService.getMetric(TotalRequestsMetric.class)).increment();
((RequestsPerRouteMetric) metricService.getMetric(RequestsPerRouteMetric.class)).increment(request.getRequestURI());
return body; return body;
} }

@ -1,6 +1,7 @@
package xyz.mcutils.backend.repository; package xyz.mcutils.backend.repository;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedEndpointStatus; import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
/** /**
@ -8,4 +9,5 @@ import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
* *
* @author Braydon * @author Braydon
*/ */
@Repository
public interface EndpointStatusRepository extends CrudRepository<CachedEndpointStatus, String> { } public interface EndpointStatusRepository extends CrudRepository<CachedEndpointStatus, String> { }

@ -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<Metric<?>, String> { }

@ -1,6 +1,7 @@
package xyz.mcutils.backend.repository; package xyz.mcutils.backend.repository;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedMinecraftServer; import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
/** /**
@ -8,4 +9,5 @@ import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
* *
* @author Braydon * @author Braydon
*/ */
@Repository
public interface MinecraftServerCacheRepository extends CrudRepository<CachedMinecraftServer, String> { } public interface MinecraftServerCacheRepository extends CrudRepository<CachedMinecraftServer, String> { }

@ -1,6 +1,7 @@
package xyz.mcutils.backend.repository; package xyz.mcutils.backend.repository;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedPlayer; import xyz.mcutils.backend.model.cache.CachedPlayer;
import java.util.UUID; import java.util.UUID;
@ -10,4 +11,5 @@ import java.util.UUID;
* *
* @author Braydon * @author Braydon
*/ */
@Repository
public interface PlayerCacheRepository extends CrudRepository<CachedPlayer, UUID> { } public interface PlayerCacheRepository extends CrudRepository<CachedPlayer, UUID> { }

@ -1,6 +1,7 @@
package xyz.mcutils.backend.repository; package xyz.mcutils.backend.repository;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedPlayerName; import xyz.mcutils.backend.model.cache.CachedPlayerName;
/** /**
@ -12,4 +13,5 @@ import xyz.mcutils.backend.model.cache.CachedPlayerName;
* *
* @author Braydon * @author Braydon
*/ */
@Repository
public interface PlayerNameCacheRepository extends CrudRepository<CachedPlayerName, String> { } public interface PlayerNameCacheRepository extends CrudRepository<CachedPlayerName, String> { }

@ -1,6 +1,7 @@
package xyz.mcutils.backend.repository; package xyz.mcutils.backend.repository;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedPlayerSkinPart; 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. * player skin part by it's id.
* </p> * </p>
*/ */
@Repository
public interface PlayerSkinPartCacheRepository extends CrudRepository<CachedPlayerSkinPart, String> { } public interface PlayerSkinPartCacheRepository extends CrudRepository<CachedPlayerSkinPart, String> { }

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

@ -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<T> {
/**
* 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();
}

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

@ -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 <A, B> extends Metric<Map<A, B>> {
public MapMetric(String id) {
super(id, new HashMap<>());
}
@Override
public Point toPoint() {
Point point = Point.measurement(getId());
for (Map.Entry<A, B> entry : getValue().entrySet()) {
point.addField(entry.getKey().toString(), entry.getValue().toString());
}
return point;
}
}

@ -0,0 +1,19 @@
package xyz.mcutils.backend.service.metric.metrics;
import xyz.mcutils.backend.service.metric.impl.MapMetric;
public class RequestsPerRouteMetric extends MapMetric<String, Integer> {
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);
}
}

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

@ -4,20 +4,36 @@ server:
servlet: servlet:
context-path: / context-path: /
# The public URL of the application
public-url: http://localhost:80
# Spring Configuration # Spring Configuration
spring: spring:
# Don't include null properties in JSON
jackson:
default-property-inclusion: non_null
data: data:
# Redis - This is used for caching # Redis - This is used for caching
redis: redis:
host: "localhost" host: "127.0.0.1"
port: 6379 port: 6379
database: 0 database: 1
auth: "" # Leave blank for no auth auth: "" # Leave blank for no auth
# Don't serialize null values # Disable default metrics
jackson: management:
default-property-inclusion: non_null 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