Skip to content

Commit

Permalink
Issue #115: Develop HTTP Extension
Browse files Browse the repository at this point in the history
* Developed Http Extension and all its classes
* Refactored all the tests and made the necessary changes to make them work
* Tested the engine and the agent
  • Loading branch information
CherfaElyes committed Apr 8, 2024
1 parent ba71684 commit 0db2850
Show file tree
Hide file tree
Showing 37 changed files with 2,214 additions and 935 deletions.
4 changes: 4 additions & 0 deletions metricshub-agent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>org.sentrysoftware</groupId>
<artifactId>http</artifactId>
</dependency>

<!-- OpenTelemetry -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public class ProtocolsConfig {
private WmiProtocolConfig wmi;

@JsonSetter(nulls = SKIP)
private HttpProtocolConfig http;
@JsonDeserialize(using = ExtensionConfigDeserializer.class)
private IConfiguration http;

@JsonSetter(nulls = SKIP)
private OsCommandProtocolConfig osCommand;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
import org.sentrysoftware.metricshub.agent.config.ResourceConfig;
import org.sentrysoftware.metricshub.agent.config.ResourceGroupConfig;
import org.sentrysoftware.metricshub.agent.config.protocols.AbstractProtocolConfig;
import org.sentrysoftware.metricshub.agent.config.protocols.HttpProtocolConfig;
import org.sentrysoftware.metricshub.agent.config.protocols.IpmiProtocolConfig;
import org.sentrysoftware.metricshub.agent.config.protocols.OsCommandProtocolConfig;
import org.sentrysoftware.metricshub.agent.config.protocols.ProtocolsConfig;
Expand Down Expand Up @@ -110,7 +109,6 @@
public class ConfigHelper {

private static final String OS_COMMAND = "OSCommand";
private static final String HTTP_PROTOCOL = "HTTP";
private static final String WMI_PROTOCOL = "WMI";
private static final String WBEM_PROTOCOL = "WBEM";
private static final String SSH_PROTOCOL = "SSH";
Expand Down Expand Up @@ -971,9 +969,9 @@ private static void validateProtocols(@NonNull final String resourceKey, final R
validateWmiInfo(resourceKey, wmiConfig.getTimeout());
}

final HttpProtocolConfig httpConfig = protocolsConfig.getHttp();
final IConfiguration httpConfig = protocolsConfig.getHttp();
if (httpConfig != null) {
validateHttpInfo(resourceKey, httpConfig.getTimeout(), httpConfig.getPort());
httpConfig.validateConfiguration(resourceKey);
}

final OsCommandProtocolConfig osCommandConfig = protocolsConfig.getOsCommand();
Expand Down Expand Up @@ -1125,29 +1123,6 @@ static void validateWmiInfo(final String resourceKey, final Long timeout) throws
);
}

/**
* Validate the given HTTP information (timeout and port)
*
* @param resourceKey Resource unique identifier
* @param timeout How long until the HTTP request times out
* @param port The HTTP port number used to perform REST queries
* @throws InvalidConfigurationException
*/
static void validateHttpInfo(final String resourceKey, final Long timeout, final Integer port)
throws InvalidConfigurationException {
StringHelper.validateConfigurationAttribute(
timeout,
INVALID_TIMEOUT_CHECKER,
() -> String.format(TIMEOUT_ERROR, resourceKey, HTTP_PROTOCOL, timeout)
);

StringHelper.validateConfigurationAttribute(
port,
INVALID_PORT_CHECKER,
() -> String.format(PORT_ERROR, resourceKey, HTTP_PROTOCOL, port)
);
}

/**
* Validate the given OS Command information: timeout
*
Expand Down Expand Up @@ -1184,7 +1159,6 @@ static HostConfiguration buildHostConfiguration(
Stream
.of(
protocols.getSsh(),
protocols.getHttp(),
protocols.getWbem(),
protocols.getWmi(),
protocols.getOsCommand(),
Expand All @@ -1202,7 +1176,7 @@ static HostConfiguration buildHostConfiguration(
? new HashMap<>()
: new HashMap<>(
Stream
.of(protocols.getSnmp())
.of(protocols.getSnmp(), protocols.getHttp())
.filter(Objects::nonNull)
.collect(Collectors.toMap(IConfiguration::getClass, Function.identity()))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/

import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.sentrysoftware.metricshub.engine.configuration.HttpConfiguration;
import org.sentrysoftware.metricshub.cli.service.CliExtensionManager;
import org.sentrysoftware.metricshub.engine.common.exception.InvalidConfigurationException;
import org.sentrysoftware.metricshub.engine.configuration.IConfiguration;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Option;

/**
* This class is used by MetricsHubCliService to configure Http protocol when using the MetricsHub CLI.
* It create the engine's {@link HttpConfiguration} object that is used to monitor a specific resource through REST.
* It create the engine's {@link IConfiguration} for HTTP object that is used to monitor a specific resource through REST.
*/
@Data
@EqualsAndHashCode(callSuper = true)
Expand Down Expand Up @@ -92,23 +98,28 @@ public static class HttpOrHttps {
defaultValue = "" + DEFAULT_TIMEOUT,
description = "Timeout in seconds for HTTP operations (default: ${DEFAULT-VALUE} s)"
)
private long timeout;
private String timeout;

/**
* @param defaultUsername Username specified at the top level of the CLI (with the --username option)
* @param defaultPassword Password specified at the top level of the CLI (with the --password option)
* @return an HttpProtocol instance corresponding to the options specified by the user in the CLI
* @throws InvalidConfigurationException
*/
@Override
public IConfiguration toProtocol(String defaultUsername, char[] defaultPassword) {
return HttpConfiguration
.builder()
.https(isHttps())
.port(getOrDeducePortNumber())
.username(username == null ? defaultUsername : username)
.password(username == null ? defaultPassword : password)
.timeout(timeout)
.build();
public IConfiguration toProtocol(String defaultUsername, char[] defaultPassword)
throws InvalidConfigurationException {
final ObjectNode configuration = JsonNodeFactory.instance.objectNode();
configuration.set("https", BooleanNode.valueOf(isHttps()));
configuration.set("username", new TextNode(username == null ? defaultUsername : username));
configuration.set("password", new TextNode(username == null ? defaultPassword.toString() : password.toString()));
configuration.set("port", new IntNode(getOrDeducePortNumber()));
configuration.set("timeout", new TextNode(timeout));

return CliExtensionManager
.getExtensionManagerSingleton()
.buildConfigurationFromJsonNode("http", configuration, value -> value)
.orElseThrow();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.sentrysoftware.metricshub.agent.extension;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.sentrysoftware.metricshub.engine.common.exception.InvalidConfigurationException;
import org.sentrysoftware.metricshub.engine.common.helpers.StringHelper;
import org.sentrysoftware.metricshub.engine.configuration.IConfiguration;
import org.sentrysoftware.metricshub.engine.deserialization.TimeDeserializer;

/**
* The HttpConfiguration class represents the configuration for HTTP connections in the MetricsHub engine.
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class HttpTestConfiguration implements IConfiguration {

@Default
private final Boolean https = true;

@Default
private final Integer port = 443;

@Default
@JsonDeserialize(using = TimeDeserializer.class)
private final Long timeout = 120L;

private String username;
private char[] password;

@Override
public String toString() {
return String.format(
"%s/%d%s",
Boolean.TRUE.equals(https) ? "HTTPS" : "HTTP",
port,
username != null ? " as " + username : ""
);
}

@Override
public void validateConfiguration(String resourceKey) throws InvalidConfigurationException {
StringHelper.validateConfigurationAttribute(
timeout,
attr -> attr == null || attr < 0L,
() ->
String.format(
"Resource %s - Timeout value is invalid for protocol %s." +
" Timeout value returned: %s. This resource will not be monitored. Please verify the configured timeout value.",
resourceKey,
"HTTP",
timeout
)
);

StringHelper.validateConfigurationAttribute(
port,
attr -> attr == null || attr < 1 || attr > 65535,
() ->
String.format(
"Resource %s - Invalid port configured for protocol %s. Port value returned: %s." +
" This resource will not be monitored. Please verify the configured port value.",
resourceKey,
"HTTP",
port
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.sentrysoftware.metricshub.agent.extension;

/*-
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
* MetricsHub HTTP Extension
* ჻჻჻჻჻჻
* 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.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
import lombok.extern.slf4j.Slf4j;
import org.sentrysoftware.metricshub.engine.common.exception.InvalidConfigurationException;
import org.sentrysoftware.metricshub.engine.configuration.IConfiguration;
import org.sentrysoftware.metricshub.engine.connector.model.identity.criterion.Criterion;
import org.sentrysoftware.metricshub.engine.connector.model.identity.criterion.HttpCriterion;
import org.sentrysoftware.metricshub.engine.connector.model.monitor.task.source.HttpSource;
import org.sentrysoftware.metricshub.engine.connector.model.monitor.task.source.Source;
import org.sentrysoftware.metricshub.engine.extension.IProtocolExtension;
import org.sentrysoftware.metricshub.engine.strategy.detection.CriterionTestResult;
import org.sentrysoftware.metricshub.engine.strategy.source.SourceTable;
import org.sentrysoftware.metricshub.engine.telemetry.TelemetryManager;

/**
* This class implements the {@link IProtocolExtension} contract, reports the supported features,
* processes HTTP sources and criteria.
*/
@Slf4j
public class HttpTestExtension implements IProtocolExtension {

/**
* Protocol up status value '1.0'
*/
public static final Double UP = 1.0;

/**
* Protocol down status value '0.0'
*/
public static final Double DOWN = 0.0;

/**
* Up metric name format that will be saved by the metric factory
*/
public static final String HTTP_UP_METRIC = "metricshub.host.up{protocol=\"http\"}";

@Override
public boolean isValidConfiguration(IConfiguration configuration) {
return configuration instanceof HttpTestConfiguration;
}

@Override
public Set<Class<? extends Source>> getSupportedSources() {
return Set.of(HttpSource.class);
}

@Override
public Map<Class<? extends IConfiguration>, Set<Class<? extends Source>>> getConfigurationToSourceMapping() {
return Map.of(HttpTestConfiguration.class, Set.of(HttpSource.class));
}

@Override
public Set<Class<? extends Criterion>> getSupportedCriteria() {
return Set.of(HttpCriterion.class);
}

@Override
public void checkProtocol(TelemetryManager telemetryManager, Long collectTime) {}

@Override
public SourceTable processSource(Source source, String connectorId, TelemetryManager telemetryManager) {
return SourceTable.empty();
}

@Override
public CriterionTestResult processCriterion(
Criterion criterion,
String connectorId,
TelemetryManager telemetryManager
) {
return CriterionTestResult.empty();
}

@Override
public boolean isSupportedConfigurationType(String configurationType) {
return "http".equalsIgnoreCase(configurationType);
}

@Override
public IConfiguration buildConfiguration(String configurationType, JsonNode jsonNode, UnaryOperator<char[]> decrypt)
throws InvalidConfigurationException {
try {
final HttpTestConfiguration httpConfiguration = newObjectMapper()
.treeToValue(jsonNode, HttpTestConfiguration.class);

if (decrypt != null) {
// Decrypt the community
final char[] passwordDecypted = decrypt.apply(httpConfiguration.getPassword());
httpConfiguration.setPassword(passwordDecypted);
}

return httpConfiguration;
} catch (Exception e) {
final String errorMessage = String.format(
"Error while reading HTTP Configuration: %s. Error: %s",
jsonNode,
e.getMessage()
);
log.error(errorMessage);
log.debug("Error while reading HTTP Configuration: {}. Stack trace:", jsonNode, e);
throw new InvalidConfigurationException(errorMessage, e);
}
}

/**
* Creates and configures a new instance of the Jackson ObjectMapper for handling YAML data.
*
* @return A configured ObjectMapper instance.
*/
public static JsonMapper newObjectMapper() {
return JsonMapper
.builder(new YAMLFactory())
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.enable(SerializationFeature.INDENT_OUTPUT)
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false)
.build();
}
}
Loading

0 comments on commit 0db2850

Please sign in to comment.