From 9e54b9ea2bd4a75175a861b3843e3e11755eb883 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 8 Sep 2025 19:28:31 +0100 Subject: [PATCH] Add human-readable HTTP client stats (#134296) Today the timestamps in the HTTP client stats output are not rendered as proper human-readable timestamps even when `?human=true` is specified. This commit adds the missing human-readable variants of these fields. --- docs/changelog/134296.yaml | 5 ++ .../org/elasticsearch/http/HttpStats.java | 17 ++++-- .../http/HttpClientStatsTrackerTests.java | 57 +++++++++++++++++++ 3 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/134296.yaml diff --git a/docs/changelog/134296.yaml b/docs/changelog/134296.yaml new file mode 100644 index 0000000000000..b7ac7ea6bfd47 --- /dev/null +++ b/docs/changelog/134296.yaml @@ -0,0 +1,5 @@ +pr: 134296 +summary: Add human-readable HTTP client stats +area: Network +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/http/HttpStats.java b/server/src/main/java/org/elasticsearch/http/HttpStats.java index d4df9c8f59a20..f9ccc4f90b7d2 100644 --- a/server/src/main/java/org/elasticsearch/http/HttpStats.java +++ b/server/src/main/java/org/elasticsearch/http/HttpStats.java @@ -15,7 +15,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.ToXContentFragment; +import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; @@ -89,8 +89,11 @@ static final class Fields { static final String CLIENT_LOCAL_ADDRESS = "local_address"; static final String CLIENT_REMOTE_ADDRESS = "remote_address"; static final String CLIENT_LAST_URI = "last_uri"; + static final String CLIENT_OPENED_TIME = "opened_time"; static final String CLIENT_OPENED_TIME_MILLIS = "opened_time_millis"; + static final String CLIENT_CLOSED_TIME = "closed_time"; static final String CLIENT_CLOSED_TIME_MILLIS = "closed_time_millis"; + static final String CLIENT_LAST_REQUEST_TIME = "last_request_time"; static final String CLIENT_LAST_REQUEST_TIME_MILLIS = "last_request_time_millis"; static final String CLIENT_REQUEST_COUNT = "request_count"; static final String CLIENT_REQUEST_SIZE_BYTES = "request_size_bytes"; @@ -136,7 +139,7 @@ public record ClientStats( long lastRequestTimeMillis, long requestCount, long requestSizeBytes - ) implements Writeable, ToXContentFragment { + ) implements Writeable, ToXContentObject { public static final long NOT_CLOSED = -1L; @@ -179,11 +182,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (opaqueId != null) { builder.field(Fields.CLIENT_OPAQUE_ID, opaqueId); } - builder.field(Fields.CLIENT_OPENED_TIME_MILLIS, openedTimeMillis); + builder.timestampFieldsFromUnixEpochMillis(Fields.CLIENT_OPENED_TIME_MILLIS, Fields.CLIENT_OPENED_TIME, openedTimeMillis); if (closedTimeMillis != NOT_CLOSED) { - builder.field(Fields.CLIENT_CLOSED_TIME_MILLIS, closedTimeMillis); + builder.timestampFieldsFromUnixEpochMillis(Fields.CLIENT_CLOSED_TIME_MILLIS, Fields.CLIENT_CLOSED_TIME, closedTimeMillis); } - builder.field(Fields.CLIENT_LAST_REQUEST_TIME_MILLIS, lastRequestTimeMillis); + builder.timestampFieldsFromUnixEpochMillis( + Fields.CLIENT_LAST_REQUEST_TIME_MILLIS, + Fields.CLIENT_LAST_REQUEST_TIME, + lastRequestTimeMillis + ); builder.field(Fields.CLIENT_REQUEST_COUNT, requestCount); builder.field(Fields.CLIENT_REQUEST_SIZE_BYTES, requestSizeBytes); builder.endObject(); diff --git a/server/src/test/java/org/elasticsearch/http/HttpClientStatsTrackerTests.java b/server/src/test/java/org/elasticsearch/http/HttpClientStatsTrackerTests.java index a1129e4a717fd..d3a2e12c5aaf9 100644 --- a/server/src/test/java/org/elasticsearch/http/HttpClientStatsTrackerTests.java +++ b/server/src/test/java/org/elasticsearch/http/HttpClientStatsTrackerTests.java @@ -9,11 +9,14 @@ package org.elasticsearch.http; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.TimeValue; import org.elasticsearch.node.Node; import org.elasticsearch.rest.RestRequest; @@ -22,8 +25,13 @@ import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.threadpool.DefaultBuiltInExecutorBuilders; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xcontent.XContentType; +import java.io.IOException; import java.net.InetSocketAddress; +import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -379,6 +387,55 @@ public void testClearsStatsIfDisabledConcurrently() throws InterruptedException } } + public void testToXContent() throws IOException { + final var clientStats = new HttpStats.ClientStats( + randomNonNegativeInt(), + randomIdentifier(), + randomIdentifier(), + randomIdentifier(), + randomIdentifier(), + randomIdentifier(), + randomIdentifier(), + randomLongBetween(1, Long.MAX_VALUE), + randomLongBetween(1, Long.MAX_VALUE), + randomLongBetween(1, Long.MAX_VALUE), + randomNonNegativeLong(), + randomNonNegativeLong() + ); + final var description = Strings.toString(clientStats, false, true); + logger.info("description: {}", description); + + final Map xcontentMap; + try (var builder = XContentFactory.jsonBuilder()) { + builder.humanReadable(true).value(clientStats, ToXContent.EMPTY_PARAMS); + xcontentMap = XContentHelper.convertToMap(BytesReference.bytes(builder), false, XContentType.JSON).v2(); + } + + assertEquals(description, clientStats.id(), xcontentMap.get("id")); + assertEquals(description, clientStats.agent(), xcontentMap.get("agent")); + assertEquals(description, clientStats.localAddress(), xcontentMap.get("local_address")); + assertEquals(description, clientStats.remoteAddress(), xcontentMap.get("remote_address")); + assertEquals(description, clientStats.lastUri(), xcontentMap.get("last_uri")); + assertEquals(description, clientStats.forwardedFor(), xcontentMap.get("x_forwarded_for")); + assertEquals(description, clientStats.opaqueId(), xcontentMap.get("x_opaque_id")); + + assertEquals(description, clientStats.openedTimeMillis(), xcontentMap.get("opened_time_millis")); + assertEquals(description, Instant.ofEpochMilli(clientStats.openedTimeMillis()).toString(), xcontentMap.get("opened_time")); + + assertEquals(description, clientStats.closedTimeMillis(), xcontentMap.get("closed_time_millis")); + assertEquals(description, Instant.ofEpochMilli(clientStats.closedTimeMillis()).toString(), xcontentMap.get("closed_time")); + + assertEquals(description, clientStats.lastRequestTimeMillis(), xcontentMap.get("last_request_time_millis")); + assertEquals( + description, + Instant.ofEpochMilli(clientStats.lastRequestTimeMillis()).toString(), + xcontentMap.get("last_request_time") + ); + + assertEquals(description, clientStats.requestCount(), (long) xcontentMap.get("request_count")); + assertEquals(description, clientStats.requestSizeBytes(), (long) xcontentMap.get("request_size_bytes")); + } + private Map getRelevantHeaders(HttpRequest httpRequest) { final Map headers = Maps.newMapWithExpectedSize(4); final String[] relevantHeaderNames = new String[] { "user-agent", "x-elastic-product-origin", "x-forwarded-for", "x-opaque-id" };