Skip to content

feat: add metadata support for Metrics #1323

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

Merged
merged 6 commits into from
Jul 13, 2022
Merged
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
@@ -1,14 +1,16 @@
package io.javaoperatorsdk.operator.monitoring.micrometer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import io.javaoperatorsdk.operator.OperatorException;
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.GroupVersionKind;
import io.javaoperatorsdk.operator.processing.event.Event;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.micrometer.core.instrument.MeterRegistry;
Expand All @@ -27,9 +29,24 @@ public MicrometerMetrics(MeterRegistry registry) {
public <T> T timeControllerExecution(ControllerExecution<T> execution) {
final var name = execution.controllerName();
final var execName = PREFIX + "controllers.execution." + execution.name();
final var resourceID = execution.resourceID();
final var metadata = execution.metadata();
final var tags = new ArrayList<String>(metadata.size() + 4);
tags.addAll(List.of(
"controller", name,
"resource.name", resourceID.getName(),
"resource.namespace", resourceID.getNamespace().orElse(""),
"resource.scope", resourceID.getNamespace().isPresent() ? "namespace" : "cluster"));
final var gvk = (GroupVersionKind) metadata.get(Constants.RESOURCE_GVK_KEY);
if (gvk != null) {
tags.addAll(List.of(
"resource.group", gvk.group,
"resource.version", gvk.version,
"resource.kind", gvk.kind));
}
final var timer =
Timer.builder(execName)
.tags("controller", name)
.tags(tags.toArray(new String[0]))
.publishPercentiles(0.3, 0.5, 0.95)
.publishPercentileHistogram()
.register(registry);
Expand All @@ -55,55 +72,70 @@ public <T> T timeControllerExecution(ControllerExecution<T> execution) {
}
}

public void receivedEvent(Event event) {
incrementCounter(event.getRelatedCustomResourceID(), "events.received", "event",
event.getClass().getSimpleName());
public void receivedEvent(Event event, Map<String, Object> metadata) {
incrementCounter(event.getRelatedCustomResourceID(), "events.received",
metadata,
"event", event.getClass().getSimpleName());
}

@Override
public void cleanupDoneFor(ResourceID resourceID) {
incrementCounter(resourceID, "events.delete");
public void cleanupDoneFor(ResourceID resourceID, Map<String, Object> metadata) {
incrementCounter(resourceID, "events.delete", metadata);
}

@Override
public void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfoNullable) {
public void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfoNullable,
Map<String, Object> metadata) {
Optional<RetryInfo> retryInfo = Optional.ofNullable(retryInfoNullable);
incrementCounter(resourceID, RECONCILIATIONS + "started",
metadata,
RECONCILIATIONS + "retries.number",
"" + retryInfo.map(RetryInfo::getAttemptCount).orElse(0),
RECONCILIATIONS + "retries.last",
"" + retryInfo.map(RetryInfo::isLastAttempt).orElse(true));
}

@Override
public void finishedReconciliation(ResourceID resourceID) {
incrementCounter(resourceID, RECONCILIATIONS + "success");
public void finishedReconciliation(ResourceID resourceID, Map<String, Object> metadata) {
incrementCounter(resourceID, RECONCILIATIONS + "success", metadata);
}

public void failedReconciliation(ResourceID resourceID, Exception exception) {
public void failedReconciliation(ResourceID resourceID, Exception exception,
Map<String, Object> metadata) {
var cause = exception.getCause();
if (cause == null) {
cause = exception;
} else if (cause instanceof RuntimeException) {
cause = cause.getCause() != null ? cause.getCause() : cause;
}
incrementCounter(resourceID, RECONCILIATIONS + "failed", "exception",
incrementCounter(resourceID, RECONCILIATIONS + "failed", metadata, "exception",
cause.getClass().getSimpleName());
}

public <T extends Map<?, ?>> T monitorSizeOf(T map, String name) {
return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map);
}

private void incrementCounter(ResourceID id, String counterName, String... additionalTags) {
var tags = List.of(
private void incrementCounter(ResourceID id, String counterName, Map<String, Object> metadata,
String... additionalTags) {
final var additionalTagsNb =
additionalTags != null && additionalTags.length > 0 ? additionalTags.length : 0;
final var metadataNb = metadata != null ? metadata.size() : 0;
final var tags = new ArrayList<String>(6 + additionalTagsNb + metadataNb);
tags.addAll(List.of(
"name", id.getName(),
"name", id.getName(), "namespace", id.getNamespace().orElse(""),
"scope", id.getNamespace().isPresent() ? "namespace" : "cluster");
if (additionalTags != null && additionalTags.length > 0) {
tags = new LinkedList<>(tags);
"namespace", id.getNamespace().orElse(""),
"scope", id.getNamespace().isPresent() ? "namespace" : "cluster"));
if (additionalTagsNb > 0) {
tags.addAll(List.of(additionalTags));
}
if (metadataNb > 0) {
final var gvk = (GroupVersionKind) metadata.get(Constants.RESOURCE_GVK_KEY);
tags.addAll(List.of(
"group", gvk.group,
"version", gvk.version,
"kind", gvk.kind));
}
registry.counter(PREFIX + counterName, tags.toArray(new String[0])).increment();
}
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,213 @@
package io.javaoperatorsdk.operator.api.monitoring;

import java.util.Collections;
import java.util.Map;

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

/**
* An interface that metrics providers can implement and that the SDK will call at different times
* of its execution cycle.
*/
public interface Metrics {

/**
* The default Metrics provider: a no-operation implementation.
*/
Metrics NOOP = new Metrics() {};

default void receivedEvent(Event event) {}
/**
* Called when an event has been accepted by the SDK from an event source, which would result in
* potentially triggering the associated Reconciler.
*
* @param event the event
* @param metadata metadata associated with the resource being processed
*/
default void receivedEvent(Event event, Map<String, Object> metadata) {}

default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo) {}
/**
*
* @deprecated Use (and implement) {@link #receivedEvent(Event, Map)} instead
*/
@Deprecated
default void receivedEvent(Event event) {
receivedEvent(event, Collections.emptyMap());
}

default void failedReconciliation(ResourceID resourceID, Exception exception) {}
/**
*
* @deprecated Use (and implement) {@link #reconcileCustomResource(ResourceID, RetryInfo, Map)}
* instead
*/
@Deprecated
default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo) {
reconcileCustomResource(resourceID, retryInfo, Collections.emptyMap());
}

default void cleanupDoneFor(ResourceID resourceID) {}
/**
* Called right before a resource is dispatched to the ExecutorService for reconciliation.
*
* @param resourceID the {@link ResourceID} associated with the resource
* @param retryInfo the current retry state information for the reconciliation request
* @param metadata metadata associated with the resource being processed
*/
default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo,
Map<String, Object> metadata) {}

default void finishedReconciliation(ResourceID resourceID) {}
/**
*
* @deprecated Use (and implement) {@link #failedReconciliation(ResourceID, Exception, Map)}
* instead
*/
@Deprecated
default void failedReconciliation(ResourceID resourceID, Exception exception) {
failedReconciliation(resourceID, exception, Collections.emptyMap());
}

/**
* Called when a precedent reconciliation for the resource associated with the specified
* {@link ResourceID} resulted in the provided exception, resulting in a retry of the
* reconciliation.
*
* @param resourceID the {@link ResourceID} associated with the resource being processed
* @param exception the exception that caused the failed reconciliation resulting in a retry
* @param metadata metadata associated with the resource being processed
*/
default void failedReconciliation(ResourceID resourceID, Exception exception,
Map<String, Object> metadata) {}

/**
*
* @deprecated Use (and implement) {@link #cleanupDoneFor(ResourceID, Map)} instead
*/
@Deprecated
default void cleanupDoneFor(ResourceID resourceID) {
cleanupDoneFor(resourceID, Collections.emptyMap());
}

/**
* Called when the resource associated with the specified {@link ResourceID} has been successfully
* deleted and the clean-up performed by the associated reconciler is finished.
*
* @param resourceID the {@link ResourceID} associated with the resource being processed
* @param metadata metadata associated with the resource being processed
*/
default void cleanupDoneFor(ResourceID resourceID, Map<String, Object> metadata) {}

/**
*
* @deprecated Use (and implement) {@link #finishedReconciliation(ResourceID, Map)} instead
*/
@Deprecated
default void finishedReconciliation(ResourceID resourceID) {
finishedReconciliation(resourceID, Collections.emptyMap());
}

/**
* Called when the
* {@link io.javaoperatorsdk.operator.api.reconciler.Reconciler#reconcile(HasMetadata, Context)}
* method of the Reconciler associated with the resource associated with the specified
* {@link ResourceID} has sucessfully finished.
*
* @param resourceID the {@link ResourceID} associated with the resource being processed
* @param metadata metadata associated with the resource being processed
*/
default void finishedReconciliation(ResourceID resourceID, Map<String, Object> metadata) {}

/**
* Encapsulates the information about a controller execution i.e. a call to either
* {@link io.javaoperatorsdk.operator.api.reconciler.Reconciler#reconcile(HasMetadata, Context)}
* or {@link io.javaoperatorsdk.operator.api.reconciler.Cleaner#cleanup(HasMetadata, Context)}.
* Note that instances are automatically created for you by the SDK and passed to your Metrics
* implementation at the appropriate time to the
* {@link #timeControllerExecution(ControllerExecution)} method.
*
* @param <T> the outcome type associated with the controller execution. Currently, one of
* {@link io.javaoperatorsdk.operator.api.reconciler.UpdateControl} or
* {@link io.javaoperatorsdk.operator.api.reconciler.DeleteControl}
*/
interface ControllerExecution<T> {

/**
* Retrieves the name of type of reconciliation being performed: either {@code reconcile} or
* {@code cleanup}.
*
* @return the name of type of reconciliation being performed
*/
String name();

/**
* Retrieves the name of the controller executing the reconciliation.
*
* @return the associated controller name
*/
String controllerName();

/**
* Retrieves the name of the successful result when the reconciliation ended positively.
* Possible values comes from the different outcomes provided by
* {@link io.javaoperatorsdk.operator.api.reconciler.UpdateControl} or
* {@link io.javaoperatorsdk.operator.api.reconciler.DeleteControl}.
*
* @param result the reconciliation result
* @return a name associated with the specified outcome
*/
String successTypeName(T result);

/**
* Retrieves the {@link ResourceID} of the resource associated with the controller execution
* being considered
*
* @return the {@link ResourceID} of the resource being reconciled
*/
ResourceID resourceID();

/**
* Retrieves metadata associated with the current reconciliation, typically additional
* information (such as kind) about the resource being reconciled
*
* @return metadata associated with the current reconciliation
*/
Map<String, Object> metadata();

/**
* Performs the controller execution.
*
* @return the result of the controller execution
* @throws Exception if an error occurred during the controller's execution
*/
T execute() throws Exception;
}

/**
* Times the execution of the controller operation encapsulated by the provided
* {@link ControllerExecution}.
*
* @param execution the controller operation to be timed
* @return the result of the controller's execution if successful
* @param <T> the type of the outcome/result of the controller's execution
* @throws Exception if an error occurred during the controller's execution, usually this should
* just be a pass-through of whatever the controller returned
*/
default <T> T timeControllerExecution(ControllerExecution<T> execution) throws Exception {
return execution.execute();
}

/**
* Monitors the size of the specified map. This currently isn't used directly by the SDK but could
* be used by operators to monitor some of their structures, such as cache size.
*
* @param map the Map which size is to be monitored
* @param name the name of the provided Map to be used in metrics data
* @return the Map that was passed in so the registration can be done as part of an assignment
* statement.
* @param <T> the type of the Map being monitored
*/
@SuppressWarnings("unused")
default <T extends Map<?, ?>> T monitorSizeOf(T map, String name) {
return map;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ public final class Constants {
public static final long NO_RECONCILIATION_MAX_INTERVAL = -1L;
public static final String SAME_AS_CONTROLLER = "JOSDK_SAME_AS_CONTROLLER";

public static final String RESOURCE_GVK_KEY = "josdk.resource.gvk";

private Constants() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public Optional<RetryInfo> getRetryInfo() {
}

@Override
@SuppressWarnings("unchecked")
public <T> Set<T> getSecondaryResources(Class<T> expectedType) {
return controller.getEventSourceManager().getEventSourcesFor(expectedType).stream()
.map(es -> es.getSecondaryResources(primaryResource))
Expand Down
Loading