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:
@@ -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'
|
||||
|
||||
+12
-11
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
@@ -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,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'
|
||||
|
||||
@@ -5,7 +5,6 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
implementation project(':conductor-common')
|
||||
implementation project(':conductor-metrics')
|
||||
implementation project(':conductor-core')
|
||||
|
||||
implementation 'org.springframework.retry:spring-retry'
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user