Skip to content

feat: native open telemetry metrics #2882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: next
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,6 @@

public class MicrometerMetrics implements Metrics {

private static final String PREFIX = "operator.sdk.";
private static final String RECONCILIATIONS = "reconciliations.";
private static final String RECONCILIATIONS_FAILED = RECONCILIATIONS + "failed";
private static final String RECONCILIATIONS_SUCCESS = RECONCILIATIONS + "success";
private static final String RECONCILIATIONS_RETRIES_LAST = RECONCILIATIONS + "retries.last";
private static final String RECONCILIATIONS_RETRIES_NUMBER = RECONCILIATIONS + "retries.number";
private static final String RECONCILIATIONS_STARTED = RECONCILIATIONS + "started";
private static final String RECONCILIATIONS_EXECUTIONS = PREFIX + RECONCILIATIONS + "executions.";
private static final String RECONCILIATIONS_QUEUE_SIZE = PREFIX + RECONCILIATIONS + "queue.size.";
private static final String NAME = "name";
private static final String NAMESPACE = "namespace";
private static final String GROUP = "group";
private static final String VERSION = "version";
private static final String KIND = "kind";
private static final String SCOPE = "scope";
private static final String METADATA_PREFIX = "resource.";
private static final String CONTROLLERS_EXECUTION = "controllers.execution.";
private static final String CONTROLLER = "controller";
private static final String SUCCESS_SUFFIX = ".success";
private static final String FAILURE_SUFFIX = ".failure";
private static final String TYPE = "type";
private static final String EXCEPTION = "exception";
private static final String EVENT = "event";
private static final String ACTION = "action";
private static final String EVENTS_RECEIVED = "events.received";
private static final String EVENTS_DELETE = "events.delete";
private static final String CLUSTER = "cluster";
private static final String SIZE_SUFFIX = ".size";
private final boolean collectPerResourceMetrics;
private final MeterRegistry registry;
private final Map<String, AtomicInteger> gauges = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -167,13 +139,13 @@ public <T> T timeControllerExecution(ControllerExecution<T> execution) {

@Override
public void receivedEvent(Event event, Map<String, Object> metadata) {
if (event instanceof ResourceEvent) {
if (event instanceof ResourceEvent resourceEvent) {
incrementCounter(
event.getRelatedCustomResourceID(),
EVENTS_RECEIVED,
metadata,
Tag.of(EVENT, event.getClass().getSimpleName()),
Tag.of(ACTION, ((ResourceEvent) event).getAction().toString()));
Tag.of(ACTION, resourceEvent.getAction().toString()));
} else {
incrementCounter(
event.getRelatedCustomResourceID(),
Expand Down
75 changes: 75 additions & 0 deletions open-telemetry-support/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.1.2-SNAPSHOT</version>
</parent>

<artifactId>open-telemetry-support</artifactId>
<name>Operator SDK - Open Telemetry Metrics Support</name>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>${opentelemetry.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit-5</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-httpclient-vertx</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.javaoperatorsdk.operator.monitoring.opentelemetry;

import java.util.HashMap;
import java.util.Map;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
import io.javaoperatorsdk.operator.api.reconciler.Constants;
import io.javaoperatorsdk.operator.api.reconciler.RetryInfo;
import io.javaoperatorsdk.operator.processing.Controller;
import io.javaoperatorsdk.operator.processing.GroupVersionKind;
import io.javaoperatorsdk.operator.processing.event.Event;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.Meter;

public class OpenTelemetryMetrics implements Metrics {

private Meter meter;
private final boolean collectPerResourceMetrics;

public OpenTelemetryMetrics(Meter meter, boolean collectPerResourceMetrics) {
this.meter = meter;
this.collectPerResourceMetrics = collectPerResourceMetrics;
}

@Override
public void controllerRegistered(Controller<? extends HasMetadata> controller) {}

@Override
public void receivedEvent(Event event, Map<String, Object> metadata) {
if (event instanceof ResourceEvent) {

} else {

}
}

@Override
public void reconcileCustomResource(
HasMetadata resource, RetryInfo retryInfo, Map<String, Object> metadata) {}

@Override
public void failedReconciliation(
HasMetadata resource, Exception exception, Map<String, Object> metadata) {}

@Override
public void reconciliationExecutionStarted(HasMetadata resource, Map<String, Object> metadata) {}

@Override
public void reconciliationExecutionFinished(HasMetadata resource, Map<String, Object> metadata) {}

@Override
public void cleanupDoneFor(ResourceID resourceID, Map<String, Object> metadata) {}

@Override
public void finishedReconciliation(HasMetadata resource, Map<String, Object> metadata) {}

@Override
public <T> T timeControllerExecution(ControllerExecution<T> execution) throws Exception {
return null;
}

@Override
public <T extends Map<?, ?>> T monitorSizeOf(T map, String name) {
return null;
}

private void incrementCounter(
ResourceID id,
String counterName,
Map<String, Object> metadata,
Map<String, String> additionalTags) {

meter.counterBuilder("").buildWithCallback(m -> {}).close();

Map<String, String> tags = new HashMap<>(additionalTags);
addMetadataTags(id, metadata, tags, false);

var counter = meter.counterBuilder(PREFIX + counterName).build();
var attributes = Attributes.builder();
additionalTags.forEach(attributes::put);
counter.add(1, attributes.build());
}

private void addMetadataTags(
ResourceID resourceID,
Map<String, Object> metadata,
Map<String, String> tags,
boolean prefixed) {
if (collectPerResourceMetrics) {
addTag(NAME, resourceID.getName(), tags, prefixed);
addTagOmittingOnEmptyValue(NAMESPACE, resourceID.getNamespace().orElse(null), tags, prefixed);
}
addTag(SCOPE, getScope(resourceID), tags, prefixed);
final var gvk = (GroupVersionKind) metadata.get(Constants.RESOURCE_GVK_KEY);
if (gvk != null) {
addGVKTags(gvk, tags, prefixed);
}
}

private static void addTag(
String name, String value, Map<String, String> tags, boolean prefixed) {
tags.put(getPrefixedMetadataTag(name, prefixed), value);
}

private static void addGVKTags(GroupVersionKind gvk, Map<String, String> tags, boolean prefixed) {
addTagOmittingOnEmptyValue(GROUP, gvk.getGroup(), tags, prefixed);
addTag(VERSION, gvk.getVersion(), tags, prefixed);
addTag(KIND, gvk.getKind(), tags, prefixed);
}

private static String getPrefixedMetadataTag(String tagName, boolean prefixed) {
return prefixed ? METADATA_PREFIX + tagName : tagName;
}

private static void addTagOmittingOnEmptyValue(
String name, String value, Map<String, String> tags, boolean prefixed) {
if (value != null && !value.isBlank()) {
addTag(name, value, tags, prefixed);
}
}

private static String getScope(ResourceID resourceID) {
return resourceID.getNamespace().isPresent() ? NAMESPACE : CLUSTER;
}
}
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 {};
}
Loading