From 58ea7229259396bfe26b3b7e8790ef42caba211e Mon Sep 17 00:00:00 2001 From: Arkesh Mishra <118651144+arkmish@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:39:12 +0530 Subject: [PATCH] Add client-side metrics to the ZooKeeper Java client Introduce ClientMetrics (mirroring ServerMetrics) and wire the existing pluggable MetricsProvider framework into the Java client. Client metrics are opt-in: the default provider is NullMetricsProvider, so there is no overhead unless explicitly enabled via the new zookeeper.clientMetricsProvider.className / .* client properties. ClientCnxn now emits, from a single request funnel and the SendThread lifecycle: - per-operation request latency and count - unsuccessful request counts labelled by KeeperException.Code - ping round-trip time and connect latency - connection established / loss / reconnect and session-expired counters The provider is a per-JVM singleton shared by all ZooKeeper instances so that, e.g., a Prometheus provider does not bind the same port twice. Adds unit and integration tests and a Client Metrics section to the monitor guide. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../resources/markdown/zookeeperMonitor.md | 39 +++ .../java/org/apache/zookeeper/ClientCnxn.java | 42 ++- .../zookeeper/client/ClientMetrics.java | 243 ++++++++++++++++++ .../zookeeper/client/ZKClientConfig.java | 51 ++++ .../client/ClientMetricsIntegrationTest.java | 121 +++++++++ .../zookeeper/client/ClientMetricsTest.java | 103 ++++++++ .../client/ZKClientConfigMetricsTest.java | 61 +++++ 7 files changed, 659 insertions(+), 1 deletion(-) create mode 100644 zookeeper-server/src/main/java/org/apache/zookeeper/client/ClientMetrics.java create mode 100644 zookeeper-server/src/test/java/org/apache/zookeeper/client/ClientMetricsIntegrationTest.java create mode 100644 zookeeper-server/src/test/java/org/apache/zookeeper/client/ClientMetricsTest.java create mode 100644 zookeeper-server/src/test/java/org/apache/zookeeper/client/ZKClientConfigMetricsTest.java diff --git a/zookeeper-docs/src/main/resources/markdown/zookeeperMonitor.md b/zookeeper-docs/src/main/resources/markdown/zookeeperMonitor.md index 3265aeff0d2..537edb18aba 100644 --- a/zookeeper-docs/src/main/resources/markdown/zookeeperMonitor.md +++ b/zookeeper-docs/src/main/resources/markdown/zookeeperMonitor.md @@ -18,6 +18,7 @@ limitations under the License. * [New Metrics System](#Metrics-System) * [Metrics](#Metrics) + * [Client Metrics](#Client-Metrics) * [Prometheus](#Prometheus) * [Grafana](#Grafana) @@ -37,6 +38,44 @@ client, security, failures, watch/session, requestProcessor, and so forth. ### Metrics All the metrics are included in the `ServerMetrics.java`. + + +### Client Metrics +In addition to the server-side metrics above, the Java client can emit **client-side metrics** that +give visibility into behavior observed from inside the application process (per-operation counts and +latency, error counts by result code, ping round-trip time, and connection/session lifecycle events). +All client metrics are defined in `ClientMetrics.java`. + +Client metrics are **opt-in** and use the same pluggable `MetricsProvider` system as the server. By +default the client uses the `NullMetricsProvider`, so there is no overhead unless the feature is +explicitly enabled. To enable it, set the metrics provider class name (and any provider-specific +configuration) via system properties: + +``` +# in-memory provider; values can be read programmatically via MetricsProvider#dump +-Dzookeeper.clientMetricsProvider.className=org.apache.zookeeper.metrics.impl.DefaultMetricsProvider + +# any property under this prefix is passed to the provider with the prefix stripped +-Dzookeeper.clientMetricsProvider.= +``` + +The metrics provider is a per-JVM singleton shared by all `ZooKeeper` client instances in the +process: the first client to configure a real provider wins, so multiple clients do not, for +example, attempt to bind the same Prometheus HTTP port twice. + +The main client metrics include: + +| Metric | Type | Description | +|---|---|---| +| `_request_latency` | Summary (per operation) | Latency of completed requests; `cnt__request_latency` also gives the per-operation request count | +| `_unsuccessful_request` | Summary (per error code) | Count of failed requests, labelled by `KeeperException.Code` | +| `ping_latency` | Summary | Client-observed ping round-trip time (ms) | +| `connect_latency` | Summary | Time to establish a connection to a server (ms) | +| `connection_established_count` | Counter | Number of connections successfully established | +| `connection_loss_count` | Counter | Number of times an established connection was lost | +| `reconnect_count` | Counter | Number of reconnection attempts | +| `session_expired_count` | Counter | Number of session expirations detected by the client | + ### Prometheus diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java index c3f0079c6e9..af3b2136c16 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java @@ -66,6 +66,7 @@ import org.apache.zookeeper.ZooDefs.OpCode; import org.apache.zookeeper.ZooKeeper.States; import org.apache.zookeeper.ZooKeeper.WatchRegistration; +import org.apache.zookeeper.client.ClientMetrics; import org.apache.zookeeper.client.HostProvider; import org.apache.zookeeper.client.ZKClientConfig; import org.apache.zookeeper.client.ZooKeeperSaslClient; @@ -279,6 +280,9 @@ static class Packet { boolean finished; + /** Client elapsed time (ms) when the request was submitted; used to compute latency. */ + long requestSubmitTimeMs; + AsyncCallback cb; Object ctx; @@ -313,6 +317,7 @@ static class Packet { this.response = response; this.readOnly = readOnly; this.watchRegistration = watchRegistration; + this.requestSubmitTimeMs = Time.currentElapsedTime(); } public void createBB() { @@ -445,6 +450,8 @@ public ClientCnxn( sendThread = new SendThread(clientCnxnSocket); eventThread = new EventThread(); this.clientConfig = zooKeeper.getClientConfig(); + ClientMetrics.initialize(clientConfig.getMetricsProviderClassName(), + clientConfig.getMetricsProviderConfiguration()); initRequestTimeout(); } @@ -746,6 +753,7 @@ private void processEvent(Object event) { // @VisibleForTesting protected void finishPacket(Packet p) { int err = p.replyHeader.getErr(); + recordRequestMetrics(p, err); if (p.watchRegistration != null) { p.watchRegistration.register(err); } @@ -782,6 +790,26 @@ protected void finishPacket(Packet p) { } } + /** + * Emit client-side request metrics for a completed packet: per-operation latency (and therefore + * per-operation count) and, for failed requests, a count labelled by the error code. + */ + private void recordRequestMetrics(Packet p, int err) { + RequestHeader h = p.requestHeader; + if (h == null) { + return; + } + ClientMetrics metrics = ClientMetrics.getMetrics(); + String op = ClientMetrics.opName(h.getType()); + if (p.requestSubmitTimeMs > 0) { + metrics.REQUEST_LATENCY.add(op, Time.currentElapsedTime() - p.requestSubmitTimeMs); + } + if (err != 0) { + Code code = Code.get(err); + metrics.UNSUCCESSFUL_REQUEST.add(code != null ? code.name() : String.valueOf(err), 1); + } + } + void queueEvent(String clientPath, int err, Set materializedWatchers, EventType eventType) { KeeperState sessionState = KeeperState.SyncConnected; if (KeeperException.Code.SESSIONEXPIRED.intValue() == err @@ -876,6 +904,7 @@ public RWServerFoundException(String msg) { class SendThread extends ZooKeeperThread { private long lastPingSentNs; + private long connectStartTimeMs; private final ClientCnxnSocket clientCnxnSocket; private boolean isFirstConnect = true; @@ -887,9 +916,11 @@ void readResponse(ByteBuffer incomingBuffer) throws IOException { replyHdr.deserialize(bbia, "header"); switch (replyHdr.getXid()) { case PING_XID: + long pingRttMs = (System.nanoTime() - lastPingSentNs) / 1000000; LOG.debug("Got ping response for session id: 0x{} after {}ms.", Long.toHexString(sessionId), - ((System.nanoTime() - lastPingSentNs) / 1000000)); + pingRttMs); + ClientMetrics.getMetrics().PING_LATENCY.add(pingRttMs); return; case AUTHPACKET_XID: LOG.debug("Got auth session id: 0x{}", Long.toHexString(sessionId)); @@ -1141,7 +1172,9 @@ private void sendPing() { private void startConnect(InetSocketAddress addr) throws IOException { // initializing it for new connection saslLoginFailed = false; + connectStartTimeMs = Time.currentElapsedTime(); if (!isFirstConnect) { + ClientMetrics.getMetrics().RECONNECT_COUNT.inc(); try { Thread.sleep(ThreadLocalRandom.current().nextLong(1000)); } catch (InterruptedException e) { @@ -1334,6 +1367,7 @@ public void run() { private void cleanAndNotifyState() { cleanup(); if (state.isAlive()) { + ClientMetrics.getMetrics().CONNECTION_LOSS_COUNT.inc(); eventThread.queueEvent(new WatchedEvent(Event.EventType.None, Event.KeeperState.Disconnected, null)); } clientCnxnSocket.updateNow(); @@ -1435,6 +1469,7 @@ void onConnected( "Unable to reconnect to ZooKeeper service, session 0x%s has expired", Long.toHexString(sessionId)); LOG.warn(warnInfo); + ClientMetrics.getMetrics().SESSION_EXPIRED_COUNT.inc(); throw new SessionExpiredException(warnInfo); } @@ -1449,6 +1484,11 @@ void onConnected( sessionPasswd = _sessionPasswd; changeZkState((isRO) ? States.CONNECTEDREADONLY : States.CONNECTED); seenRwServerBefore |= !isRO; + ClientMetrics establishedMetrics = ClientMetrics.getMetrics(); + establishedMetrics.CONNECTION_ESTABLISHED_COUNT.inc(); + if (connectStartTimeMs > 0) { + establishedMetrics.CONNECT_LATENCY.add(Time.currentElapsedTime() - connectStartTimeMs); + } LOG.info( "Session establishment complete on server {}, session id = 0x{}, negotiated timeout = {}{}", clientCnxnSocket.getRemoteSocketAddress(), diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/client/ClientMetrics.java b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ClientMetrics.java new file mode 100644 index 00000000000..d9eb51d4216 --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ClientMetrics.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper.client; + +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.zookeeper.ZooDefs.OpCode; +import org.apache.zookeeper.metrics.Counter; +import org.apache.zookeeper.metrics.MetricsContext; +import org.apache.zookeeper.metrics.MetricsContext.DetailLevel; +import org.apache.zookeeper.metrics.MetricsProvider; +import org.apache.zookeeper.metrics.MetricsProviderLifeCycleException; +import org.apache.zookeeper.metrics.Summary; +import org.apache.zookeeper.metrics.SummarySet; +import org.apache.zookeeper.metrics.impl.DefaultMetricsProvider; +import org.apache.zookeeper.metrics.impl.MetricsProviderBootstrap; +import org.apache.zookeeper.metrics.impl.NullMetricsProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tracks ZooKeeper client-side metrics. + * + *

This is the client-side counterpart to {@code ServerMetrics}. Metrics are emitted only when a + * {@link MetricsProvider} is configured on the client through + * {@link org.apache.zookeeper.client.ZKClientConfig#ZOOKEEPER_CLIENT_METRICS_PROVIDER_CLASS_NAME}. + * By default the client uses the {@link NullMetricsProvider}, so there is no overhead unless the + * feature is explicitly enabled.

+ */ +public final class ClientMetrics { + + private static final Logger LOG = LoggerFactory.getLogger(ClientMetrics.class); + + /** + * Dummy instance useful for tests and as a no-op default. + */ + public static final ClientMetrics NULL_METRICS = new ClientMetrics(NullMetricsProvider.INSTANCE); + + /** + * Dummy instance useful for tests. + */ + public static final ClientMetrics DEFAULT_METRICS_FOR_TESTS = new ClientMetrics(new DefaultMetricsProvider()); + + /** + * Real instance used for tracking client side metrics. The final value is + * assigned after the {@link MetricsProvider} bootstrap. + */ + private static volatile ClientMetrics CURRENT = DEFAULT_METRICS_FOR_TESTS; + + /** + * Access current ClientMetrics. + * + * @return a reference to the current Metrics + */ + public static ClientMetrics getMetrics() { + return CURRENT; + } + + public static void metricsProviderInitialized(MetricsProvider metricsProvider) { + LOG.info("ClientMetrics initialized with provider {}", metricsProvider); + CURRENT = new ClientMetrics(metricsProvider); + } + + /** + * Tracks whether a real (non-null) metrics provider has already been booted for this JVM. + * Client metrics are a per-JVM singleton (mirroring {@code ServerMetrics}); the first client to + * configure a provider wins, and later clients do not re-boot or tear it down. + */ + private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); + + /** + * Bootstraps the client metrics provider from client configuration, if not already done. + * + *

When no provider (or the {@link NullMetricsProvider}) is configured, client metrics stay a + * no-op and impose no overhead. When a real provider is configured, it is booted once per JVM; + * subsequent calls are ignored so that multiple {@code ZooKeeper} instances share a single + * provider and do not, for example, attempt to bind the same Prometheus port twice.

+ * + * @param providerClassName the configured metrics provider class name. + * @param config the configuration to pass to the provider. + */ + public static void initialize(String providerClassName, Properties config) { + if (INITIALIZED.get()) { + return; + } + if (providerClassName == null || NullMetricsProvider.class.getName().equals(providerClassName)) { + CURRENT = NULL_METRICS; + return; + } + if (!INITIALIZED.compareAndSet(false, true)) { + return; + } + try { + MetricsProvider provider = MetricsProviderBootstrap.startMetricsProvider(providerClassName, config); + metricsProviderInitialized(provider); + LOG.info("Client metrics enabled with provider {}", providerClassName); + } catch (MetricsProviderLifeCycleException error) { + INITIALIZED.set(false); + CURRENT = NULL_METRICS; + LOG.error("Cannot boot client MetricsProvider {}; client metrics are disabled", providerClassName, error); + } + } + + /** + * Build a fresh instance bound to the given provider without touching the global + * {@link #CURRENT} reference. Useful for tests that need an isolated set of metrics. + */ + public static ClientMetrics newInstanceForTests(MetricsProvider metricsProvider) { + return new ClientMetrics(metricsProvider); + } + + private final MetricsProvider metricsProvider; + + /** + * Latency (milliseconds) of completed client requests, labelled by operation name. + * The {@code cnt__request_latency} value doubles as the per-operation request count. + */ + public final SummarySet REQUEST_LATENCY; + + /** + * Count of unsuccessful client requests, labelled by {@code KeeperException.Code} name. + */ + public final SummarySet UNSUCCESSFUL_REQUEST; + + /** + * Round-trip time (milliseconds) of client pings. + */ + public final Summary PING_LATENCY; + + /** + * Time (milliseconds) taken to establish a connection to a server. + */ + public final Summary CONNECT_LATENCY; + + public final Counter CONNECTION_ESTABLISHED_COUNT; + public final Counter CONNECTION_LOSS_COUNT; + public final Counter RECONNECT_COUNT; + public final Counter SESSION_EXPIRED_COUNT; + + private ClientMetrics(MetricsProvider metricsProvider) { + this.metricsProvider = metricsProvider; + MetricsContext metricsContext = this.metricsProvider.getRootContext(); + + REQUEST_LATENCY = metricsContext.getSummarySet("request_latency", DetailLevel.ADVANCED); + UNSUCCESSFUL_REQUEST = metricsContext.getSummarySet("unsuccessful_request", DetailLevel.BASIC); + PING_LATENCY = metricsContext.getSummary("ping_latency", DetailLevel.BASIC); + CONNECT_LATENCY = metricsContext.getSummary("connect_latency", DetailLevel.BASIC); + + CONNECTION_ESTABLISHED_COUNT = metricsContext.getCounter("connection_established_count"); + CONNECTION_LOSS_COUNT = metricsContext.getCounter("connection_loss_count"); + RECONNECT_COUNT = metricsContext.getCounter("reconnect_count"); + SESSION_EXPIRED_COUNT = metricsContext.getCounter("session_expired_count"); + } + + public MetricsProvider getMetricsProvider() { + return metricsProvider; + } + + /** + * Maps a ZooKeeper opcode to a human readable operation name used as a metric label. + * + * @param opCode the {@link OpCode} value from the request header. + * @return a readable operation name, or {@code op_} for unrecognized opcodes. + */ + public static String opName(int opCode) { + switch (opCode) { + case OpCode.create: + return "create"; + case OpCode.create2: + return "create2"; + case OpCode.createTTL: + return "createTTL"; + case OpCode.createContainer: + return "createContainer"; + case OpCode.delete: + return "delete"; + case OpCode.deleteContainer: + return "deleteContainer"; + case OpCode.exists: + return "exists"; + case OpCode.getData: + return "getData"; + case OpCode.setData: + return "setData"; + case OpCode.getACL: + return "getACL"; + case OpCode.setACL: + return "setACL"; + case OpCode.getChildren: + return "getChildren"; + case OpCode.getChildren2: + return "getChildren2"; + case OpCode.getAllChildrenNumber: + return "getAllChildrenNumber"; + case OpCode.getEphemerals: + return "getEphemerals"; + case OpCode.sync: + return "sync"; + case OpCode.ping: + return "ping"; + case OpCode.multi: + return "multi"; + case OpCode.reconfig: + return "reconfig"; + case OpCode.check: + return "check"; + case OpCode.addWatch: + return "addWatch"; + case OpCode.checkWatches: + return "checkWatches"; + case OpCode.removeWatches: + return "removeWatches"; + case OpCode.setWatches: + return "setWatches"; + case OpCode.setWatches2: + return "setWatches2"; + case OpCode.auth: + return "auth"; + case OpCode.sasl: + return "sasl"; + case OpCode.closeSession: + return "closeSession"; + default: + return "op_" + opCode; + } + } +} diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZKClientConfig.java b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZKClientConfig.java index 10c61375d1d..8b5254e261d 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZKClientConfig.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZKClientConfig.java @@ -19,9 +19,12 @@ package org.apache.zookeeper.client; import java.io.File; +import java.util.Map; +import java.util.Properties; import org.apache.yetus.audience.InterfaceAudience; import org.apache.zookeeper.ZooKeeper; import org.apache.zookeeper.common.ZKConfig; +import org.apache.zookeeper.metrics.impl.NullMetricsProvider; import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException; /** @@ -65,6 +68,22 @@ public class ZKClientConfig extends ZKConfig { */ public static final long ZOOKEEPER_REQUEST_TIMEOUT_DEFAULT = 0; + /** + * Fully qualified class name of the {@link org.apache.zookeeper.metrics.MetricsProvider} to use + * on the client. Defaults to {@link NullMetricsProvider} so that client-side metrics are opt-in + * and impose no overhead unless explicitly enabled. + */ + public static final String ZOOKEEPER_CLIENT_METRICS_PROVIDER_CLASS_NAME = + "zookeeper.clientMetricsProvider.className"; + + /** + * Prefix for client metrics provider configuration. Every system property that starts with this + * prefix (except {@code className}) is passed to the provider with the prefix stripped, mirroring + * the {@code metricsProvider.*} handling on the server. + */ + public static final String ZOOKEEPER_CLIENT_METRICS_PROVIDER_PREFIX = + "zookeeper.clientMetricsProvider."; + public ZKClientConfig() { super(); initFromJavaSystemProperties(); @@ -85,6 +104,8 @@ public ZKClientConfig(String configPath) throws ConfigException { private void initFromJavaSystemProperties() { setProperty(ZOOKEEPER_REQUEST_TIMEOUT, System.getProperty(ZOOKEEPER_REQUEST_TIMEOUT)); setProperty(ZOOKEEPER_SERVER_PRINCIPAL, System.getProperty(ZOOKEEPER_SERVER_PRINCIPAL)); + setProperty(ZOOKEEPER_CLIENT_METRICS_PROVIDER_CLASS_NAME, + System.getProperty(ZOOKEEPER_CLIENT_METRICS_PROVIDER_CLASS_NAME)); } @Override @@ -142,4 +163,34 @@ public long getLong(String key, long defaultValue) { return defaultValue; } + /** + * Returns the configured client {@link org.apache.zookeeper.metrics.MetricsProvider} class name, + * or the {@link NullMetricsProvider} (no-op) when none is configured. + * + * @return the metrics provider class name to bootstrap on the client. + */ + public String getMetricsProviderClassName() { + return getProperty(ZOOKEEPER_CLIENT_METRICS_PROVIDER_CLASS_NAME, NullMetricsProvider.class.getName()); + } + + /** + * Collects the configuration to pass to the client metrics provider. Every system property that + * starts with {@link #ZOOKEEPER_CLIENT_METRICS_PROVIDER_PREFIX} (except {@code className}) is + * added to the returned {@link Properties} with the prefix stripped. + * + * @return the metrics provider configuration. + */ + public Properties getMetricsProviderConfiguration() { + Properties config = new Properties(); + for (Map.Entry entry : System.getProperties().entrySet()) { + String key = String.valueOf(entry.getKey()); + if (key.startsWith(ZOOKEEPER_CLIENT_METRICS_PROVIDER_PREFIX) + && !key.equals(ZOOKEEPER_CLIENT_METRICS_PROVIDER_CLASS_NAME)) { + String strippedKey = key.substring(ZOOKEEPER_CLIENT_METRICS_PROVIDER_PREFIX.length()); + config.put(strippedKey, entry.getValue()); + } + } + return config; + } + } diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/client/ClientMetricsIntegrationTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/client/ClientMetricsIntegrationTest.java new file mode 100644 index 00000000000..d4ad57dbbaa --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/client/ClientMetricsIntegrationTest.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper.client; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.io.File; +import java.util.Map; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.PortAssignment; +import org.apache.zookeeper.ZKTestCase; +import org.apache.zookeeper.ZooDefs; +import org.apache.zookeeper.ZooKeeper; +import org.apache.zookeeper.metrics.MetricsProvider; +import org.apache.zookeeper.metrics.MetricsUtils; +import org.apache.zookeeper.metrics.impl.DefaultMetricsProvider; +import org.apache.zookeeper.server.ServerCnxnFactory; +import org.apache.zookeeper.test.ClientBase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * End-to-end test that a real client, when configured with a metrics provider, emits client-side + * request and connection metrics. Runs a standalone server directly to stay independent of the + * JMX-based {@link ClientBase} lifecycle. + */ +public class ClientMetricsIntegrationTest extends ZKTestCase { + + private File tmpDir; + private ServerCnxnFactory serverFactory; + private String hostPort; + + @Before + public void setUp() throws Exception { + ClientBase.setupTestEnv(); + tmpDir = ClientBase.createTmpDir(); + hostPort = "127.0.0.1:" + PortAssignment.unique(); + serverFactory = ClientBase.createNewServerInstance(null, hostPort, -1); + ClientBase.startServerInstance(tmpDir, serverFactory, hostPort, 1); + } + + @After + public void tearDown() throws Exception { + if (serverFactory != null) { + serverFactory.shutdown(); + } + if (tmpDir != null) { + ClientBase.recursiveDelete(tmpDir); + } + } + + private static long value(Map metrics, String key) { + Object v = metrics.get(key); + return (v == null) ? 0L : ((Number) v).longValue(); + } + + @Test + public void testClientEmitsRequestAndConnectionMetrics() throws Exception { + String key = ZKClientConfig.ZOOKEEPER_CLIENT_METRICS_PROVIDER_CLASS_NAME; + String previous = System.getProperty(key); + System.setProperty(key, DefaultMetricsProvider.class.getName()); + ZooKeeper zk = null; + try { + ClientBase.CountdownWatcher watcher = new ClientBase.CountdownWatcher(); + zk = new ZooKeeper(hostPort, ClientBase.CONNECTION_TIMEOUT, watcher); + watcher.waitForConnected(ClientBase.CONNECTION_TIMEOUT); + + MetricsProvider provider = ClientMetrics.getMetrics().getMetricsProvider(); + Map before = MetricsUtils.collect(provider); + long createBefore = value(before, "cnt_create_request_latency"); + long getDataBefore = value(before, "cnt_getData_request_latency"); + long noNodeBefore = value(before, "cnt_NONODE_unsuccessful_request"); + + zk.create("/clientmetrics", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); + zk.getData("/clientmetrics", false, null); + try { + zk.getData("/clientmetrics-missing", false, null); + fail("expected NoNodeException"); + } catch (KeeperException.NoNodeException expected) { + // expected: drives the unsuccessful-request metric + } + + Map after = MetricsUtils.collect(provider); + assertTrue("create request count should increase", + value(after, "cnt_create_request_latency") > createBefore); + assertTrue("getData request count should increase", + value(after, "cnt_getData_request_latency") > getDataBefore); + assertTrue("NONODE error count should increase", + value(after, "cnt_NONODE_unsuccessful_request") > noNodeBefore); + assertTrue("connection should have been established at least once", + value(after, "connection_established_count") >= 1); + } finally { + if (zk != null) { + zk.close(); + } + if (previous == null) { + System.clearProperty(key); + } else { + System.setProperty(key, previous); + } + } + } +} diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/client/ClientMetricsTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/client/ClientMetricsTest.java new file mode 100644 index 00000000000..327dd1da7e3 --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/client/ClientMetricsTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import java.util.Map; +import org.apache.zookeeper.ZKTestCase; +import org.apache.zookeeper.metrics.MetricsUtils; +import org.apache.zookeeper.metrics.impl.DefaultMetricsProvider; +import org.apache.zookeeper.metrics.impl.NullMetricsProvider; +import org.junit.Test; + +public class ClientMetricsTest extends ZKTestCase { + + @Test + public void testGetMetricsNeverNull() { + assertNotNull(ClientMetrics.getMetrics()); + assertNotNull(ClientMetrics.NULL_METRICS); + assertSame(NullMetricsProvider.INSTANCE, ClientMetrics.NULL_METRICS.getMetricsProvider()); + } + + @Test + public void testMetricsProviderInitializedSwapsCurrentInstance() { + DefaultMetricsProvider provider = new DefaultMetricsProvider(); + ClientMetrics previous = ClientMetrics.getMetrics(); + try { + ClientMetrics.metricsProviderInitialized(provider); + ClientMetrics metrics = ClientMetrics.getMetrics(); + assertSame(provider, metrics.getMetricsProvider()); + } finally { + ClientMetrics.metricsProviderInitialized(previous.getMetricsProvider()); + } + } + + @Test + public void testRequestLatencyRecordsPerOperationCountAndLatency() { + DefaultMetricsProvider provider = new DefaultMetricsProvider(); + ClientMetrics metrics = ClientMetrics.newInstanceForTests(provider); + + metrics.REQUEST_LATENCY.add("getData", 7); + metrics.REQUEST_LATENCY.add("getData", 3); + metrics.REQUEST_LATENCY.add("create", 5); + + Map values = MetricsUtils.collect(provider); + assertEquals(2L, values.get("cnt_getData_request_latency")); + assertEquals(1L, values.get("cnt_create_request_latency")); + assertEquals(10L, values.get("sum_getData_request_latency")); + } + + @Test + public void testUnsuccessfulRequestRecordsCountByErrorCode() { + DefaultMetricsProvider provider = new DefaultMetricsProvider(); + ClientMetrics metrics = ClientMetrics.newInstanceForTests(provider); + + metrics.UNSUCCESSFUL_REQUEST.add("NONODE", 1); + metrics.UNSUCCESSFUL_REQUEST.add("NONODE", 1); + metrics.UNSUCCESSFUL_REQUEST.add("CONNECTIONLOSS", 1); + + Map values = MetricsUtils.collect(provider); + assertEquals(2L, values.get("cnt_NONODE_unsuccessful_request")); + assertEquals(1L, values.get("cnt_CONNECTIONLOSS_unsuccessful_request")); + } + + @Test + public void testLifecycleCountersAndSummaries() { + DefaultMetricsProvider provider = new DefaultMetricsProvider(); + ClientMetrics metrics = ClientMetrics.newInstanceForTests(provider); + + metrics.CONNECTION_ESTABLISHED_COUNT.add(1); + metrics.CONNECTION_LOSS_COUNT.add(3); + metrics.RECONNECT_COUNT.add(2); + metrics.SESSION_EXPIRED_COUNT.inc(); + metrics.PING_LATENCY.add(11); + metrics.CONNECT_LATENCY.add(20); + + Map values = MetricsUtils.collect(provider); + assertEquals(1L, values.get("connection_established_count")); + assertEquals(3L, values.get("connection_loss_count")); + assertEquals(2L, values.get("reconnect_count")); + assertEquals(1L, values.get("session_expired_count")); + assertEquals(1L, values.get("cnt_ping_latency")); + assertEquals(11L, values.get("sum_ping_latency")); + assertEquals(1L, values.get("cnt_connect_latency")); + } +} diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/client/ZKClientConfigMetricsTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/client/ZKClientConfigMetricsTest.java new file mode 100644 index 00000000000..3cceddb3977 --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/client/ZKClientConfigMetricsTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper.client; + +import static org.junit.Assert.assertEquals; +import java.util.Properties; +import org.apache.zookeeper.ZKTestCase; +import org.apache.zookeeper.metrics.impl.DefaultMetricsProvider; +import org.apache.zookeeper.metrics.impl.NullMetricsProvider; +import org.junit.Test; + +public class ZKClientConfigMetricsTest extends ZKTestCase { + + @Test + public void testDefaultMetricsProviderIsNullProvider() { + ZKClientConfig conf = new ZKClientConfig(); + assertEquals(NullMetricsProvider.class.getName(), conf.getMetricsProviderClassName()); + } + + @Test + public void testConfiguredMetricsProviderClassName() { + ZKClientConfig conf = new ZKClientConfig(); + conf.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_METRICS_PROVIDER_CLASS_NAME, + DefaultMetricsProvider.class.getName()); + assertEquals(DefaultMetricsProvider.class.getName(), conf.getMetricsProviderClassName()); + } + + @Test + public void testMetricsProviderConfigurationPassthroughFromSystemProperties() { + String key = "zookeeper.clientMetricsProvider.httpPort"; + String previous = System.getProperty(key); + try { + System.setProperty(key, "7100"); + ZKClientConfig conf = new ZKClientConfig(); + Properties props = conf.getMetricsProviderConfiguration(); + assertEquals("7100", props.getProperty("httpPort")); + } finally { + if (previous == null) { + System.clearProperty(key); + } else { + System.setProperty(key, previous); + } + } + } +}