refactor(metrics): make Monitors self-contained; MetricsCollector becomes a registry wiring bean (#1059)

* refactor(metrics): Monitors owns its CompositeMeterRegistry; MetricsCollector wires registries in

Previously Monitors pulled its registry from MetricsCollector, creating an
awkward dependency from conductor-core → conductor-metrics. Now Monitors
owns the CompositeMeterRegistry directly and exposes addMeterRegistry() /
getRegistry(). MetricsCollector (contribs) becomes a thin Spring wiring
component that calls Monitors.addMeterRegistry() on startup.

- Removes conductor-core → conductor-metrics build dependency (cycle-free)
- Adds conductor-metrics → conductor-core build dependency
- Adds getGauge() / getDistributionSummary() aliases for callers using the
  'get' naming convention
- Deprecates MetricsCollector.getMeterRegistry() in favour of
  Monitors.getRegistry()

* Applied spotless

* test(metrics): add MonitorsTest covering registry ownership and meter APIs

Verifies addMeterRegistry(), getRegistry(), counter/timer/gauge identity
caching, getGauge/getDistributionSummary aliases, and tag isolation.

* test(metrics): add Spring integration test verifying MetricsCollector wires registries into Monitors

Boots a minimal Spring context with a SimpleMeterRegistry, confirms that
counters/timers/gauges recorded via Monitors are visible in the
Spring-wired registry after MetricsCollector initialises.

* Applied spotless

* refactor(metrics): retire conductor-metrics module, move classes to core/server

MetricsCollector moves to core alongside Monitors — they are companion classes
(Monitors owns the registry, MetricsCollector wires Spring-managed registries in).
Registry-specific configs (Logging, CloudWatch, AzureMonitor) move to server where
they belong as deployment-level concerns. No Java code imported from the old
contribs.metrics package, so this is a pure relocation with no call-site changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(metrics): delete retired conductor-metrics module directory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(metrics): add micrometer-registry-prometheus to server and server-lite

Without this dependency Spring Boot cannot create PrometheusMeterRegistry,
so /actuator/prometheus silently returns 404 even though
conductor.metrics-prometheus.enabled=true is set in all default configs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: simplify metrics comment in server build files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(build): remove stale :conductor-metrics dep from scheduler-core

The scheduler module (merged from main via #1064) still referenced
:conductor-metrics, which this branch retired. Monitors is already
provided by :conductor-core.

---------
This commit is contained in:
Shailesh Padave
2026-05-21 23:33:21 +05:30
committed by GitHub
parent ce5b05ad60
commit 396a09a4fd
16 changed files with 210 additions and 94 deletions
-1
View File
@@ -14,7 +14,6 @@ apply plugin: 'groovy'
dependencies {
implementation project(':conductor-common')
implementation project(':conductor-metrics')
compileOnly 'org.springframework.boot:spring-boot-starter'
compileOnly 'org.springframework.boot:spring-boot-starter-validation'
@@ -10,38 +10,39 @@
* 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 com.netflix.conductor.contribs.metrics;
package com.netflix.conductor.metrics;
import org.springframework.stereotype.Component;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import lombok.extern.slf4j.Slf4j;
/**
* Spring component that registers all available {@link MeterRegistry} instances with {@link
* Monitors} at startup. Monitors owns the composite registry; this class is purely a wiring point
* between Spring-managed registries and the static Monitors API.
*/
@Slf4j
@Component
public class MetricsCollector {
static final CompositeMeterRegistry compositeRegistry = new CompositeMeterRegistry();
private static final MeterRegistry simpleRegistry = new SimpleMeterRegistry();
public MetricsCollector(MeterRegistry... registries) {
log.info("=========");
log.info("Conductor configured with {} metrics registries", registries.length);
for (MeterRegistry registry : registries) {
log.info("Metrics registry: {}", registry);
Monitors.addMeterRegistry(registry);
}
log.info(
"check https://docs.micrometer.io/micrometer/reference/ for configuration options");
log.info("=========");
compositeRegistry.add(simpleRegistry);
for (MeterRegistry meterRegistry : registries) {
compositeRegistry.add(meterRegistry);
}
}
/**
* @deprecated Use {@link Monitors#getRegistry()} directly.
*/
@Deprecated
public static MeterRegistry getMeterRegistry() {
return compositeRegistry;
return Monitors.getRegistry();
}
}
@@ -21,7 +21,6 @@ import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import com.netflix.conductor.contribs.metrics.MetricsCollector;
import com.netflix.conductor.model.TaskModel;
import com.netflix.conductor.model.WorkflowModel;
@@ -33,13 +32,34 @@ import io.micrometer.core.instrument.ImmutableTag;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Monitors {
public static final String NO_DOMAIN = "NO_DOMAIN";
private static final MeterRegistry registry = MetricsCollector.getMeterRegistry();
private static final CompositeMeterRegistry registry = new CompositeMeterRegistry();
static {
// Always include an in-process registry so meters are never dropped
// before a real registry is wired in by MetricsCollector.
registry.add(new SimpleMeterRegistry());
}
/**
* Returns the shared composite registry. Callers may add common tags via {@code
* getRegistry().config()}.
*/
public static MeterRegistry getRegistry() {
return registry;
}
/** Register an additional {@link MeterRegistry}. Called by MetricsCollector on startup. */
public static void addMeterRegistry(MeterRegistry meterRegistry) {
registry.add(meterRegistry);
}
private static final double[] percentiles = new double[] {0.5, 0.75, 0.90, 0.95, 0.99};
private static final Map<String, AtomicDouble> gauges = new ConcurrentHashMap<>();
@@ -90,6 +110,19 @@ public class Monitors {
});
}
/** Alias for {@link #gauge(String, String...)} — preferred name for new call sites. */
public static AtomicDouble getGauge(String name, String... tags) {
return gauge(name, tags);
}
/**
* Alias for {@link #distributionSummary(String, String...)} — preferred name for new call
* sites.
*/
public static DistributionSummary getDistributionSummary(String name, String... tags) {
return distributionSummary(name, tags);
}
private static Iterable<Tag> toTags(String... kv) {
List<Tag> tags = new ArrayList<>();
for (int i = 0; i < kv.length - 1; i += 2) {
@@ -0,0 +1,40 @@
/*
* Copyright 2025 Conductor Authors.
* <p>
* Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 com.netflix.conductor.metrics;
import org.junit.Test;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import static org.junit.Assert.*;
public class MetricsCollectorTest {
@Test
public void constructor_wiresRegistryIntoMonitors() {
SimpleMeterRegistry registry = new SimpleMeterRegistry();
new MetricsCollector(registry);
Monitors.getCounter("mc_test_counter", "source", "test").increment(7);
Counter counter = registry.find("mc_test_counter").counter();
assertNotNull("Counter should be visible in the wired registry", counter);
assertEquals(7.0, counter.count(), 0.001);
}
@Test
public void getMeterRegistry_delegatesToMonitors() {
assertSame(Monitors.getRegistry(), MetricsCollector.getMeterRegistry());
}
}
@@ -0,0 +1,111 @@
/*
* Copyright 2025 Conductor Authors.
* <p>
* Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 com.netflix.conductor.metrics;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import static org.junit.Assert.*;
public class MonitorsTest {
@Test
public void getRegistry_returnsNonNull() {
assertNotNull(Monitors.getRegistry());
}
@Test
public void addMeterRegistry_metersAreVisibleInAddedRegistry() {
SimpleMeterRegistry extra = new SimpleMeterRegistry();
Monitors.addMeterRegistry(extra);
// Record something after adding the registry
Monitors.getCounter("test_add_registry_counter", "tag", "value").increment(3);
Counter counter = extra.find("test_add_registry_counter").counter();
assertNotNull("Counter should be visible in newly added registry", counter);
assertEquals(3.0, counter.count(), 0.001);
}
@Test
public void getCounter_sameKeyReturnsSameInstance() {
Counter c1 = Monitors.getCounter("test_counter_identity", "k", "v");
Counter c2 = Monitors.getCounter("test_counter_identity", "k", "v");
assertSame(c1, c2);
}
@Test
public void getTimer_sameKeyReturnsSameInstance() {
Timer t1 = Monitors.getTimer("test_timer_identity", "k", "v");
Timer t2 = Monitors.getTimer("test_timer_identity", "k", "v");
assertSame(t1, t2);
}
@Test
public void getGauge_aliasMatchesGauge() {
// getGauge and gauge should return the same AtomicDouble for the same key
assertSame(
Monitors.gauge("test_gauge_alias", "k", "v"),
Monitors.getGauge("test_gauge_alias", "k", "v"));
}
@Test
public void getDistributionSummary_aliasMatchesDistributionSummary() {
assertSame(
Monitors.distributionSummary("test_dist_alias", "k", "v"),
Monitors.getDistributionSummary("test_dist_alias", "k", "v"));
}
@Test
public void recordGauge_valueIsReflectedInRegistry() {
SimpleMeterRegistry probe = new SimpleMeterRegistry();
Monitors.addMeterRegistry(probe);
Monitors.recordGauge("test_gauge_value", 42L);
// gauge() in Monitors uses an AtomicDouble — the value set should be reflected
assertNotNull(probe.find("test_gauge_value").gauge());
assertEquals(42.0, probe.find("test_gauge_value").gauge().value(), 0.001);
}
@Test
public void timerRecordsTime() {
SimpleMeterRegistry probe = new SimpleMeterRegistry();
Monitors.addMeterRegistry(probe);
Monitors.getTimer("test_timer_record", "op", "read").record(100, TimeUnit.MILLISECONDS);
Timer timer = probe.find("test_timer_record").timer();
assertNotNull(timer);
assertEquals(1, timer.count());
}
@Test
public void metersWithDifferentTagsAreDistinct() {
MeterRegistry registry = Monitors.getRegistry();
Monitors.getCounter("test_tag_distinct", "env", "prod").increment(1);
Monitors.getCounter("test_tag_distinct", "env", "staging").increment(2);
// Two separate meters, not the same object
assertNotSame(
Monitors.getCounter("test_tag_distinct", "env", "prod"),
Monitors.getCounter("test_tag_distinct", "env", "staging"));
}
}
-31
View File
@@ -1,31 +0,0 @@
# Metrics
Conductor publishes detailed metrics for the server.
For the list of metrics published by server, see:
https://netflix.github.io/conductor/metrics/server/
Conductor supports plugging in metrics collectors, the following are currently supported by this module:
1. Datadog
2. Prometheus
3. Logging (dumps all the metrics to Slf4J logger)
## Published Artifacts
Group: `com.netflix.conductor`
| Published Artifact | Description |
| ----------- | ----------- |
| conductor-metrics | Metrics configuration |
**Note**: If you are using `condutor-contribs` as a dependency, the metrics module is already included, you do not need to include it separately.
#### Configuration
* Logging Metrics
`conductor.metrics-logger.enabled=true`
* Prometheus
`conductor.metrics-prometheus.enabled=true`
* Datadog
`conductor.metrics-datadog.enabled=true`
-37
View File
@@ -1,37 +0,0 @@
dependencies {
implementation project(':conductor-common')
compileOnly 'org.springframework.boot:spring-boot-starter'
compileOnly 'org.springframework.boot:spring-boot-starter-web'
implementation "org.apache.commons:commons-lang3:"
implementation "com.google.guava:guava:${revGuava}"
implementation "javax.ws.rs:jsr311-api:${revJsr311Api}"
implementation "io.reactivex:rxjava:${revRxJava}"
// Metrics
implementation "io.micrometer:micrometer-core:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-atlas:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-prometheus:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-datadog:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-jmx:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-otlp:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-dynatrace:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-elastic:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-new-relic:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-stackdriver:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-statsd:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-cloudwatch2:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-azure-monitor:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-influx:${revMicrometer}"
testImplementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation "org.testcontainers:mockserver:${revTestContainer}"
testImplementation "org.mock-server:mockserver-client-java:${revMockServerClient}"
}
-1
View File
@@ -1,6 +1,5 @@
dependencies {
implementation project(':conductor-core')
implementation project(':conductor-metrics')
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-autoconfigure'
-1
View File
@@ -5,7 +5,6 @@ plugins {
dependencies {
implementation project(':conductor-common')
implementation project(':conductor-metrics')
implementation project(':conductor-core')
implementation 'org.springframework.retry:spring-retry'
+3 -1
View File
@@ -46,7 +46,9 @@ dependencies {
implementation project(':conductor-kafka')
//Metrics
implementation project(':conductor-metrics')
implementation "io.micrometer:micrometer-registry-cloudwatch2:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-azure-monitor:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-prometheus:${revMicrometer}"
//Event Listener
implementation project(':conductor-workflow-event-listener')
+3 -1
View File
@@ -76,7 +76,9 @@ dependencies {
implementation project(':conductor-scheduler-sqlite-persistence')
//Metrics
implementation project(':conductor-metrics')
implementation "io.micrometer:micrometer-registry-cloudwatch2:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-azure-monitor:${revMicrometer}"
implementation "io.micrometer:micrometer-registry-prometheus:${revMicrometer}"
//Event Listener
implementation project(':conductor-workflow-event-listener')
@@ -10,7 +10,7 @@
* 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 com.netflix.conductor.contribs.metrics;
package com.netflix.conductor.server.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -34,7 +34,7 @@ public class AzureMonitorMetricsConfiguration {
public MeterRegistry getAzureMonitorMeterRegistry(
@Value("${management.azuremonitor.metrics.export.instrumentationKey:null}")
String instrumentationKey) {
AzureMonitorConfig cloudWatchConfig =
AzureMonitorConfig azureMonitorConfig =
new AzureMonitorConfig() {
@Override
public String instrumentationKey() {
@@ -46,6 +46,6 @@ public class AzureMonitorMetricsConfiguration {
return null;
}
};
return new AzureMonitorMeterRegistry(cloudWatchConfig, Clock.SYSTEM);
return new AzureMonitorMeterRegistry(azureMonitorConfig, Clock.SYSTEM);
}
}
@@ -10,7 +10,7 @@
* 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 com.netflix.conductor.contribs.metrics;
package com.netflix.conductor.server.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Conductor Authors.
* Copyright 2022 Conductor Authors.
* <p>
* Licensed 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
@@ -10,7 +10,7 @@
* 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 com.netflix.conductor.contribs.metrics;
package com.netflix.conductor.server.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
-1
View File
@@ -53,7 +53,6 @@ include 'common-persistence'
include 'mysql-persistence'
include 'postgres-persistence'
include 'sqlite-persistence'
include 'metrics'
include 'es7-persistence'
include 'es8-persistence'
include 'os-persistence'
-1
View File
@@ -11,7 +11,6 @@ dependencies {
implementation project(':conductor-grpc-server')
implementation project(':conductor-grpc-client')
implementation project(':conductor-redis-persistence')
implementation project(':conductor-metrics')
implementation "com.fasterxml.jackson.core:jackson-databind"