Skip to content

Commit

Permalink
Issue #131: Fixed incorrect source keys when collect type is undefined
Browse files Browse the repository at this point in the history
* Source keys are now generated at the pre processing phase.
* Added unit tests.
* Fixed failed unit tests.
* Removed keys retrieval method from the AbstractMapDeserializer class.
  • Loading branch information
NassimBtk committed Mar 21, 2024
1 parent ca696e1 commit aa54121
Show file tree
Hide file tree
Showing 16 changed files with 319 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.sentrysoftware.metricshub.engine.connector.model.monitor.MonitorJob;
import org.sentrysoftware.metricshub.engine.connector.parser.ConnectorParser;
import org.sentrysoftware.metricshub.engine.connector.parser.ReferenceResolverProcessor;
import org.sentrysoftware.metricshub.engine.connector.parser.SourceKeyProcessor;
import org.sentrysoftware.metricshub.engine.connector.update.ConnectorUpdateChain;

/**
Expand All @@ -64,7 +65,7 @@ public Map<String, MonitorJob> deserialize(JsonParser parser, DeserializationCon
}

// Resolve relative source references through the ReferenceResolverProcessor
final JsonNode monitorsNode = new ReferenceResolverProcessor(null)
final JsonNode monitorsNode = new ReferenceResolverProcessor(new SourceKeyProcessor())
.process(JsonNodeFactory.instance.objectNode().set("monitors", jsonNode));

// Parse the connector like it is done by the engine for regular connectors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,12 @@
*/

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonStreamContext;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Map;
import java.util.stream.Collectors;

/**
* An abstract class providing deserialization support for custom types using Jackson's ObjectMapper
Expand All @@ -40,16 +37,12 @@
*/
public abstract class AbstractMapDeserializer<T> extends JsonDeserializer<Map<String, T>> {

protected String nodePath;

@Override
public Map<String, T> deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException {
if (parser == null) {
return emptyMap();
}

nodePath = getNodePath(parser.getParsingContext().getParent(), new LinkedList<>());

final Map<String, T> map = parser.readValueAs(getTypeReference());

if (map == null) {
Expand All @@ -60,47 +53,13 @@ public Map<String, T> deserialize(JsonParser parser, DeserializationContext ctxt
throw new InvalidFormatException(parser, messageOnInvalidMap(parser.getCurrentName()), map, Map.class);
}

updateMapValues(parser, ctxt, map);

if (isExpectedInstance(map)) {
return map;
}

return fromMap(map);
}

/**
* Get the current node path E.g. monitors.enclosure.discovery.sources
*
* @param context streaming processing contexts used during reading the content
* @param path linked list used to construct a sequence of characters separated by the dot delimiter
* @return String value
*/
private String getNodePath(final JsonStreamContext context, final LinkedList<String> path) {
// Recursively call the parent of the context to build the full node path
// Stop if the parent is null, it means that root parent is reached
if (context != null && context.getParent() != null) {
if (context.inObject()) {
path.push(context.getCurrentName());
} else if (context.inArray()) {
path.push(String.format("[%s]", context.getCurrentIndex()));
}

getNodePath(context.getParent(), path);
}

return path.stream().collect(Collectors.joining(".", "${source::", ""));
}

/**
* Update the given map using the current parser and its context
*
* @param parser
* @param ctxt Context that can be used to access information about this deserialization activity
* @param map Parsed used for reading JSON content
*/
protected abstract void updateMapValues(JsonParser parser, DeserializationContext ctxt, Map<String, T> map);

/**
* Get the error message to display when the map is invalid
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import java.util.Map;
import java.util.TreeMap;

Expand All @@ -34,11 +32,6 @@
*/
public class CaseInsensitiveTreeMapDeserializer extends AbstractMapDeserializer<String> {

@Override
protected void updateMapValues(JsonParser parser, DeserializationContext ctxt, Map<String, String> map) {
// No updates
}

@Override
protected String messageOnInvalidMap(String nodeKey) {
return String.format("The key referenced by '%s' cannot be empty.", nodeKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
*/
private void callPostDeserialize(final Object deserializedObject) {
if (deserializedObject instanceof Source source) {
// Temporary remove the source key so that
// the 'references' method will not detect the key as a reference
// thus, a source will not reference itself incorrectly
final String sourceKey = source.getKey();
source.setKey(null);

final Set<String> refs = new HashSet<>();

references(
Expand All @@ -90,6 +96,9 @@ private void callPostDeserialize(final Object deserializedObject) {
val -> REFERENCE_PATTERN.matcher(val).find()
);

// Set the source key
source.setKey(sourceKey);

source.setReferences(refs);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import java.util.Map;
import org.sentrysoftware.metricshub.engine.connector.model.monitor.task.source.Source;

Expand All @@ -42,11 +40,6 @@ protected boolean isValidMap(Map<String, Source> map) {
return map.keySet().stream().noneMatch(key -> key == null || key.isBlank());
}

@Override
protected void updateMapValues(JsonParser parser, DeserializationContext ctxt, Map<String, Source> map) {
map.forEach((key, source) -> source.setKey(String.format("%s.%s}", nodePath, key)));
}

@Override
protected TypeReference<Map<String, Source>> getTypeReference() {
return new TypeReference<Map<String, Source>>() {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import static org.sentrysoftware.metricshub.engine.common.helpers.MetricsHubConstants.NEW_LINE;
import static org.sentrysoftware.metricshub.engine.common.helpers.StringHelper.addNonNull;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
Expand All @@ -43,6 +42,7 @@
import org.sentrysoftware.metricshub.engine.connector.model.common.ExecuteForEachEntryOf;
import org.sentrysoftware.metricshub.engine.connector.model.common.IEntryConcatMethod;
import org.sentrysoftware.metricshub.engine.connector.model.monitor.task.source.compute.Compute;
import org.sentrysoftware.metricshub.engine.connector.parser.SourceKeyProcessor;
import org.sentrysoftware.metricshub.engine.strategy.source.ISourceProcessor;
import org.sentrysoftware.metricshub.engine.strategy.source.SourceTable;

Expand Down Expand Up @@ -89,9 +89,9 @@ public abstract class Source implements Serializable {
protected boolean forceSerialization;

/**
* A key associated with the source, excluded from JSON serialization using @JsonIgnore.
* A key associated with the source, this key is automatically set during the pre processing phase of the connector.
* See {@link SourceKeyProcessor}
*/
@JsonIgnore
protected String key;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,10 @@ public class ConstantsProcessor extends AbstractNodeProcessor {
/**
* Constructs a ConstantsProcessor with a next processor.
*/
private ConstantsProcessor(AbstractNodeProcessor next) {
public ConstantsProcessor(AbstractNodeProcessor next) {
super(next);
}

/**
* Constructs a ConstantsProcessor without a next processor.
*/
public ConstantsProcessor() {
this(null);
}

@Override
public JsonNode processNode(JsonNode node) {
final JsonNode constantsNode = node.get("constants");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@
public class NodeProcessorHelper {

/**
* Creates a new {@link ConstantsProcessor}.
* Creates a new {@link ConstantsProcessor} with a {@link SourceKeyProcessor} as destination.
*
* @return A new {@link ConstantsProcessor} instance.
*/
private static AbstractNodeProcessor constantsProcessor() {
return new ConstantsProcessor();
private static AbstractNodeProcessor constantsProcessorWithSourceKeyProcessor() {
return new ConstantsProcessor(new SourceKeyProcessor());
}

/**
Expand All @@ -57,7 +57,7 @@ public static AbstractNodeProcessor withExtendsAndConstantsProcessor(
.builder()
.connectorDirectory(connectorDirectory)
.mapper(mapper)
.next(new ReferenceResolverProcessor(constantsProcessor()))
.next(new ReferenceResolverProcessor(constantsProcessorWithSourceKeyProcessor()))
.build();
}

Expand All @@ -82,7 +82,7 @@ public static AbstractNodeProcessor withExtendsAndTemplateVariableProcessor(
TemplateVariableProcessor
.builder()
.connectorVariables(connectorVariables)
.next(new ReferenceResolverProcessor(constantsProcessor()))
.next(new ReferenceResolverProcessor(constantsProcessorWithSourceKeyProcessor()))
.build()
)
.mapper(mapper)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package org.sentrysoftware.metricshub.engine.connector.parser;

/*-
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
* MetricsHub Engine
* ჻჻჻჻჻჻
* Copyright 2023 - 2024 Sentry Software
* ჻჻჻჻჻჻
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;

/**
* Processes source keys in a given JSON node structure. This processor modifies the input JSON node by
* adding a "key" field to the source nodes. It is designed to work within a chain of node processors,
* handling specific node transformations related to source keys.
*
* <br>The processor handles two specific parts of the node structure:
* <ul>
* <li>
* Pre-nodes: Adds a "key" to each source node under "pre" based on the source name.
* E.g. <strong>${source::pre.source_1}</strong>.
* </li>
* <li>
* Monitor nodes: For specified monitor job types ("discovery", "collect", "simple"), adds a "key" to
* each source node under "monitors" based on the monitor name, job type, and source name.
* E.g. <strong>${source::monitors.system.collect.sources.source_1}</strong>.
* </li>
* </ul>
*
* <p>This class extends {@link AbstractNodeProcessor}, allowing for chain-of-responsibility pattern implementation.</p>
*/
public class SourceKeyProcessor extends AbstractNodeProcessor {

/**
* The source's key property
*/
private static final String SOURCE_KEY_PROPERTY = "key";

/**
* Known monitor job type: discovery, collect and simple
*/
private static final Set<String> MONITOR_JOB_TYPES;

static {
final Set<String> monitorJobTypes = new LinkedHashSet<>();
monitorJobTypes.addAll(Set.of("discovery", "collect", "simple"));
MONITOR_JOB_TYPES = monitorJobTypes;
}

/**
* Constructs a SourceKeyProcessor with a next processor.
*/
public SourceKeyProcessor(AbstractNodeProcessor next) {
super(next);
}

/**
* Constructs a SourceKeyProcessor without a next processor.
*/
public SourceKeyProcessor() {
this(null);
}

@Override
protected JsonNode processNode(JsonNode node) throws IOException {
// Get the "pre" JSON node
final JsonNode preNode = node.get("pre");
// Make sure the node is available
if (preNode != null && !preNode.isNull()) {
// Loop over the source nodes and set the key property on each source node
preNode
.fields()
.forEachRemaining(sourceNodeEntry -> {
final String sourceName = sourceNodeEntry.getKey();
final JsonNode sourceNode = sourceNodeEntry.getValue();
final ObjectNode sourceObjectNode = (ObjectNode) sourceNode;
sourceObjectNode.set(SOURCE_KEY_PROPERTY, new TextNode(String.format("${source::pre.%s}", sourceName)));
});
}

// Attempt to get the "monitors" node
final JsonNode monitorsNode = node.get("monitors");
// Make sure the node is available
if (monitorsNode != null && !monitorsNode.isNull()) {
// Traverse the monitors until the source nodes and set the key of each source node
monitorsNode
.fields()
.forEachRemaining(monitorEntry -> {
final String monitorName = monitorEntry.getKey();
final JsonNode monitorJobsNode = monitorEntry.getValue();
monitorJobsNode
.fields()
.forEachRemaining(monitorJobEntry -> {
final String monitorJobType = monitorJobEntry.getKey();
// Make sure the node is a monitor job node (discovery, collect, simple)
if (MONITOR_JOB_TYPES.contains(monitorJobType)) {
final JsonNode monitorJobNode = monitorJobEntry.getValue();
final JsonNode sourcesNode = monitorJobNode.get("sources");
if (sourcesNode != null && !sourcesNode.isNull()) {
sourcesNode
.fields()
.forEachRemaining(sourceEntry -> {
final String sourceName = sourceEntry.getKey();
final JsonNode sourceNode = sourceEntry.getValue();
final ObjectNode sourceObjectNode = (ObjectNode) sourceNode;
sourceObjectNode.set(
SOURCE_KEY_PROPERTY,
new TextNode(
String.format("${source::monitors.%s.%s.sources.%s}", monitorName, monitorJobType, sourceName)
)
);
});
}
}
});
});
}
return node;
}
}
Loading

0 comments on commit aa54121

Please sign in to comment.