Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions docs/content/en/docs/documentation/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Operator operator = new Operator(client, o -> o.withMetrics(metrics));
### Micrometer implementation

The micrometer implementation is typically created using one of the provided factory methods which, depending on which
is used, will return either a ready to use instance or a builder allowing users to customized how the implementation
is used, will return either a ready to use instance or a builder allowing users to customize how the implementation
behaves, in particular when it comes to the granularity of collected metrics. It is, for example, possible to collect
metrics on a per-resource basis via tags that are associated with meters. This is the default, historical behavior but
this will change in a future version of JOSDK because this dramatically increases the cardinality of metrics, which
Expand All @@ -62,14 +62,13 @@ instance via:

```java
MeterRegistry registry; // initialize your registry implementation
Metrics metrics = new MicrometerMetrics(registry);
Metrics metrics = MicrometerMetrics.newMicrometerMetricsBuilder(registry).build();
```

Note, however, that this constructor is deprecated and we encourage you to use the factory methods instead, which either
return a fully pre-configured instance or a builder object that will allow you to configure more easily how the instance
will behave. You can, for example, configure whether or not the implementation should collect metrics on a per-resource
basis, whether or not associated meters should be removed when a resource is deleted and how the clean-up is performed.
See the relevant classes documentation for more details.
The class provides factory methods which either return a fully pre-configured instance or a builder object that will
allow you to configure more easily how the instance will behave. You can, for example, configure whether the
implementation should collect metrics on a per-resource basis, whether associated meters should be removed when a
resource is deleted and how the clean-up is performed. See the relevant classes documentation for more details.

For example, the following will create a `MicrometerMetrics` instance configured to collect metrics on a per-resource
basis, deleting the associated meters after 5 seconds when a resource is deleted, using up to 2 threads to do so.
Expand Down Expand Up @@ -109,4 +108,30 @@ brackets (`[]`) won't be present when per-resource collection is disabled and ta
omitted if the associated value is empty. Of note, when in the context of controllers' execution metrics, these tag
names are prefixed with `resource.`. This prefix might be removed in a future version for greater consistency.

### Aggregated Metrics

The `AggregatedMetrics` class provides a way to combine multiple metrics providers into a single metrics instance using
the composite pattern. This is particularly useful when you want to simultaneously collect metrics data from different
monitoring systems or providers.

You can create an `AggregatedMetrics` instance by providing a list of existing metrics implementations:

```java
// create individual metrics instances
Metrics micrometerMetrics = MicrometerMetrics.withoutPerResourceMetrics(registry);
Metrics customMetrics = new MyCustomMetrics();
Metrics loggingMetrics = new LoggingMetrics();

// combine them into a single aggregated instance
Metrics aggregatedMetrics = new AggregatedMetrics(List.of(
micrometerMetrics,
customMetrics,
loggingMetrics
));

// use the aggregated metrics with your operator
Operator operator = new Operator(client, o -> o.withMetrics(aggregatedMetrics));
```

This approach allows you to easily combine different metrics collection strategies, such as sending metrics to both
Prometheus (via Micrometer) and a custom logging system simultaneously.
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import io.fabric8.kubernetes.api.builder.Builder;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.utils.Serialization;
Expand Down Expand Up @@ -73,36 +70,6 @@ public static String getNameFor(Class<? extends Reconciler> reconcilerClass) {
return getDefaultNameFor(reconcilerClass);
}

public static void checkIfCanAddOwnerReference(HasMetadata owner, HasMetadata resource) {
if (owner instanceof GenericKubernetesResource
|| resource instanceof GenericKubernetesResource) {
return;
}
if (owner instanceof Namespaced) {
if (!(resource instanceof Namespaced)) {
throw new OperatorException(
"Cannot add owner reference from a cluster scoped to a namespace scoped resource."
+ resourcesIdentifierDescription(owner, resource));
} else if (!Objects.equals(
owner.getMetadata().getNamespace(), resource.getMetadata().getNamespace())) {
throw new OperatorException(
"Cannot add owner reference between two resource in different namespaces."
+ resourcesIdentifierDescription(owner, resource));
}
}
}

private static String resourcesIdentifierDescription(HasMetadata owner, HasMetadata resource) {
return " Owner name: "
+ owner.getMetadata().getName()
+ " Kind: "
+ owner.getKind()
+ ", Resource name: "
+ resource.getMetadata().getName()
+ " Kind: "
+ resource.getKind();
}

public static String getNameFor(Reconciler reconciler) {
return getNameFor(reconciler.getClass());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.javaoperatorsdk.operator.api.config.informer;

public @interface Field {

String path();

String value();

boolean negated() default false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.javaoperatorsdk.operator.api.config.informer;

import java.util.Arrays;
import java.util.List;

public class FieldSelector {
private final List<Field> fields;

public FieldSelector(List<Field> fields) {
this.fields = fields;
}

public FieldSelector(Field... fields) {
this.fields = Arrays.asList(fields);
}

public List<Field> getFields() {
return fields;
}

public record Field(String path, String value, boolean negated) {
public Field(String path, String value) {
this(path, value, false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.javaoperatorsdk.operator.api.config.informer;

import java.util.ArrayList;
import java.util.List;

public class FieldSelectorBuilder {

private final List<FieldSelector.Field> fields = new ArrayList<>();

public FieldSelectorBuilder withField(String path, String value) {
fields.add(new FieldSelector.Field(path, value));
return this;
}

public FieldSelectorBuilder withoutField(String path, String value) {
fields.add(new FieldSelector.Field(path, value, true));
return this;
}

public FieldSelector build() {
return new FieldSelector(fields);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,7 @@
* the informer cache.
*/
long informerListLimit() default NO_LONG_VALUE_SET;

/** Kubernetes field selector for additional resource filtering */
Field[] fieldSelector() default {};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.javaoperatorsdk.operator.api.config.informer;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
Expand Down Expand Up @@ -36,6 +37,7 @@ public class InformerConfiguration<R extends HasMetadata> {
private GenericFilter<? super R> genericFilter;
private ItemStore<R> itemStore;
private Long informerListLimit;
private FieldSelector fieldSelector;

protected InformerConfiguration(
Class<R> resourceClass,
Expand All @@ -48,7 +50,8 @@ protected InformerConfiguration(
OnDeleteFilter<? super R> onDeleteFilter,
GenericFilter<? super R> genericFilter,
ItemStore<R> itemStore,
Long informerListLimit) {
Long informerListLimit,
FieldSelector fieldSelector) {
this(resourceClass);
this.name = name;
this.namespaces = namespaces;
Expand All @@ -60,6 +63,7 @@ protected InformerConfiguration(
this.genericFilter = genericFilter;
this.itemStore = itemStore;
this.informerListLimit = informerListLimit;
this.fieldSelector = fieldSelector;
}

private InformerConfiguration(Class<R> resourceClass) {
Expand Down Expand Up @@ -93,7 +97,8 @@ public static <R extends HasMetadata> InformerConfiguration<R>.Builder builder(
original.onDeleteFilter,
original.genericFilter,
original.itemStore,
original.informerListLimit)
original.informerListLimit,
original.fieldSelector)
.builder;
}

Expand Down Expand Up @@ -264,6 +269,10 @@ public Long getInformerListLimit() {
return informerListLimit;
}

public FieldSelector getFieldSelector() {
return fieldSelector;
}

@SuppressWarnings("UnusedReturnValue")
public class Builder {

Expand Down Expand Up @@ -329,6 +338,12 @@ public InformerConfiguration<R>.Builder initFromAnnotation(
final var informerListLimit =
informerListLimitValue == Constants.NO_LONG_VALUE_SET ? null : informerListLimitValue;
withInformerListLimit(informerListLimit);

withFieldSelector(
new FieldSelector(
Arrays.stream(informerConfig.fieldSelector())
.map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated()))
.toList()));
}
return this;
}
Expand Down Expand Up @@ -424,5 +439,10 @@ public Builder withInformerListLimit(Long informerListLimit) {
InformerConfiguration.this.informerListLimit = informerListLimit;
return this;
}

public Builder withFieldSelector(FieldSelector fieldSelector) {
InformerConfiguration.this.fieldSelector = fieldSelector;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ public Builder<R> withInformerListLimit(Long informerListLimit) {
return this;
}

public Builder<R> withFieldSelector(FieldSelector fieldSelector) {
config.withFieldSelector(fieldSelector);
return this;
}

public void updateFrom(InformerConfiguration<R> informerConfig) {
if (informerConfig != null) {
final var informerConfigName = informerConfig.getName();
Expand All @@ -281,7 +286,8 @@ public void updateFrom(InformerConfiguration<R> informerConfig) {
.withOnUpdateFilter(informerConfig.getOnUpdateFilter())
.withOnDeleteFilter(informerConfig.getOnDeleteFilter())
.withGenericFilter(informerConfig.getGenericFilter())
.withInformerListLimit(informerConfig.getInformerListLimit());
.withInformerListLimit(informerConfig.getInformerListLimit())
.withFieldSelector(informerConfig.getFieldSelector());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package io.javaoperatorsdk.operator.api.monitoring;

import java.util.List;
import java.util.Map;
import java.util.Objects;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.RetryInfo;
import io.javaoperatorsdk.operator.processing.Controller;
import io.javaoperatorsdk.operator.processing.event.Event;
import io.javaoperatorsdk.operator.processing.event.ResourceID;

/**
* An aggregated implementation of the {@link Metrics} interface that delegates method calls to a
* collection of {@link Metrics} instances using the composite pattern.
*
* <p>This class allows multiple metrics providers to be combined into a single metrics instance,
* enabling simultaneous collection of metrics data by different monitoring systems or providers.
* All method calls are delegated to each metrics instance in the list in the order they were
* provided to the constructor.
*
* <p><strong>Important:</strong> The {@link #timeControllerExecution(ControllerExecution)} method
* is handled specially - it is only invoked on the first metrics instance in the list, since it's
* not an idempotent operation and can only be executed once. The controller execution cannot be
* repeated multiple times as it would produce side effects and potentially inconsistent results.
*
* <p>All other methods are called on every metrics instance in the list, preserving the order of
* execution as specified in the constructor.
*
* @see Metrics
*/
public final class AggregatedMetrics implements Metrics {

private final List<Metrics> metricsList;

/**
* Creates a new AggregatedMetrics instance that will delegate method calls to the provided list
* of metrics instances.
*
* @param metricsList the list of metrics instances to delegate to; must not be null and must
* contain at least one metrics instance
* @throws NullPointerException if metricsList is null
* @throws IllegalArgumentException if metricsList is empty
*/
public AggregatedMetrics(List<Metrics> metricsList) {
Objects.requireNonNull(metricsList, "metricsList must not be null");
if (metricsList.isEmpty()) {
throw new IllegalArgumentException("metricsList must contain at least one Metrics instance");
}
this.metricsList = List.copyOf(metricsList);
}

@Override
public void controllerRegistered(Controller<? extends HasMetadata> controller) {
metricsList.forEach(metrics -> metrics.controllerRegistered(controller));
}

@Override
public void receivedEvent(Event event, Map<String, Object> metadata) {
metricsList.forEach(metrics -> metrics.receivedEvent(event, metadata));
}

@Override
public void reconcileCustomResource(
HasMetadata resource, RetryInfo retryInfo, Map<String, Object> metadata) {
metricsList.forEach(metrics -> metrics.reconcileCustomResource(resource, retryInfo, metadata));
}

@Override
public void failedReconciliation(
HasMetadata resource, Exception exception, Map<String, Object> metadata) {
metricsList.forEach(metrics -> metrics.failedReconciliation(resource, exception, metadata));
}

@Override
public void reconciliationExecutionStarted(HasMetadata resource, Map<String, Object> metadata) {
metricsList.forEach(metrics -> metrics.reconciliationExecutionStarted(resource, metadata));
}

@Override
public void reconciliationExecutionFinished(HasMetadata resource, Map<String, Object> metadata) {
metricsList.forEach(metrics -> metrics.reconciliationExecutionFinished(resource, metadata));
}

@Override
public void cleanupDoneFor(ResourceID resourceID, Map<String, Object> metadata) {
metricsList.forEach(metrics -> metrics.cleanupDoneFor(resourceID, metadata));
}

@Override
public void finishedReconciliation(HasMetadata resource, Map<String, Object> metadata) {
metricsList.forEach(metrics -> metrics.finishedReconciliation(resource, metadata));
}

@Override
public <T> T timeControllerExecution(ControllerExecution<T> execution) throws Exception {
return metricsList.get(0).timeControllerExecution(execution);
}

@Override
public <T extends Map<?, ?>> T monitorSizeOf(T map, String name) {
metricsList.forEach(metrics -> metrics.monitorSizeOf(map, name));
return map;
}
}
Loading
Loading