diff --git a/jmx-scraper/README.md b/jmx-scraper/README.md index b57c508c1..83f5d88a0 100644 --- a/jmx-scraper/README.md +++ b/jmx-scraper/README.md @@ -21,20 +21,70 @@ Minimal configuration required Configuration can be provided through: - command line arguments: - `java -jar scraper.jar --config otel.jmx.service.url=service:jmx:rmi:///jndi/rmi://tomcat:9010/jmxrmi otel.jmx.target.system=tomcat`. + `java -jar scraper.jar -config otel.jmx.service.url=service:jmx:rmi:///jndi/rmi://tomcat:9010/jmxrmi otel.jmx.target.system=tomcat`. - command line arguments JVM system properties: `java -Dotel.jmx.service.url=service:jmx:rmi:///jndi/rmi://tomcat:9010/jmxrmi -Dotel.jmx.target.system=tomcat -jar scraper.jar`. - java properties file: `java -jar scraper.jar -config config.properties`. - stdin: `java -jar scraper.jar -config -` where `otel.jmx.target.system=tomcat` and `otel.jmx.service.url=service:jmx:rmi:///jndi/rmi://tomcat:9010/jmxrmi` is written to stdin. +- environment variables: `OTEL_JMX_TARGET_SYSTEM=tomcat OTEL_JMX_SERVICE_URL=service:jmx:rmi:///jndi/rmi://tomcat:9010/jmxrmi java -jar scraper.jar` -TODO: update this once autoconfiguration is supported +SDK auto-configuration is being used, so all the configuration options can be set using the java +properties syntax or the corresponding environment variables. -### Configuration reference +For example the `otel.jmx.service.url` option can be set with the `OTEL_JMX_SERVICE_URL` environment variable. -TODO +## Configuration reference -### Extra libraries in classpath +| config option | description | +|-----------------------------------|----------------------------------------------------------------------------------------------| +| `otel.jmx.service.url` | mandatory JMX URL to connect to the remote JVM | +| `otel.jmx.target.system` | comma-separated list of systems to monitor, mandatory unless a custom configuration is used | +| `otel.jmx.custom.scraping.config` | path to a custom YAML metrics definition, mandatory when `otel.jmx.target.system` is not set | +| `otel.jmx.username` | user name for JMX connection, mandatory when JMX authentication is enabled on target JVM | +| `otel.jmx.password` | password for JMX connection, mandatory when JMX authentication is enabled on target JVM | + +Supported values for `otel.jmx.target.system`: + +| `otel.jmx.target.system` | description | +|--------------------------|-----------------------| +| `activemq` | Apache ActiveMQ | +| `cassandra` | Apache Cassandra | +| `hbase` | Apache HBase | +| `hadoop` | Apache Hadoop | +| `jetty` | Eclipse Jetty | +| `jvm` | JVM runtime metrics | +| `kafka` | Apache Kafka | +| `kafka-consumer` | Apache Kafka consumer | +| `kafka-producer` | Apache Kafka producer | +| `solr` | Apache Solr | +| `tomcat` | Apache Tomcat | +| `wildfly` | Wildfly | + +The following SDK configuration options are also relevant + +| config option | default value | description | +|-------------------------------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.metric.export.interval` | `1m` (1 minute) | metric export interval, also controls the JMX sampling interval | +| `otel.metrics.exporter` | `otlp` | comma-separated list of metrics exporters supported values are `otlp` and `logging`, additional values might be provided through extra libraries in the classpath | + +In addition to OpenTelemetry configuration, the following Java system properties can be provided +through the command-line arguments, properties file or stdin and will be propagated to the JVM system properties: + +- `javax.net.ssl.keyStore` +- `javax.net.ssl.keyStorePassword` +- `javax.net.ssl.trustStore` +- `javax.net.ssl.trustStorePassword` + +Those JVM system properties can't be set through individual environment variables, but they can still +be set through the standard `JAVA_TOOL_OPTIONS` environment variable using the `-D` prefix. + +## Troubleshooting + +In order to investigate when and what metrics are being captured and sent, setting the `otel.metrics.exporter` +configuration option to include `logging` exporter provides log messages when metrics are being exported. + +## Extra libraries in classpath By default, only the RMI JMX connector is provided by the JVM, so it might be required to add extra libraries in the classpath when connecting to remote JVMs that are not directly accessible with RMI. @@ -45,7 +95,7 @@ needs to be used to support `otel.jmx.service.url` = `service:jmx:remote+http:// When doing so, the `java -jar` command canĀ“t be used, we have to provide the classpath with `-cp`/`--class-path`/`-classpath` option and provide the main class file name: -``` +```bash java -cp scraper.jar:jboss-client.jar io.opentelemetry.contrib.jmxscraper.JmxScraper ``` diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java index cbb06a784..455b53c73 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java @@ -24,7 +24,6 @@ public class JmxScraperContainer extends GenericContainer { private final String endpoint; private final Set targetSystems; private String serviceUrl; - private int intervalMillis; private final Set customYamlFiles; private String user; private String password; @@ -44,7 +43,6 @@ public JmxScraperContainer(String otlpEndpoint, String baseImage) { this.endpoint = otlpEndpoint; this.targetSystems = new HashSet<>(); this.customYamlFiles = new HashSet<>(); - this.intervalMillis = 1000; this.extraJars = new ArrayList<>(); } @@ -54,12 +52,6 @@ public JmxScraperContainer withTargetSystem(String targetSystem) { return this; } - @CanIgnoreReturnValue - public JmxScraperContainer withIntervalMillis(int intervalMillis) { - this.intervalMillis = intervalMillis; - return this; - } - @CanIgnoreReturnValue public JmxScraperContainer withRmiServiceUrl(String host, int port) { // TODO: adding a way to provide 'host:port' syntax would make this easier for end users @@ -132,7 +124,8 @@ public void start() { throw new IllegalStateException("Missing service URL"); } arguments.add("-Dotel.jmx.service.url=" + serviceUrl); - arguments.add("-Dotel.jmx.interval.milliseconds=" + intervalMillis); + // always use a very short export interval for testing + arguments.add("-Dotel.metric.export.interval=1s"); if (user != null) { arguments.add("-Dotel.jmx.username=" + user); diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java deleted file mode 100644 index f006269cb..000000000 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/ArgumentsParsingException.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.jmxscraper; - -public class ArgumentsParsingException extends Exception { - private static final long serialVersionUID = 0L; - - public ArgumentsParsingException(String msg) { - super(msg); - } -} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/InvalidArgumentException.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/InvalidArgumentException.java new file mode 100644 index 000000000..bdfb93272 --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/InvalidArgumentException.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +/** + * Exception indicating something is wrong with the provided arguments or reading the configuration + * from them + */ +public class InvalidArgumentException extends Exception { + + private static final long serialVersionUID = 0L; + + public InvalidArgumentException(String msg) { + super(msg); + } + + public InvalidArgumentException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java index fe82c698d..29156c67c 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -6,11 +6,14 @@ package io.opentelemetry.contrib.jmxscraper; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.contrib.jmxscraper.config.ConfigurationException; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; +import io.opentelemetry.contrib.jmxscraper.config.PropertiesCustomizer; +import io.opentelemetry.contrib.jmxscraper.config.PropertiesSupplier; import io.opentelemetry.instrumentation.jmx.engine.JmxMetricInsight; import io.opentelemetry.instrumentation.jmx.engine.MetricConfiguration; import io.opentelemetry.instrumentation.jmx.yaml.RuleParser; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; @@ -19,9 +22,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Properties; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; import java.util.logging.Logger; import javax.management.MBeanServerConnection; import javax.management.remote.JMXConnector; @@ -30,8 +35,6 @@ public class JmxScraper { private static final Logger logger = Logger.getLogger(JmxScraper.class.getName()); private static final String CONFIG_ARG = "-config"; - private static final String OTEL_AUTOCONFIGURE = "otel.java.global-autoconfigure.enabled"; - private final JmxConnectorBuilder client; private final JmxMetricInsight service; private final JmxScraperConfig config; @@ -43,58 +46,77 @@ public class JmxScraper { * * @param args - must be of the form "-config {jmx_config_path,'-'}" */ - @SuppressWarnings({"SystemOut", "SystemExitOutsideMain"}) + @SuppressWarnings("SystemExitOutsideMain") public static void main(String[] args) { - // enable SDK auto-configure if not explicitly set by user - // TODO: refactor this to use AutoConfiguredOpenTelemetrySdk - if (System.getProperty(OTEL_AUTOCONFIGURE) == null) { - System.setProperty(OTEL_AUTOCONFIGURE, "true"); - } + // set log format + System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tF %1$tT %4$s %5$s%n"); try { - JmxScraperConfig config = - JmxScraperConfig.fromProperties(parseArgs(Arrays.asList(args)), System.getProperties()); - // propagate effective user-provided configuration to JVM system properties - // this also enables SDK auto-configuration to use those properties - config.propagateSystemProperties(); + Properties argsConfig = parseArgs(Arrays.asList(args)); + propagateToSystemProperties(argsConfig); + + // auto-configure and register SDK + PropertiesCustomizer configCustomizer = new PropertiesCustomizer(); + AutoConfiguredOpenTelemetrySdk.builder() + .addPropertiesSupplier(new PropertiesSupplier(argsConfig)) + .addPropertiesCustomizer(configCustomizer) + .setResultAsGlobal() + .build(); + + JmxScraperConfig scraperConfig = configCustomizer.getScraperConfig(); + + long exportSeconds = scraperConfig.getSamplingInterval().toMillis() / 1000; + logger.log(Level.INFO, "metrics export interval (seconds) = " + exportSeconds); JmxMetricInsight service = JmxMetricInsight.createService( - GlobalOpenTelemetry.get(), config.getIntervalMilliseconds()); - JmxConnectorBuilder connectorBuilder = JmxConnectorBuilder.createNew(config.getServiceUrl()); + GlobalOpenTelemetry.get(), scraperConfig.getSamplingInterval().toMillis()); + JmxConnectorBuilder connectorBuilder = + JmxConnectorBuilder.createNew(scraperConfig.getServiceUrl()); - Optional.ofNullable(config.getUsername()).ifPresent(connectorBuilder::withUser); - Optional.ofNullable(config.getPassword()).ifPresent(connectorBuilder::withPassword); + Optional.ofNullable(scraperConfig.getUsername()).ifPresent(connectorBuilder::withUser); + Optional.ofNullable(scraperConfig.getPassword()).ifPresent(connectorBuilder::withPassword); - JmxScraper jmxScraper = new JmxScraper(connectorBuilder, service, config); + JmxScraper jmxScraper = new JmxScraper(connectorBuilder, service, scraperConfig); jmxScraper.start(); - - } catch (ArgumentsParsingException e) { - System.err.println("ERROR: " + e.getMessage()); - System.err.println( + } catch (ConfigurationException e) { + logger.log(Level.SEVERE, "invalid configuration ", e); + System.exit(1); + } catch (InvalidArgumentException e) { + logger.log(Level.SEVERE, "invalid configuration provided through arguments", e); + logger.info( "Usage: java -jar " + "-config "); System.exit(1); - } catch (ConfigurationException e) { - System.err.println(e.getMessage()); - System.exit(1); } catch (IOException e) { - System.err.println("Unable to connect " + e.getMessage()); + logger.log(Level.SEVERE, "Unable to connect ", e); System.exit(2); } catch (RuntimeException e) { - e.printStackTrace(System.err); + logger.log(Level.SEVERE, e.getMessage(), e); System.exit(3); } } + // package private for testing + static void propagateToSystemProperties(Properties properties) { + for (Map.Entry entry : properties.entrySet()) { + String key = entry.getKey().toString(); + String value = entry.getValue().toString(); + if (key.startsWith("javax.net.ssl.keyStore") || key.startsWith("javax.net.ssl.trustStore")) { + if (System.getProperty(key) == null) { + System.setProperty(key, value); + } + } + } + } + /** * Create {@link Properties} from command line options * * @param args application commandline arguments */ - static Properties parseArgs(List args) - throws ArgumentsParsingException, ConfigurationException { + static Properties parseArgs(List args) throws InvalidArgumentException { if (args.isEmpty()) { // empty properties from stdin or external file @@ -102,10 +124,10 @@ static Properties parseArgs(List args) return new Properties(); } if (args.size() != 2) { - throw new ArgumentsParsingException("Exactly two arguments expected, got " + args.size()); + throw new InvalidArgumentException("Exactly two arguments expected, got " + args.size()); } if (!args.get(0).equalsIgnoreCase(CONFIG_ARG)) { - throw new ArgumentsParsingException("Unexpected first argument must be '" + CONFIG_ARG + "'"); + throw new InvalidArgumentException("Unexpected first argument must be '" + CONFIG_ARG + "'"); } String path = args.get(1); @@ -116,27 +138,30 @@ static Properties parseArgs(List args) } } - private static Properties loadPropertiesFromStdin() throws ConfigurationException { + private static Properties loadPropertiesFromStdin() throws InvalidArgumentException { Properties properties = new Properties(); try (InputStream is = new DataInputStream(System.in)) { properties.load(is); return properties; } catch (IOException e) { - throw new ConfigurationException("Failed to read config properties from stdin", e); + // an IO error is very unlikely here + throw new InvalidArgumentException("Failed to read config properties from stdin", e); } } - private static Properties loadPropertiesFromPath(String path) throws ConfigurationException { + private static Properties loadPropertiesFromPath(String path) throws InvalidArgumentException { Properties properties = new Properties(); try (InputStream is = Files.newInputStream(Paths.get(path))) { properties.load(is); return properties; } catch (IOException e) { - throw new ConfigurationException("Failed to read config properties file: '" + path + "'", e); + throw new InvalidArgumentException( + "Failed to read config properties file: '" + path + "'", e); } } - JmxScraper(JmxConnectorBuilder client, JmxMetricInsight service, JmxScraperConfig config) { + private JmxScraper( + JmxConnectorBuilder client, JmxMetricInsight service, JmxScraperConfig config) { this.client = client; this.service = service; this.config = config; diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/ConfigurationException.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/ConfigurationException.java deleted file mode 100644 index 76c69998a..000000000 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/ConfigurationException.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.jmxscraper.config; - -public class ConfigurationException extends Exception { - private static final long serialVersionUID = 0L; - - public ConfigurationException(String message, Throwable cause) { - super(message, cause); - } - - public ConfigurationException(String message) { - super(message); - } -} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfig.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfig.java index 4e04fe145..339fc33c2 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfig.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfig.java @@ -5,38 +5,39 @@ package io.opentelemetry.contrib.jmxscraper.config; -import static io.opentelemetry.contrib.jmxscraper.internal.StringUtils.isBlank; - +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import java.time.Duration; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; import java.util.Set; -import java.util.stream.Collectors; +import javax.annotation.Nullable; /** This class keeps application settings */ public class JmxScraperConfig { - static final String SERVICE_URL = "otel.jmx.service.url"; - static final String CUSTOM_JMX_SCRAPING_CONFIG = "otel.jmx.custom.scraping.config"; - static final String TARGET_SYSTEM = "otel.jmx.target.system"; - static final String INTERVAL_MILLISECONDS = "otel.jmx.interval.milliseconds"; - static final String METRICS_EXPORTER_TYPE = "otel.metrics.exporter"; - static final String EXPORTER_INTERVAL = "otel.metric.export.interval"; - static final String REGISTRY_SSL = "otel.jmx.remote.registry.ssl"; + // metric sdk configuration + static final String METRIC_EXPORT_INTERVAL = "otel.metric.export.interval"; + + // not documented on purpose as using the SDK 'otel.metric.export.interval' is preferred + static final String JMX_INTERVAL_LEGACY = "otel.jmx.interval.milliseconds"; - static final String OTLP_ENDPOINT = "otel.exporter.otlp.endpoint"; + static final String JMX_SERVICE_URL = "otel.jmx.service.url"; + // TODO: align with instrumentation 'otel.jmx.config' + support list of values + static final String JMX_CUSTOM_CONFIG = "otel.jmx.custom.scraping.config"; + static final String JMX_TARGET_SYSTEM = "otel.jmx.target.system"; static final String JMX_USERNAME = "otel.jmx.username"; static final String JMX_PASSWORD = "otel.jmx.password"; + + // TODO: document those when they will be supported + static final String JMX_REGISTRY_SSL = "otel.jmx.remote.registry.ssl"; static final String JMX_REMOTE_PROFILE = "otel.jmx.remote.profile"; static final String JMX_REALM = "otel.jmx.realm"; - static final String OTLP_METRICS_EXPORTER = "otlp"; - - static final List AVAILABLE_TARGET_SYSTEMS = + private static final List AVAILABLE_TARGET_SYSTEMS = Collections.unmodifiableList( Arrays.asList( "activemq", @@ -53,28 +54,29 @@ public class JmxScraperConfig { "wildfly")); private String serviceUrl = ""; - private String customJmxScrapingConfigPath = ""; + + @Nullable private String customJmxScrapingConfigPath; + private Set targetSystems = Collections.emptySet(); - private int intervalMilliseconds; // TODO only used to set 'otel.metric.export.interval' from SDK - private String metricsExporterType = ""; // TODO only used to default to 'logging' if not set - private String otlpExporterEndpoint = ""; // TODO not really needed here as handled by SDK - private String username = ""; - private String password = ""; - private String realm = ""; - private String remoteProfile = ""; - private boolean registrySsl; - /** Combined properties kept for initializing system properties */ - private final Properties properties; + private Duration samplingInterval = Duration.ofMinutes(1); - private JmxScraperConfig(Properties properties) { - this.properties = properties; - } + @Nullable private String username; + + @Nullable private String password; + + @Nullable private String realm; + + @Nullable private String remoteProfile; + private boolean registrySsl; + + private JmxScraperConfig() {} public String getServiceUrl() { return serviceUrl; } + @Nullable public String getCustomJmxScrapingConfigPath() { return customJmxScrapingConfigPath; } @@ -83,30 +85,26 @@ public Set getTargetSystems() { return targetSystems; } - public int getIntervalMilliseconds() { - return intervalMilliseconds; - } - - public String getMetricsExporterType() { - return metricsExporterType; - } - - public String getOtlpExporterEndpoint() { - return otlpExporterEndpoint; + public Duration getSamplingInterval() { + return samplingInterval; } + @Nullable public String getUsername() { return username; } + @Nullable public String getPassword() { return password; } + @Nullable public String getRealm() { return realm; } + @Nullable public String getRemoteProfile() { return remoteProfile; } @@ -116,133 +114,50 @@ public boolean isRegistrySsl() { } /** - * Builds scraper configuration from user and system properties + * Builds JMX scraper configuration from auto-configuration * - * @param userProperties user-provided configuration - * @param systemProperties system properties through '-Dxxx' JVM arguments + * @param config auto-configuration properties * @return JMX scraper configuration - * @throws ConfigurationException if there is any configuration error - */ - public static JmxScraperConfig fromProperties( - Properties userProperties, Properties systemProperties) throws ConfigurationException { - - Properties properties = new Properties(); - properties.putAll(userProperties); - - // command line takes precedence so replace any that were specified via config file properties - properties.putAll(systemProperties); - - JmxScraperConfig config = new JmxScraperConfig(properties); - - config.serviceUrl = properties.getProperty(SERVICE_URL); - config.customJmxScrapingConfigPath = properties.getProperty(CUSTOM_JMX_SCRAPING_CONFIG); - String targetSystem = - properties.getProperty(TARGET_SYSTEM, "").toLowerCase(Locale.ENGLISH).trim(); - - List targets = - Arrays.asList(isBlank(targetSystem) ? new String[0] : targetSystem.split(",")); - config.targetSystems = targets.stream().map(String::trim).collect(Collectors.toSet()); - - int interval = getProperty(properties, INTERVAL_MILLISECONDS, 0); - config.intervalMilliseconds = (interval == 0 ? 10000 : interval); - // configure SDK metric exporter interval from jmx metric interval - getAndSetPropertyIfUndefined(properties, EXPORTER_INTERVAL, config.intervalMilliseconds); - - config.metricsExporterType = - getAndSetPropertyIfUndefined(properties, METRICS_EXPORTER_TYPE, "logging"); - if (OTLP_METRICS_EXPORTER.equalsIgnoreCase(config.metricsExporterType)) { - config.otlpExporterEndpoint = - getAndSetPropertyIfUndefined(properties, OTLP_ENDPOINT, "http://localhost:4318"); - } - config.username = properties.getProperty(JMX_USERNAME); - config.password = properties.getProperty(JMX_PASSWORD); - - config.remoteProfile = properties.getProperty(JMX_REMOTE_PROFILE); - config.realm = properties.getProperty(JMX_REALM); - - config.registrySsl = Boolean.parseBoolean(properties.getProperty(REGISTRY_SSL)); - - validateConfig(config); - return config; - } - - /** - * Sets system properties from effective configuration, must be called once and early before any - * OTel SDK or SSL/TLS stack initialization. This allows to override JVM system properties using - * user-provided configuration and also to set standard OTel SDK configuration. - */ - public void propagateSystemProperties() { - for (Map.Entry entry : properties.entrySet()) { - - String key = entry.getKey().toString(); - String value = entry.getValue().toString(); - if (key.startsWith("otel.") - || key.startsWith("javax.net.ssl.keyStore") - || key.startsWith("javax.net.ssl.trustStore")) { - System.setProperty(key, value); - } - } - } - - private static int getProperty(Properties properties, String key, int defaultValue) - throws ConfigurationException { - String propVal = properties.getProperty(key); - if (propVal == null) { - return defaultValue; - } - try { - return Integer.parseInt(propVal); - } catch (NumberFormatException e) { - throw new ConfigurationException("Failed to parse " + key, e); - } - } - - /** - * Similar to getProperty(key, defaultValue) but sets the property to default if not in object. */ - private static String getAndSetPropertyIfUndefined( - Properties properties, String key, String defaultValue) { - String propVal = properties.getProperty(key, defaultValue); - if (propVal.equals(defaultValue)) { - properties.setProperty(key, defaultValue); + public static JmxScraperConfig fromConfig(ConfigProperties config) { + JmxScraperConfig scraperConfig = new JmxScraperConfig(); + + Duration exportInterval = config.getDuration(METRIC_EXPORT_INTERVAL); + if (exportInterval == null || exportInterval.isNegative() || exportInterval.isZero()) { + // if not explicitly set, use default value of 1 minute as defined in specification + scraperConfig.samplingInterval = Duration.ofMinutes(1); + } else { + scraperConfig.samplingInterval = exportInterval; } - return propVal; - } - private static int getAndSetPropertyIfUndefined( - Properties properties, String key, int defaultValue) throws ConfigurationException { - int propVal = getProperty(properties, key, defaultValue); - if (propVal == defaultValue) { - properties.setProperty(key, String.valueOf(defaultValue)); + String serviceUrl = config.getString(JMX_SERVICE_URL); + if (serviceUrl == null) { + throw new ConfigurationException("missing mandatory " + JMX_SERVICE_URL); } - return propVal; - } + scraperConfig.serviceUrl = serviceUrl; - /** Will determine if parsed config is complete, setting any applicable values and defaults. */ - private static void validateConfig(JmxScraperConfig config) throws ConfigurationException { - if (isBlank(config.serviceUrl)) { - throw new ConfigurationException(SERVICE_URL + " must be specified."); - } - - if (isBlank(config.customJmxScrapingConfigPath) && config.targetSystems.isEmpty()) { + // TODO: we could support multiple values + String customConfig = config.getString(JMX_CUSTOM_CONFIG, ""); + List targetSystem = config.getList(JMX_TARGET_SYSTEM); + if (targetSystem.isEmpty() && customConfig.isEmpty()) { throw new ConfigurationException( - CUSTOM_JMX_SCRAPING_CONFIG + " or " + TARGET_SYSTEM + " must be specified."); - } - - if (!config.targetSystems.isEmpty() - && !AVAILABLE_TARGET_SYSTEMS.containsAll(config.targetSystems)) { - throw new ConfigurationException( - String.format( - "%s must specify targets from %s", config.targetSystems, AVAILABLE_TARGET_SYSTEMS)); - } - - if (OTLP_METRICS_EXPORTER.equalsIgnoreCase(config.metricsExporterType) - && isBlank(config.otlpExporterEndpoint)) { - throw new ConfigurationException(OTLP_ENDPOINT + " must be specified for otlp format."); - } - - if (config.intervalMilliseconds < 0) { - throw new ConfigurationException(INTERVAL_MILLISECONDS + " must be positive."); + "at least one of '" + JMX_TARGET_SYSTEM + "' or '" + JMX_CUSTOM_CONFIG + "' must be set"); } + targetSystem.forEach( + s -> { + if (!AVAILABLE_TARGET_SYSTEMS.contains(s)) { + throw new ConfigurationException("unsupported target system: '" + s + "'"); + } + }); + scraperConfig.customJmxScrapingConfigPath = customConfig; + scraperConfig.targetSystems = new HashSet<>(targetSystem); + + scraperConfig.username = config.getString("otel.jmx.username"); + scraperConfig.password = config.getString("otel.jmx.password"); + scraperConfig.remoteProfile = config.getString("otel.jmx.remote.profile"); + scraperConfig.realm = config.getString("otel.jmx.realm"); + scraperConfig.registrySsl = config.getBoolean("otel.jmx.remote.registry.ssl", false); + + return scraperConfig; } } diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesCustomizer.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesCustomizer.java new file mode 100644 index 000000000..9d6812146 --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesCustomizer.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.config; + +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_INTERVAL_LEGACY; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.METRIC_EXPORT_INTERVAL; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** Customizer of default SDK configuration and provider of effective scraper config */ +public class PropertiesCustomizer implements Function> { + + private static final Logger logger = Logger.getLogger(PropertiesCustomizer.class.getName()); + + private static final String METRICS_EXPORTER = "otel.metrics.exporter"; + + @Nullable private JmxScraperConfig scraperConfig; + + @Override + public Map apply(ConfigProperties config) { + Map result = new HashMap<>(); + + // set default exporter to 'otlp' to be consistent with SDK + if (config.getList(METRICS_EXPORTER).isEmpty()) { + result.put(METRICS_EXPORTER, "otlp"); + } + + // providing compatibility with the existing 'otel.jmx.interval.milliseconds' config option + long intervalLegacy = config.getLong(JMX_INTERVAL_LEGACY, -1); + if (config.getDuration(METRIC_EXPORT_INTERVAL) == null && intervalLegacy >= 0) { + logger.warning( + METRIC_EXPORT_INTERVAL + + " deprecated option is used, replacing with '" + + METRIC_EXPORT_INTERVAL + + "' metric sdk configuration is recommended"); + result.put(METRIC_EXPORT_INTERVAL, intervalLegacy + "ms"); + } + + scraperConfig = JmxScraperConfig.fromConfig(config); + return result; + } + + /** + * Get scraper configuration from the previous call to {@link #apply(ConfigProperties)} + * + * @return JMX scraper configuration + * @throws IllegalStateException when {@link #apply(ConfigProperties)} hasn't been called first + */ + public JmxScraperConfig getScraperConfig() { + if (scraperConfig == null) { + throw new IllegalStateException("apply() must be called before getConfig()"); + } + return scraperConfig; + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesSupplier.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesSupplier.java new file mode 100644 index 000000000..071e2b8fa --- /dev/null +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesSupplier.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.config; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.function.Supplier; + +/** Configuration supplier for java properties */ +public class PropertiesSupplier implements Supplier> { + + private final Properties properties; + + public PropertiesSupplier(Properties properties) { + this.properties = properties; + } + + @Override + public Map get() { + Map map = new HashMap<>(); + properties.forEach((k, v) -> map.put((String) k, (String) v)); + return map; + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/internal/StringUtils.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/internal/StringUtils.java deleted file mode 100644 index fa12d24b4..000000000 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/internal/StringUtils.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.jmxscraper.internal; - -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time.
This is a utility class implementing miscellaneous String operations. - */ -public final class StringUtils { - private StringUtils() {} - - /** - * Determines if a String is null or without non-whitespace chars. - * - * @param s - {@link String} to evaluate - * @return - if s is null or without non-whitespace chars. - */ - public static boolean isBlank(@Nullable String s) { - return (s == null) || s.trim().isEmpty(); - } -} diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java index 1dfe2717d..1532715d9 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/JmxScraperTest.java @@ -8,8 +8,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import io.opentelemetry.contrib.jmxscraper.config.ConfigurationException; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; +import io.opentelemetry.contrib.jmxscraper.config.TestUtil; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; @@ -17,47 +17,53 @@ import java.util.List; import java.util.Properties; import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ClearSystemProperty; class JmxScraperTest { + @Test void shouldThrowExceptionWhenInvalidCommandLineArgsProvided() { - // Given - List emptyArgs = Collections.singletonList("-nonExistentOption"); + testInvalidArguments("-nonExistentOption"); + testInvalidArguments("-potato", "-config"); + testInvalidArguments("-config", "path", "-nonExistentOption"); + } - // When and Then - assertThatThrownBy(() -> JmxScraper.parseArgs(emptyArgs)) - .isInstanceOf(ArgumentsParsingException.class); + @Test + void emptyArgumentsAllowed() throws InvalidArgumentException { + assertThat(JmxScraper.parseArgs(Collections.emptyList())) + .describedAs("empty arguments allowed to use JVM properties") + .isEmpty(); } @Test - void shouldThrowExceptionWhenTooManyCommandLineArgsProvided() { - // Given - List args = Arrays.asList("-config", "path", "-nonExistentOption"); + void shouldThrowExceptionWhenMissingProperties() { + testInvalidArguments("-config", "missing.properties"); + } - // When and Then - assertThatThrownBy(() -> JmxScraper.parseArgs(args)) - .isInstanceOf(ArgumentsParsingException.class); + private static void testInvalidArguments(String... args) { + assertThatThrownBy(() -> JmxScraper.parseArgs(Arrays.asList(args))) + .isInstanceOf(InvalidArgumentException.class); } @Test - void shouldCreateConfig_propertiesLoadedFromFile() - throws ConfigurationException, ArgumentsParsingException { + void shouldCreateConfig_propertiesLoadedFromFile() throws InvalidArgumentException { // Given String filePath = ClassLoader.getSystemClassLoader().getResource("validConfig.properties").getPath(); List args = Arrays.asList("-config", filePath); // When - JmxScraperConfig config = - JmxScraperConfig.fromProperties(JmxScraper.parseArgs(args), new Properties()); + Properties parsedConfig = JmxScraper.parseArgs(args); + JmxScraperConfig config = JmxScraperConfig.fromConfig(TestUtil.configProperties(parsedConfig)); // Then assertThat(config).isNotNull(); + assertThat(config.getServiceUrl()) + .isEqualTo("service:jmx:rmi:///jndi/rmi://myhost:12345/jmxrmi"); } @Test - void shouldCreateConfig_propertiesLoadedFromStdIn() - throws ConfigurationException, ArgumentsParsingException, IOException { + void shouldCreateConfig_propertiesLoadedFromStdIn() throws InvalidArgumentException, IOException { InputStream originalIn = System.in; try (InputStream stream = ClassLoader.getSystemClassLoader().getResourceAsStream("validConfig.properties")) { @@ -66,13 +72,58 @@ void shouldCreateConfig_propertiesLoadedFromStdIn() List args = Arrays.asList("-config", "-"); // When + Properties parsedConfig = JmxScraper.parseArgs(args); JmxScraperConfig config = - JmxScraperConfig.fromProperties(JmxScraper.parseArgs(args), new Properties()); + JmxScraperConfig.fromConfig(TestUtil.configProperties(parsedConfig)); // Then assertThat(config).isNotNull(); + assertThat(config.getServiceUrl()) + .isEqualTo("service:jmx:rmi:///jndi/rmi://myhost:12345/jmxrmi"); } finally { System.setIn(originalIn); } } + + @Test + @ClearSystemProperty(key = "javax.net.ssl.keyStore") + @ClearSystemProperty(key = "javax.net.ssl.keyStorePassword") + @ClearSystemProperty(key = "javax.net.ssl.keyStoreType") + @ClearSystemProperty(key = "javax.net.ssl.trustStore") + @ClearSystemProperty(key = "javax.net.ssl.trustStorePassword") + @ClearSystemProperty(key = "javax.net.ssl.trustStoreType") + void systemPropertiesPropagation() { + + assertThat(System.getProperty("javax.net.ssl.keyStore")) + .describedAs("keystore config should not be set") + .isNull(); + + Properties properties = new Properties(); + properties.setProperty("javax.net.ssl.keyStore", "/my/key/store"); + properties.setProperty("javax.net.ssl.keyStorePassword", "abc123"); + properties.setProperty("javax.net.ssl.keyStoreType", "JKS"); + properties.setProperty("javax.net.ssl.trustStore", "/my/trust/store"); + properties.setProperty("javax.net.ssl.trustStorePassword", "def456"); + properties.setProperty("javax.net.ssl.trustStoreType", "JKS"); + + properties.setProperty("must.be.ignored", "should not be propagated"); + + JmxScraper.propagateToSystemProperties(properties); + + assertThat(System.getProperty("javax.net.ssl.keyStore")).isEqualTo("/my/key/store"); + assertThat(System.getProperty("javax.net.ssl.keyStorePassword")).isEqualTo("abc123"); + assertThat(System.getProperty("javax.net.ssl.keyStoreType")).isEqualTo("JKS"); + assertThat(System.getProperty("javax.net.ssl.trustStore")).isEqualTo("/my/trust/store"); + assertThat(System.getProperty("javax.net.ssl.trustStorePassword")).isEqualTo("def456"); + assertThat(System.getProperty("javax.net.ssl.trustStoreType")).isEqualTo("JKS"); + + assertThat(System.getProperty("must.be.ignored")).isNull(); + + // when already set, current system properties have priority + properties.setProperty("javax.net.ssl.keyStore", "/another/key/store"); + JmxScraper.propagateToSystemProperties(properties); + assertThat(System.getProperty("javax.net.ssl.keyStore")) + .describedAs("already set system properties must be preserved") + .isEqualTo("/my/key/store"); + } } diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java index 4764ada6a..e5de3ebb6 100644 --- a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/JmxScraperConfigTest.java @@ -5,27 +5,23 @@ package io.opentelemetry.contrib.jmxscraper.config; -import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.CUSTOM_JMX_SCRAPING_CONFIG; -import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.INTERVAL_MILLISECONDS; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_CUSTOM_CONFIG; import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_PASSWORD; import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_REALM; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_REGISTRY_SSL; import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_REMOTE_PROFILE; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_SERVICE_URL; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_TARGET_SYSTEM; import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_USERNAME; -import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.METRICS_EXPORTER_TYPE; -import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.OTLP_ENDPOINT; -import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.REGISTRY_SSL; -import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.SERVICE_URL; -import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.TARGET_SYSTEM; -import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.fromProperties; +import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.fromConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import java.time.Duration; import java.util.Properties; -import java.util.stream.Stream; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junitpioneer.jupiter.ClearSystemProperty; class JmxScraperConfigTest { private static Properties validProperties; @@ -34,246 +30,95 @@ class JmxScraperConfigTest { static void setUp() { validProperties = new Properties(); validProperties.setProperty( - SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); - validProperties.setProperty(CUSTOM_JMX_SCRAPING_CONFIG, ""); - validProperties.setProperty(TARGET_SYSTEM, "tomcat, activemq"); - validProperties.setProperty(METRICS_EXPORTER_TYPE, "otlp"); - validProperties.setProperty(INTERVAL_MILLISECONDS, "1410"); - validProperties.setProperty(REGISTRY_SSL, "true"); - validProperties.setProperty(OTLP_ENDPOINT, "http://localhost:4317"); + JMX_SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + validProperties.setProperty(JMX_CUSTOM_CONFIG, "/path/to/config.yaml"); + validProperties.setProperty(JMX_TARGET_SYSTEM, "tomcat, activemq"); + validProperties.setProperty(JMX_REGISTRY_SSL, "true"); validProperties.setProperty(JMX_USERNAME, "some-user"); validProperties.setProperty(JMX_PASSWORD, "some-password"); validProperties.setProperty(JMX_REMOTE_PROFILE, "some-profile"); validProperties.setProperty(JMX_REALM, "some-realm"); - } - - @AfterEach - void afterEach() { - // make sure that no test leaked in global system properties - Stream.of(System.getProperties().keySet()) - .map(Object::toString) - .forEach( - key -> { - if (key.startsWith("otel.") || key.startsWith("javax.net.ssl.")) { - System.clearProperty(key); - } - }); + // otel sdk metric export interval + validProperties.setProperty("otel.metric.export.interval", "10s"); } @Test - void shouldCreateMinimalValidConfiguration() throws ConfigurationException { - // Given - Properties properties = new Properties(); - properties.setProperty(SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); - properties.setProperty(CUSTOM_JMX_SCRAPING_CONFIG, "/file.properties"); - + void shouldPassValidation() { // When - JmxScraperConfig config = fromProperties(properties, new Properties()); + JmxScraperConfig config = fromConfig(TestUtil.configProperties(validProperties)); // Then assertThat(config.getServiceUrl()) .isEqualTo("jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); - assertThat(config.getCustomJmxScrapingConfigPath()).isEqualTo("/file.properties"); - assertThat(config.getTargetSystems()).isEmpty(); - assertThat(config.getIntervalMilliseconds()).isEqualTo(10000); - assertThat(config.getMetricsExporterType()).isEqualTo("logging"); - assertThat(config.getOtlpExporterEndpoint()).isBlank(); - assertThat(config.getUsername()).isNull(); - assertThat(config.getPassword()).isNull(); - assertThat(config.getRemoteProfile()).isNull(); - assertThat(config.getRealm()).isNull(); - } - - @Test - void shouldCreateConfig_defaultOtlEndpoint() throws ConfigurationException { - // Given - Properties properties = new Properties(); - properties.setProperty(SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); - properties.setProperty(CUSTOM_JMX_SCRAPING_CONFIG, "/file.properties"); - properties.setProperty(METRICS_EXPORTER_TYPE, "otlp"); - - // When - JmxScraperConfig config = fromProperties(properties, new Properties()); - - // Then - assertThat(config.getMetricsExporterType()).isEqualTo("otlp"); - assertThat(config.getOtlpExporterEndpoint()).isEqualTo("http://localhost:4318"); - } - - @Test - @ClearSystemProperty(key = "javax.net.ssl.keyStore") - @ClearSystemProperty(key = "javax.net.ssl.keyStorePassword") - @ClearSystemProperty(key = "javax.net.ssl.keyStoreType") - @ClearSystemProperty(key = "javax.net.ssl.trustStore") - @ClearSystemProperty(key = "javax.net.ssl.trustStorePassword") - @ClearSystemProperty(key = "javax.net.ssl.trustStoreType") - void shouldUseValuesFromProperties() throws ConfigurationException { - // Given - // Properties to be propagated to system, properties - Properties properties = (Properties) validProperties.clone(); - properties.setProperty("javax.net.ssl.keyStore", "/my/key/store"); - properties.setProperty("javax.net.ssl.keyStorePassword", "abc123"); - properties.setProperty("javax.net.ssl.keyStoreType", "JKS"); - properties.setProperty("javax.net.ssl.trustStore", "/my/trust/store"); - properties.setProperty("javax.net.ssl.trustStorePassword", "def456"); - properties.setProperty("javax.net.ssl.trustStoreType", "JKS"); - - assertThat(System.getProperty("javax.net.ssl.keyStore")) - .describedAs("keystore config should not be set") - .isNull(); - - // When - JmxScraperConfig config = fromProperties(properties, new Properties()); - config.propagateSystemProperties(); - - // Then - assertThat(config.getServiceUrl()) - .isEqualTo("jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); - assertThat(config.getCustomJmxScrapingConfigPath()).isEqualTo(""); - assertThat(config.getTargetSystems()).containsOnly("tomcat", "activemq"); - assertThat(config.getIntervalMilliseconds()).isEqualTo(1410); - assertThat(config.getMetricsExporterType()).isEqualTo("otlp"); - assertThat(config.getOtlpExporterEndpoint()).isEqualTo("http://localhost:4317"); + assertThat(config.getCustomJmxScrapingConfigPath()).isEqualTo("/path/to/config.yaml"); + assertThat(config.getTargetSystems()).containsExactlyInAnyOrder("tomcat", "activemq"); + assertThat(config.getSamplingInterval()).isEqualTo(Duration.ofSeconds(10)); assertThat(config.getUsername()).isEqualTo("some-user"); assertThat(config.getPassword()).isEqualTo("some-password"); assertThat(config.getRemoteProfile()).isEqualTo("some-profile"); assertThat(config.getRealm()).isEqualTo("some-realm"); - assertThat(config.isRegistrySsl()).isTrue(); - - // These properties are set from the config file loading into JmxConfig - assertThat(System.getProperty("javax.net.ssl.keyStore")).isEqualTo("/my/key/store"); - assertThat(System.getProperty("javax.net.ssl.keyStorePassword")).isEqualTo("abc123"); - assertThat(System.getProperty("javax.net.ssl.keyStoreType")).isEqualTo("JKS"); - assertThat(System.getProperty("javax.net.ssl.trustStore")).isEqualTo("/my/trust/store"); - assertThat(System.getProperty("javax.net.ssl.trustStorePassword")).isEqualTo("def456"); - assertThat(System.getProperty("javax.net.ssl.trustStoreType")).isEqualTo("JKS"); } @Test - @ClearSystemProperty(key = "otel.jmx.service.url") - @ClearSystemProperty(key = "javax.net.ssl.keyStorePassword") - void shouldRetainPredefinedSystemProperties() throws ConfigurationException { + void shouldCreateMinimalValidConfiguration() { // Given - // user properties to be propagated to system properties - Properties properties = (Properties) validProperties.clone(); - properties.setProperty("javax.net.ssl.keyStorePassword", "abc123"); - - // system properties - Properties systemProperties = new Properties(); - systemProperties.put("otel.jmx.service.url", "originalServiceUrl"); - systemProperties.put("javax.net.ssl.keyStorePassword", "originalPassword"); + Properties properties = new Properties(); + properties.setProperty(JMX_SERVICE_URL, "jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + properties.setProperty(JMX_CUSTOM_CONFIG, "/file.properties"); // When - JmxScraperConfig config = fromProperties(properties, systemProperties); - // even when effective configuration is propagated to system properties original values are kept - // due to priority of system properties over user-provided ones. - config.propagateSystemProperties(); + JmxScraperConfig config = fromConfig(TestUtil.configProperties(properties)); // Then - assertThat(System.getProperty("otel.jmx.service.url")).isEqualTo("originalServiceUrl"); - assertThat(System.getProperty("javax.net.ssl.keyStorePassword")).isEqualTo("originalPassword"); + assertThat(config.getServiceUrl()) + .isEqualTo("jservice:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi"); + assertThat(config.getCustomJmxScrapingConfigPath()).isEqualTo("/file.properties"); + assertThat(config.getTargetSystems()).isEmpty(); + assertThat(config.getSamplingInterval()) + .describedAs("default sampling interval must align to default metric export interval") + .isEqualTo(Duration.ofMinutes(1)); + assertThat(config.getUsername()).isNull(); + assertThat(config.getPassword()).isNull(); + assertThat(config.getRemoteProfile()).isNull(); + assertThat(config.getRealm()).isNull(); } @Test void shouldFailValidation_missingServiceUrl() { // Given Properties properties = (Properties) validProperties.clone(); - properties.remove(SERVICE_URL); + properties.remove(JMX_SERVICE_URL); // When and Then - assertThatThrownBy(() -> fromProperties(properties, new Properties())) + assertThatThrownBy(() -> fromConfig(TestUtil.configProperties(properties))) .isInstanceOf(ConfigurationException.class) - .hasMessage("otel.jmx.service.url must be specified."); + .hasMessage("missing mandatory otel.jmx.service.url"); } @Test void shouldFailValidation_missingConfigPathAndTargetSystem() { // Given Properties properties = (Properties) validProperties.clone(); - properties.remove(CUSTOM_JMX_SCRAPING_CONFIG); - properties.remove(TARGET_SYSTEM); + properties.remove(JMX_CUSTOM_CONFIG); + properties.remove(JMX_TARGET_SYSTEM); // When and Then - assertThatThrownBy(() -> fromProperties(properties, new Properties())) + assertThatThrownBy(() -> fromConfig(TestUtil.configProperties(properties))) .isInstanceOf(ConfigurationException.class) - .hasMessage("otel.jmx.custom.scraping.config or otel.jmx.target.system must be specified."); + .hasMessage( + "at least one of 'otel.jmx.target.system' or 'otel.jmx.custom.scraping.config' must be set"); } @Test void shouldFailValidation_invalidTargetSystem() { // Given Properties properties = (Properties) validProperties.clone(); - properties.setProperty(TARGET_SYSTEM, "hal9000"); - - // When and Then - assertThatThrownBy(() -> fromProperties(properties, new Properties())) - .isInstanceOf(ConfigurationException.class) - .hasMessageStartingWith("[hal9000] must specify targets from "); - } - - @Test - void shouldFailValidation_blankOtlpEndpointProvided() { - // Given - Properties properties = (Properties) validProperties.clone(); - properties.setProperty(OTLP_ENDPOINT, ""); - properties.setProperty(METRICS_EXPORTER_TYPE, "otlp"); - - // When and Then - assertThatThrownBy(() -> fromProperties(properties, new Properties())) - .isInstanceOf(ConfigurationException.class) - .hasMessage("otel.exporter.otlp.endpoint must be specified for otlp format."); - } - - @Test - void shouldPassValidation_noMetricsExporterType() throws ConfigurationException { - // Given - Properties properties = (Properties) validProperties.clone(); - properties.remove(OTLP_ENDPOINT); - properties.remove(METRICS_EXPORTER_TYPE); - - // When - JmxScraperConfig config = fromProperties(properties, new Properties()); - - // Then - assertThat(config).isNotNull(); - } - - @Test - void shouldPassValidation_nonOtlpMetricsExporterType() throws ConfigurationException { - // Given - Properties properties = (Properties) validProperties.clone(); - properties.remove(OTLP_ENDPOINT); - properties.setProperty(METRICS_EXPORTER_TYPE, "logging"); - - // When - JmxScraperConfig config = fromProperties(properties, new Properties()); - - // Then - assertThat(config).isNotNull(); - } - - @Test - void shouldFailValidation_negativeInterval() { - // Given - Properties properties = (Properties) validProperties.clone(); - properties.setProperty(INTERVAL_MILLISECONDS, "-1"); - - // When and Then - assertThatThrownBy(() -> fromProperties(properties, new Properties())) - .isInstanceOf(ConfigurationException.class) - .hasMessage("otel.jmx.interval.milliseconds must be positive."); - } - - @Test - void shouldFailConfigCreation_invalidInterval() { - // Given - Properties properties = (Properties) validProperties.clone(); - properties.setProperty(INTERVAL_MILLISECONDS, "abc"); + properties.setProperty(JMX_TARGET_SYSTEM, "hal9000"); // When and Then - assertThatThrownBy(() -> fromProperties(properties, new Properties())) + assertThatThrownBy(() -> fromConfig(TestUtil.configProperties(properties))) .isInstanceOf(ConfigurationException.class) - .hasMessage("Failed to parse otel.jmx.interval.milliseconds"); + .hasMessageStartingWith("unsupported target system"); } // TODO: Tests below will be reimplemented diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesCustomizerTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesCustomizerTest.java new file mode 100644 index 000000000..6ee1e7ba1 --- /dev/null +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesCustomizerTest.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class PropertiesCustomizerTest { + + @Test + void tryGetConfigBeforeApply() { + assertThatThrownBy(() -> new PropertiesCustomizer().getScraperConfig()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void defaultOtlpExporter() { + Map map = new HashMap<>(); + map.put("otel.jmx.service.url", "dummy-url"); + map.put("otel.jmx.target.system", "jvm"); + ConfigProperties config = DefaultConfigProperties.createFromMap(map); + + PropertiesCustomizer customizer = new PropertiesCustomizer(); + assertThat(customizer.apply(config)).containsEntry("otel.metrics.exporter", "otlp"); + } + + @Test + void explicitExporterSet() { + Map map = new HashMap<>(); + map.put("otel.jmx.service.url", "dummy-url"); + map.put("otel.jmx.target.system", "jvm"); + map.put("otel.metrics.exporter", "otlp,logging"); + ConfigProperties config = DefaultConfigProperties.createFromMap(map); + + PropertiesCustomizer customizer = new PropertiesCustomizer(); + assertThat(customizer.apply(config)).isEmpty(); + } + + @Test + void getSomeConfiguration() { + Map map = new HashMap<>(); + map.put("otel.jmx.service.url", "dummy-url"); + map.put("otel.jmx.target.system", "jvm"); + map.put("otel.metrics.exporter", "otlp"); + ConfigProperties config = DefaultConfigProperties.createFromMap(map); + + PropertiesCustomizer customizer = new PropertiesCustomizer(); + assertThat(customizer.apply(config)) + .describedAs("sdk configuration should not be overridden") + .isEmpty(); + + JmxScraperConfig scraperConfig = customizer.getScraperConfig(); + assertThat(scraperConfig).isNotNull(); + assertThat(scraperConfig.getTargetSystems()).containsOnly("jvm"); + } + + @Test + void setSdkMetricExportFromJmxInterval() { + Map map = new HashMap<>(); + map.put("otel.jmx.service.url", "dummy-url"); + map.put("otel.jmx.target.system", "jvm"); + map.put("otel.metrics.exporter", "otlp"); + map.put("otel.jmx.interval.milliseconds", "10000"); + ConfigProperties config = DefaultConfigProperties.createFromMap(map); + + PropertiesCustomizer customizer = new PropertiesCustomizer(); + assertThat(customizer.apply(config)) + .describedAs("sdk export interval must be set") + .hasSize(1) + .containsEntry("otel.metric.export.interval", "10000ms"); + } + + @Test + void sdkMetricExportIntervalPriority() { + Map map = new HashMap<>(); + map.put("otel.jmx.service.url", "dummy-url"); + map.put("otel.jmx.target.system", "jvm"); + map.put("otel.metrics.exporter", "otlp"); + map.put("otel.jmx.interval.milliseconds", "10000"); + map.put("otel.metric.export.interval", "15s"); + ConfigProperties config = DefaultConfigProperties.createFromMap(map); + + PropertiesCustomizer customizer = new PropertiesCustomizer(); + assertThat(customizer.apply(config)) + .describedAs("provided sdk export interval must keep provided value") + .isEmpty(); + + assertThat(customizer.getScraperConfig().getSamplingInterval()) + .describedAs("jmx export interval must be ignored") + .isEqualTo(Duration.ofSeconds(15)); + } +} diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesSupplierTest.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesSupplierTest.java new file mode 100644 index 000000000..e0a4811e0 --- /dev/null +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesSupplierTest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Properties; +import org.junit.jupiter.api.Test; + +class PropertiesSupplierTest { + + @Test + void empty() { + PropertiesSupplier supplier = new PropertiesSupplier(new Properties()); + assertThat(supplier.get()).isEmpty(); + } + + @Test + void someValues() { + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + properties.setProperty("hello", "world"); + PropertiesSupplier supplier = new PropertiesSupplier(properties); + assertThat(supplier.get()) + .hasSize(2) + .containsEntry("foo", "bar") + .containsEntry("hello", "world"); + } +} diff --git a/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/TestUtil.java b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/TestUtil.java new file mode 100644 index 000000000..ef8ac68f5 --- /dev/null +++ b/jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/TestUtil.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.config; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class TestUtil { + + private TestUtil() {} + + public static ConfigProperties configProperties(Properties properties) { + Map map = new HashMap<>(); + properties.forEach((k, v) -> map.put((String) k, (String) v)); + return DefaultConfigProperties.createFromMap(map); + } +} diff --git a/jmx-scraper/src/test/resources/validConfig.properties b/jmx-scraper/src/test/resources/validConfig.properties index c4c7ac092..d486af451 100644 --- a/jmx-scraper/src/test/resources/validConfig.properties +++ b/jmx-scraper/src/test/resources/validConfig.properties @@ -1,7 +1,6 @@ otel.jmx.service.url=service:jmx:rmi:///jndi/rmi://myhost:12345/jmxrmi otel.jmx.custom.scraping.config=/my/scraping-config.yaml otel.jmx.target.system=jvm,cassandra -otel.jmx.interval.milliseconds=20000 otel.metrics.exporter=otlp otel.metric.export.interval=1000 otel.exporter.otlp.endpoint=https://myotlpendpoint @@ -17,4 +16,3 @@ javax.net.ssl.keyStoreType=JKS javax.net.ssl.trustStore=/my/trust/store javax.net.ssl.trustStorePassword=def456 javax.net.ssl.trustStoreType=JKS -otel.jmx.aggregate.across.mbeans=true