Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions http/http-server-common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ plugins {
dependencies {
annotationProcessor project(':config:config-annotation-processor')

compileOnly project(':micrometer:micrometer-module')

api project(':core:common')
api project(':logging:logging-common')
api project(':telemetry:telemetry-common')
api project(':http:http-common')

testImplementation project(':config:config-hocon')
testImplementation project(':micrometer:micrometer-module')
testImplementation libs.micrometer.registry.prometheus

testFixturesApi libs.okhttp
testFixturesImplementation libs.junit.jupiter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import io.koraframework.http.server.common.request.HttpServerRequest;
import io.koraframework.http.server.common.response.HttpServerResponse;
import io.koraframework.micrometer.module.VictoriaMetricsHistogram;
import io.koraframework.telemetry.common.TelemetryConfig;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.opentelemetry.semconv.ErrorAttributes;
import io.opentelemetry.semconv.HttpAttributes;
import io.opentelemetry.semconv.ServerAttributes;
Expand All @@ -24,7 +27,11 @@ public class DefaultHttpServerMetricsFactory {
public static final DefaultHttpServerMetricsFactory INSTANCE = new DefaultHttpServerMetricsFactory();

public DefaultHttpServerMetrics create(DefaultHttpServerTelemetry.TelemetryContext context) {
return new DefaultHttpServerMetrics(context);
return switch (context.config().metrics().mode()) {
case SUMMARY -> new SummaryHttpServerMetrics(context);
case SLO -> new DefaultHttpServerMetrics(context);
case VM -> new VictoriaMetricsHttpServerMetrics(context);
};
}

public static class DefaultHttpServerMetrics {
Expand Down Expand Up @@ -97,6 +104,15 @@ protected Timer.Builder createMetricServerDuration(DurationKey metricKey,
HttpServerRequest request,
HttpServerResponse response,
@Nullable Throwable throwable) {
return Timer.builder("http.server.request.duration")
.serviceLevelObjectives(this.context.config().metrics().slo())
.tags(createMetricServerDurationTags(metricKey, request, response, throwable));
}

protected Tags createMetricServerDurationTags(DurationKey metricKey,
HttpServerRequest request,
HttpServerResponse response,
@Nullable Throwable throwable) {
var extraTags = 0;
if (metricKey.extraTags != null) {
for (Tag _ : metricKey.extraTags) {
Expand All @@ -105,7 +121,7 @@ protected Timer.Builder createMetricServerDuration(DurationKey metricKey,
}
var staticTags = new ArrayList<Tag>(5 + this.context.config().metrics().tags().size() + extraTags);

var errorType = (throwable == null) ? "" : throwable.getClass().getCanonicalName();
var errorType = (metricKey.errorType == null) ? "" : metricKey.errorType.getCanonicalName();
staticTags.add(Tag.of(HttpAttributes.HTTP_REQUEST_METHOD.getKey(), request.method()));
staticTags.add(Tag.of(HttpAttributes.HTTP_ROUTE.getKey(), metricKey.pathTemplate()));
staticTags.add(Tag.of(UrlAttributes.URL_SCHEME.getKey(), request.scheme()));
Expand All @@ -121,9 +137,7 @@ protected Timer.Builder createMetricServerDuration(DurationKey metricKey,
}
}

return Timer.builder("http.server.request.duration")
.serviceLevelObjectives(this.context.config().metrics().slo())
.tags(Tags.of(staticTags));
return Tags.of(staticTags);
}

protected ActiveRequestsKey createMetricActiveRequestsGaugeKey(HttpServerRequest request) {
Expand Down Expand Up @@ -171,4 +185,60 @@ protected AtomicLong createMetricActiveRequests(ActiveRequestsKey metricKey, Htt
return value;
}
}

public static final class SummaryHttpServerMetrics extends DefaultHttpServerMetrics {

public SummaryHttpServerMetrics(DefaultHttpServerTelemetry.TelemetryContext context) {
super(context);
}

@Override
protected Timer.Builder createMetricServerDuration(DurationKey metricKey,
HttpServerRequest request,
HttpServerResponse response,
@Nullable Throwable throwable) {
return super.createMetricServerDuration(metricKey, request, response, throwable)
.serviceLevelObjectives();
}
}

public static final class VictoriaMetricsHttpServerMetrics extends DefaultHttpServerMetrics {

private final PrometheusMeterRegistry meterRegistry;
private final ConcurrentHashMap<DurationKey, VictoriaMetricsHistogram> requestDurationCache = new ConcurrentHashMap<>();

public VictoriaMetricsHttpServerMetrics(DefaultHttpServerTelemetry.TelemetryContext context) {
super(context);
var meterRegistry = context.meterRegistry();
if (!(meterRegistry instanceof PrometheusMeterRegistry prometheusMeterRegistry)) {
throw new IllegalStateException("%s=%s requires PrometheusMeterRegistry"
.formatted(TelemetryConfig.MetricsConfig.class.getSimpleName() + ".mode", TelemetryConfig.MetricsConfig.MetricsMode.VM));
}
this.meterRegistry = prometheusMeterRegistry;
}

@Override
public void recordEnd(HttpServerRequest request,
HttpServerResponse response,
@Nullable Throwable exception,
long processingTimeNanos) {
var key = createMetricServerDurationKey(request, response, exception);
var meter = this.requestDurationCache.computeIfAbsent(key, _ -> createVictoriaMetricsServerDuration(key, request, response, exception));
meter.record(processingTimeNanos, TimeUnit.NANOSECONDS);
createMetricActiveRequestsGaugeCounter(request).decrementAndGet();
}

private VictoriaMetricsHistogram createVictoriaMetricsServerDuration(DurationKey metricKey,
HttpServerRequest request,
HttpServerResponse response,
@Nullable Throwable throwable) {
return VictoriaMetricsHistogram.builder("http.server.request.duration")
.baseUnit("seconds")
.min(this.context.config().metrics().vm().min())
.max(this.context.config().metrics().vm().max())
.buckets(this.context.config().metrics().vm().buckets())
.tags(createMetricServerDurationTags(metricKey, request, response, throwable))
.register(this.meterRegistry);
}
}
}
3 changes: 3 additions & 0 deletions http/http-server-common/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
requires transitive kora.http.common;
requires transitive kora.logging.common;
requires transitive kora.telemetry.common;
requires transitive micrometer.core;
requires kora.micrometer.module;
requires micrometer.registry.prometheus;

exports io.koraframework.http.server.common;
exports io.koraframework.http.server.common.annotation;
Expand Down
10 changes: 5 additions & 5 deletions micrometer/micrometer-module/build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
dependencies {
annotationProcessor project(':config:config-annotation-processor')

compileOnly project(':resilient:resilient-kora')

api project(":telemetry:telemetry-common")
api libs.micrometer.core
api libs.micrometer.registry.prometheus
api libs.opentelemetry.api
api libs.opentelemetry.micrometer.meter.provider
api project(":telemetry:telemetry-common")

annotationProcessor project(':config:config-annotation-processor')

compileOnly project(':resilient:resilient-kora')
}
Loading
Loading