Skip to content

Metrics instrumentation across all DataStoreTypes (ZK, HTTP, DROVE) - #108

Closed
jitendradhawan wants to merge 43 commits into
appform-io:2.xfrom
jitendradhawan:metrics_instrumentation
Closed

jitendradhawan wants to merge 43 commits into
appform-io:2.xfrom
jitendradhawan:metrics_instrumentation

Conversation

@jitendradhawan

@jitendradhawan jitendradhawan commented Jun 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Dropwizard metrics instrumentation across Ranger's ZooKeeper, HTTP, and Drove discovery paths. Metrics now cover upstream availability, remote calls, parsing/serialization, registry refreshes, stale-data retention, health checks, provider updates, and node/service counts.

This change also introduces stable upstream IDs for metric attribution, wires metrics through Dropwizard bundles, adds backend-focused integration tests, and prepares release 2.0.0-RC7.

Why

Ranger previously exposed limited visibility into discovery failures and data quality. Operators could not reliably distinguish upstream outages, malformed responses, stale-node retention, zombie filtering, or registry refresh latency.

These metrics make those states observable and separate them by datastore type and configured upstream.

What Changed

Metrics foundation

  • Added shared MetricRecorder, backed by Dropwizard MetricRegistry and safe no-op behavior before initialization.
  • Added DataStoreType (ZK, HTTP, DROVE) and upstream ID dimensions.
  • Standardized names under io.appform.ranger.
  • Used meters for events/status observations, timers for refresh latency, gauges for latest node counts, and 64-sample UniformReservoir histograms for API response counts.
  • Added metrics enablement to discovery and server bundles. YAML/no-arg discovery configuration and server-bundle configuration default to enabled; ServiceDiscoveryConfiguration.builder() callers must explicitly set .metricsEnabled(true).

Instrumented flows

Area Coverage
Registry refresh Success/failure latency, fetched nodes, accepted nodes, zombie filtering, stale-node retention
Upstream health Active/inactive observations for ZooKeeper, HTTP, and Drove
Remote operations Response status, empty/null response, parse failure, transport/unknown failure
Service providers ZooKeeper and HTTP registration/update, serialization, and write failures
Health checks Healthy/unhealthy results and checker exceptions
Drove cache Event-triggered application/instance cache updates
Ranger server API Services returned and nodes returned per service

Representative metric hierarchy:

io.appform.ranger.dataStoreType.<ZK|HTTP|DROVE>.dataSource.<upstreamId>...

Data flow

Bundle MetricRegistry
  -> MetricRecorder initialization
  -> configured upstream ID
  -> client/factory/builder
  -> NodeDataSource or NodeDataSink
  -> operation/status/latency/count metric

Bundle and configuration wiring

  • Discovery, server, and hub-server bundles initialize instrumentation from their Dropwizard MetricRegistry.
  • HTTP, Drove, and Ranger Hub ZooKeeper upstream configurations now carry a required non-blank id.
  • Finder/provider builders and backend factories propagate the ID to sources and sinks.
  • Added metricsEnabled configuration and server-bundle enablement hooks.
  • Discovery bundles use the fixed upstream ID zkServiceDiscovery.
  • MetricRecorder is process-global. Disabling one bundle skips initialization but does not clear a registry initialized by another bundle.

Breaking Changes / Migration

Before After / action required
HTTP or Drove upstream without an ID Add a unique, stable id
Ranger Hub ZooKeeper zookeepers: [...] Use zookeeper: "connection-string" and add id
Multiple ZooKeeper upstreams in one list Configure separate upstream entries, each with a unique ID
Finder/provider builder without upstream metadata Call the corresponding withUpstreamId(...) API
Custom NodeDataSource / NodeDataSink Implement getUpstreamId() and getDataStoreType()
Overridden source/sink factory hook Accept and propagate the new upstream ID argument
Direct backend constructor calls Supply the new upstream ID parameter
Direct hub-client builder Add .upstreamId(...), including SimpleRangerZKClient.builder()
Discovery-bundle default constants Import DEFAULT_NAMESPACE / DEFAULT_HOST from discovery-core

HTTP upstream migration

Each entry under httpClientConfigs now requires a non-blank id. Use a unique, stable value for correct metric attribution. Other HTTP client fields remain unchanged. The ID is propagated to the HTTP hub client and appears in metric names as <upstreamId>.

# Before
- type: HTTP
  nodeRefreshTimeMs: 5000
  serviceRefreshTimeoutMs: 300000
  hubStartTimeoutMs: 210000
  httpClientConfigs:
    - host: ranger-a.example.com
      port: 443
      secure: true

# After
- type: HTTP
  nodeRefreshTimeMs: 5000
  serviceRefreshTimeoutMs: 300000
  hubStartTimeoutMs: 210000
  httpClientConfigs:
    - id: primary-http
      host: ranger-a.example.com
      port: 443
      secure: true

Add an ID to every configured endpoint when multiple HTTP clients exist:

httpClientConfigs:
  - id: primary-http
    host: ranger-a.example.com
    port: 443
    secure: true
  - id: secondary-http
    host: ranger-b.example.com
    port: 443
    secure: true

Programmatic configuration must also set the ID:

HttpClientConfig config = HttpClientConfig.builder()
        .id("primary-http")
        .host("ranger-a.example.com")
        .port(443)
        .secure(true)
        .connectionTimeoutMs(5_000)
        .operationTimeoutMs(5_000)
        .refreshIntervalMillis(10_000)
        .build();

Direct callers of the generated all-arguments constructor must pass id as its new first argument. Prefer the builder to avoid positional migration mistakes.

Resulting metrics use the configured value, for example:

io.appform.ranger.dataStoreType.HTTP.dataSource.primary-http...

Drove upstream migration

Each DroveUpstreamConfig now requires a non-blank id. Use a unique, stable value for correct metric attribution. Add it to YAML and programmatic builders:

- type: DROVE
  droveClusters:
    - id: primary-drove
      endpoints:
        - drove-a.example.com:443
DroveUpstreamConfig config = DroveUpstreamConfig.builder()
        .id("primary-drove")
        .endpoints(List.of("drove-a.example.com:443"))
        .build();

Direct DroveOkHttpTransport callers must change (okHttpClient) to (upstreamId, okHttpClient). Remove explicit generic type witnesses from RangerDroveUtils.buildDroveClient(...), which is no longer generic.

ZooKeeper upstream migration

Each Ranger Hub ZooKeeper upstream now requires a non-blank id. Configuration changes from plural zookeepers (List<String>) to singular zookeeper (String). A ZooKeeper ensemble remains one comma-separated connection string; configure separate upstream entries when connecting to multiple independent ensembles.

# Before
- type: ZK
  zookeepers:
    - zk-a:2181,zk-b:2181

# After
- type: ZK
  id: primary-zk
  zookeeper: zk-a:2181,zk-b:2181

Multiple independent ensembles now require separate entries:

- type: ZK
  id: primary-zk
  zookeeper: zk-a:2181,zk-b:2181
- type: ZK
  id: secondary-zk
  zookeeper: zk-c:2181,zk-d:2181

Programmatic hub configuration must call setId(...) and setZookeeper(...) instead of setZookeepers(...). Direct ZooKeeper clients, finder/provider builders, factories, data sources, and sinks must also receive and propagate upstreamId.

Resulting metrics use the configured value, for example:

io.appform.ranger.dataStoreType.ZK.dataSource.primary-zk...

Upstream IDs become metric-name segments. Validation enforces non-blank values but not uniqueness or metric-safe characters; choose unique, stable, low-cardinality IDs.

Programmatic extension migration

Custom implementations and direct callers must update these signatures:

Previous New
dataSource(Service) dataSource(String upstreamId, Service)
dataSink(Service) dataSink(String upstreamId, Service)
getDefaultDataSource() getDefaultDataSource(String upstreamId)
HTTP/ZooKeeper source, service-source, and sink constructors Prepend upstreamId
ZooKeeper finder-factory builders/constructors Supply upstreamId

Behavioral Notes

  • Active/inactive metrics are meters incremented when isActive() is observed, not current-state gauges or transition counters.
  • Refresh success is recorded for any non-null result, including an empty node list. Exceptions record refresh failure and retain only currently healthy stale nodes with refreshed timestamps.
  • An active source returning Optional.empty() records neither refresh success/failure nor stale retention.
  • HTTP status/parse metrics and unknownFailure metrics can overlap because handled failures are rethrown as communication exceptions.
  • ZooKeeper missing paths, empty children, and child-level KeeperExceptions are classified as empty responses; broader read failures use zkRead.*.unknownFailure.
  • Drove is source-only; HTTP and ZooKeeper instrument both discovery sources and provider sinks.
  • Health-check metrics are process-global and do not include datastore, upstream, or service dimensions.

Operational Considerations

  • One metric object is retained per complete metric name. Bounded histogram reservoirs limit samples per histogram, not number of service/upstream metrics.
  • Metric names include raw upstream IDs, service names, exception classes, HTTP statuses, and Drove event names. Keep configured IDs and discovered service sets bounded.
  • Reinitializing the process-global recorder changes the registry for all Ranger producers. Review shared-process deployments and multiple bundle initialization carefully.
  • Namespace is not included in service-level metric names; identical service names across namespaces aggregate.

Dependency Impact

  • ranger-core gains production dependency on Dropwizard Metrics.
  • ranger-http gains production dependency on javax.validation-api for HttpClientConfig.id validation.
  • ranger-zookeeper gains WireMock as test dependency.

Testing

  • Added 12 metrics-focused unit/integration test suites across ranger-core, ranger-http, ranger-drove, and ranger-zookeeper.
  • Covered metric naming/counting, latest-value gauges, refresh success/failure latency, stale retention, zombie filtering, health checks, upstream status, response handling, parse failures, and ZooKeeper reads/writes.
  • Updated existing client, factory, builder, bundle, and hub tests for upstream IDs and datastore metadata.
  • Replaced PingCheckMonitorTest external network dependency with local WireMock.
  • Focused core tests and affected-module compilation pass; full reactor test suite was not run during this review.
  • Not directly covered: bundle enable/disable and registry replacement, Ranger API count histograms, HTTP sink failure metrics, Drove event-cache metrics, and missing/blank-ID configuration migration.

Suggested verification:

mvn -pl ranger-core,ranger-http,ranger-drove,ranger-zookeeper -am test
mvn test

Reviewer Guide

Recommended review order:

  1. MetricRecorder metric taxonomy, registry lifecycle, and cardinality behavior.
  2. ServiceRegistryUpdater, HealthChecker, and FinderUtils instrumentation semantics.
  3. HTTP, Drove, and ZooKeeper source/sink failure classification.
  4. Upstream ID propagation through configs, clients, factories, and builders.
  5. Bundle initialization and metricsEnabled behavior.
  6. Metrics integration tests and migration coverage.

Please pay particular attention to:

  • Global registry behavior when multiple Ranger bundles share one process.
  • Gauge registration/reinitialization and concurrent registry access.
  • Metric cardinality for service names, upstream IDs, event names, and exception classes.
  • Distinction between empty responses, transport failures, parse failures, and stale-data retention.
  • Backward compatibility of public constructors, extension hooks, custom sources/sinks, and YAML configuration.
  • HTTP activity now follows HttpCommunicator.healthy() instead of always reporting active.

Additional Changes

  • Bumped project version from 2.0.0-RC6 to 2.0.0-RC7 and documented migration in CHANGELOG.md.
  • Included Sonar-oriented cleanup and test reliability changes.
  • Updated ID-generator performance snapshots, changed one invalid-state exception to IllegalStateException, and added Sonar suppressions in ID model classes.
  • Updated server-bundle README examples from Guava list creation to List.of(...).

jitendradhawan and others added 24 commits April 8, 2026 08:48
…r-fix

[Fix] Collision Check concurrency handling
…nanoseconds-fix

[Fix] Prevent duplicate IDs by switching to monotonic clock (nanoTime)

@ToOnlyGaurav ToOnlyGaurav left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please go through comments

  • Let's call metric_id to be something meaningful like upstream_id or dataSourceId etc.

@SuperBuilder
public abstract class AbstractRangerHubClient<T, R extends ServiceRegistry<T>, D extends Deserializer<T>> implements RangerHubClient<T,R> {

private final String metricId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it generic name closer to functional. Keeping non-functional attribute with functional group looks a bit off

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to upstreamId


// Wait for initial update and a brief period for metric recording to complete
awaitRefresh(registry);
sleep(100); // Allow time for MetricRecorder call after updateNodes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible to use any synchronisation mechanism which gives better guarantees than sleep. Most of the time these tests fails if we have slow environments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using awaitility now instead of sleep to check periodically with few ms wait

List<ServiceNode<T>> nodes = new ArrayList<>(children.size());
log.debug("Found {} nodes for [{}]", children.size(), serviceName);
if(children.isEmpty()){
MetricRecorder.recordNullOrEmptyListNodeResponse(DataStoreType.ZK, metricId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have wrapper method for these two.

@jitendradhawan jitendradhawan Jun 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extracted new method recordNullOrEmptyResponse to encapsulate both methods

public static final String STALE_DATA_RETAINED = "staleDataRetained";
public static final String ZK_READ = "zkRead";

private static MetricRegistry metricRegistry = new MetricRegistry();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new MetricRegistry(); is of no use.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed


public static void recordNodeDataSinkUpdateStatus(DataStoreType dataStoreType, String metricId, String status) {
if (metricRegistry != null) {
metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_SOURCE, dataStoreType.name(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks a bit confusion when read with line#110

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by using DATA_STORE_TYPE, dataStoreType.name(), DATA_SOURCE, upstreamId in all MetricRecorder methods

? oldV
: newV))
.values();
MetricRecorder.recordServiceNodesReturned(service.getServiceName(), serviceNodes.size());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are getting these from upstreams, how we will figure out which upstream is returning wrong value?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we're pushing node count also which are fetched from any upstream in ServiceRegistryUpdater

dataLock.lock();
try {
long resolvedTime = resolution.convert(timeInMillis, TimeUnit.MILLISECONDS);
long resolvedTime = resolution.convert(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't get the need of doing this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

public boolean check(long timeInMillis, int location) {
public long checkAndGetTime(int location) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method looks overloaded from the response perspective.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* Checks collisions between ids in given period
*/
@Slf4j
public class CollisionChecker {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@r0goyal Please take a look.

val serviceRegistrationResponse = registerService(service.getServiceName(), httpUrl, requestBody).orElse(null);
if(null == serviceRegistrationResponse || !serviceRegistrationResponse.valid()){
log.warn("Http call to {} returned a failure response {}", httpUrl, serviceRegistrationResponse);
MetricRecorder.recordNullOrEmptyRegisterServiceResponse(DataStoreType.HTTP, metricId, service.getServiceName());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's shrink these two methods to one.
Don't want metrics related code to be surfaced more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extracted to recordNullOrEmptyRegisterServiceResponse

@sonarqubecloud

sonarqubecloud Bot commented Jun 2, 2026

Copy link
Copy Markdown

- MatchingShardSelector: remove unused Collectors import (S1128)
- HealthcheckStatus: suppress S115 - renaming healthy/unhealthy breaks 145 call sites
- ServiceDiscoveryBundle: suppress S1172 on overridable protected methods - param required by subclasses
- Domain: rename field 'domain' -> 'domainName' to avoid S1700 (field same name as class)
- Id: rename field 'id' -> 'idValue' to avoid S1700; add @JsonProperty to preserve JSON serialization
- IdGeneratorBase: replace RuntimeException with IllegalStateException (S112)
- RangerDroveUtils: add explanatory comments to intentional no-op SSL methods (S1186/S4830);
  upgrade SSLContext from 'SSL' to 'TLSv1.2' (S4423); remove unused type param <T> (S2326);
  remove commented-out code block (S125)
- RangerHubServerBundle: remove explicit type param from buildDroveClient call
- HttpApiCommunicator: add comment to empty close() method (S1186)
- PathBuilder: make REGISTERED_SERVICES_PATH static final (S1104/S1444/S3008)
…r suppressions

- Domain.java: revert domainName->domain rename; add @SuppressWarnings(java:S1700) on class
- Id.java: revert idValue->id rename and @JsonProperty; add @SuppressWarnings(java:S1700) on class
- RangerDroveUtils.java: revert TLSv1.2->SSL; add @SuppressWarnings(java:S4423) on SSLContext line
- Bump all Maven modules to 2.0.0-RC7
- Document metrics instrumentation and fixes
- Call out breaking API and configuration migrations
Cap node-count reservoirs and remove unbounded service zombie metrics.\nMove standalone metric initialization behind configurable bundle lifecycle.
Replace inventory node-count histograms with latest-value gauges to reduce retained samples and update overhead. Keep service response histograms bounded.
@sonarqubecloud

Copy link
Copy Markdown

@ToOnlyGaurav ToOnlyGaurav left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jitendradhawan jitendradhawan changed the title Draft: Metrics instrumentation across all DataStoreTypes (ZK, HTTP, DROVE) Metrics instrumentation across all DataStoreTypes (ZK, HTTP, DROVE) Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants