Skip to content
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

[WIP] JMX scraper #1445

Merged
merged 45 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0f60e22
module bootstrap
SylvainJuge Sep 5, 2024
720633c
add dependencies + test setup
SylvainJuge Sep 5, 2024
07027a2
spotless
SylvainJuge Sep 5, 2024
5a82aac
resolve dependencies on maven local repo
SylvainJuge Sep 6, 2024
d623abe
@robsunday PR with upstream updates
SylvainJuge Sep 17, 2024
f7b5a31
@SylvainJuge PR
SylvainJuge Sep 17, 2024
48b095d
spotless
SylvainJuge Sep 17, 2024
8dfe174
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-c…
SylvainJuge Sep 17, 2024
e4f4769
prepare for tomcat e2e test
SylvainJuge Sep 17, 2024
71e6258
start wiring things together
SylvainJuge Sep 17, 2024
586f780
remove stack trace when sasl not supported
SylvainJuge Sep 18, 2024
966aee6
enhance comments & test cmd log msg
SylvainJuge Sep 18, 2024
350f8a9
More tests
robsunday Sep 18, 2024
91976c0
refactor to use test containers
SylvainJuge Sep 18, 2024
d28fd42
make it mergeable without snapshot
SylvainJuge Sep 18, 2024
9072de0
tidy a few things
SylvainJuge Sep 18, 2024
c0ac43f
remove JmxClient
SylvainJuge Sep 18, 2024
39b6341
Merge pull request #3 from robsunday/jmx-scraper-impl
SylvainJuge Sep 19, 2024
14c0c2d
post-review changes
SylvainJuge Sep 19, 2024
1a6533e
remove warnings
SylvainJuge Sep 19, 2024
aecf5ff
Merge pull request #4 from SylvainJuge/tomcat-it
SylvainJuge Sep 19, 2024
ea46e8d
reformat again
SylvainJuge Sep 19, 2024
f8461b2
update readme
SylvainJuge Sep 19, 2024
71a5749
disable publication until ready
SylvainJuge Sep 19, 2024
317fcde
remove test dependency from runtime
SylvainJuge Sep 19, 2024
c89e527
Merge pull request #5 from SylvainJuge/update-readme
SylvainJuge Sep 19, 2024
378af7a
add TODOs to ensure we don't forget about it
SylvainJuge Sep 20, 2024
178fae3
Apply suggestions from code review
SylvainJuge Sep 25, 2024
c656bf7
remove maven local repo
SylvainJuge Sep 25, 2024
2d24b22
post-review: arguments parsing
SylvainJuge Sep 25, 2024
bb74e6e
post-review: move factory creation
SylvainJuge Sep 25, 2024
1f0fc84
post-review: properties loading
SylvainJuge Sep 25, 2024
140ac17
restore missing serialization ids
SylvainJuge Sep 25, 2024
4c61c22
rework JmxScraper creation
SylvainJuge Sep 25, 2024
8917452
cleanup
SylvainJuge Sep 25, 2024
7a7fed7
remove useless config check
SylvainJuge Sep 25, 2024
ffc05c1
post-review: JmxRemoteClient > JmxConnectorBuilder
SylvainJuge Sep 25, 2024
30094f5
post-review de-groovify + static factory
SylvainJuge Sep 25, 2024
2c16a92
fix tests
SylvainJuge Sep 25, 2024
8ad40e0
attempt to fix tests on some jdks
SylvainJuge Sep 25, 2024
7d0be38
allow empty user config when using system properties
SylvainJuge Sep 25, 2024
c15f40d
Code review followup changes
robsunday Sep 26, 2024
86c3e54
Merge pull request #6 from robsunday/jmx-scraper-impl
SylvainJuge Sep 26, 2024
2c95782
Code review followup changes - added default value for OTLP endpoint
robsunday Sep 27, 2024
3a1021f
Merge pull request #7 from robsunday/jmx-scraper-impl
SylvainJuge Sep 27, 2024
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
4 changes: 4 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ components:
- sfriberg
jmx-metrics:
- breedx-splk
jmx-scraper:
- breedx-splk
- robsunday
- sylvainjuge
maven-extension:
- cyrille-leclerc
- kenfinnigan
Expand Down
8 changes: 8 additions & 0 deletions jmx-scraper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# JMX Metric Scraper

This utility provides a way to query JMX metrics and export them to an OTLP endpoint.
The JMX MBeans and their metric mappings are defined in YAML and reuse implementation from
[jmx-metrics instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/jmx-metrics).

This is currently a work-in-progress component not ready to be used in production.
The end goal is to provide an alternative to the [JMX Gatherer](../jmx-metrics/README.md) utility.
94 changes: 94 additions & 0 deletions jmx-scraper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
plugins {
application
id("com.github.johnrengelman.shadow")

id("otel.java-conventions")

// TODO publishing disabled until component is ready to be used
// id("otel.publish-conventions")
}

description = "JMX metrics scraper"
otelJava.moduleName.set("io.opentelemetry.contrib.jmxscraper")

application.mainClass.set("io.opentelemetry.contrib.jmxscraper.JmxScraper")

dependencies {
// TODO remove snapshot dependency on upstream once 2.9.0 is released
// api(enforcedPlatform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:2.9.0-SNAPSHOT-alpha",))

implementation("io.opentelemetry:opentelemetry-api")
implementation("io.opentelemetry:opentelemetry-sdk")
implementation("io.opentelemetry:opentelemetry-sdk-metrics")
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")

implementation("io.opentelemetry.instrumentation:opentelemetry-jmx-metrics")

testImplementation("org.junit-pioneer:junit-pioneer")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
}

testing {
suites {
val integrationTest by registering(JvmTestSuite::class) {
dependencies {
implementation("org.testcontainers:junit-jupiter")
implementation("org.slf4j:slf4j-simple")
implementation("com.linecorp.armeria:armeria-junit5")
implementation("com.linecorp.armeria:armeria-grpc")
implementation("io.opentelemetry.proto:opentelemetry-proto:0.20.0-alpha")
}
}
}
}

tasks {
shadowJar {
mergeServiceFiles()

manifest {
attributes["Implementation-Version"] = project.version
}
// This should always be standalone, so remove "-all" to prevent unnecessary artifact.
archiveClassifier.set("")
}

jar {
archiveClassifier.set("noshadow")
}

withType<Test>().configureEach {
dependsOn(shadowJar)
dependsOn(named("appJar"))
systemProperty("shadow.jar.path", shadowJar.get().archiveFile.get().asFile.absolutePath)
systemProperty("app.jar.path", named<Jar>("appJar").get().archiveFile.get().asFile.absolutePath)
systemProperty("gradle.project.version", "${project.version}")
}

// Because we reconfigure publishing to only include the shadow jar, the Gradle metadata is not correct.
// Since we are fully bundled and have no dependencies, Gradle metadata wouldn't provide any advantage over
// the POM anyways so in practice we shouldn't be losing anything.
withType<GenerateModuleMetadata>().configureEach {
enabled = false
}
}

tasks.register<Jar>("appJar") {
from(sourceSets.get("integrationTest").output)
archiveClassifier.set("app")
manifest {
attributes["Main-Class"] = "io.opentelemetry.contrib.jmxscraper.TestApp"
}
}

// Don't publish non-shadowed jar (shadowJar is in shadowRuntimeElements)
with(components["java"] as AdhocComponentWithVariants) {
configurations.forEach {
withVariantsFromConfiguration(configurations["apiElements"]) {
skip()
}
withVariantsFromConfiguration(configurations["runtimeElements"]) {
skip()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.jmxscraper;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Network;

public class JmxConnectorBuilderTest {

private static Network network;

@BeforeAll
static void beforeAll() {
network = Network.newNetwork();
}

@AfterAll
static void afterAll() {
network.close();
}

@Test
void noAuth() {
try (TestAppContainer app = new TestAppContainer().withNetwork(network).withJmxPort(9990)) {
app.start();
testConnector(
() -> JmxConnectorBuilder.createNew(app.getHost(), app.getMappedPort(9990)).build());
}
}

@Test
void loginPwdAuth() {
String login = "user";
String pwd = "t0p!Secret";
try (TestAppContainer app =
new TestAppContainer().withNetwork(network).withJmxPort(9999).withUserAuth(login, pwd)) {
app.start();
testConnector(
() ->
JmxConnectorBuilder.createNew(app.getHost(), app.getMappedPort(9999))
.userCredentials(login, pwd)
.build());
}
}

@Test
void serverSSL() {
// TODO: test with SSL enabled as RMI registry seems to work differently with SSL

// create keypair (public,private)
// create server keystore with private key
// configure server keystore
//
// create client truststore with public key
// can we configure to use a custom truststore ???
// connect to server
}

private static void testConnector(ConnectorSupplier connectorSupplier) {
try (JMXConnector connector = connectorSupplier.get()) {
assertThat(connector.getMBeanServerConnection())
.isNotNull()
.satisfies(
connection -> {
try {
ObjectName name = new ObjectName(TestApp.OBJECT_NAME);
Object value = connection.getAttribute(name, "IntValue");
assertThat(value).isEqualTo(42);
} catch (Exception e) {
throw new RuntimeException(e);
}
});

} catch (IOException e) {
throw new RuntimeException(e);
}
}

private interface ConnectorSupplier {
JMXConnector get() throws IOException;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.jmxscraper;

import static org.assertj.core.api.Assertions.assertThat;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.MountableFile;

/** Test container that allows to execute {@link JmxScraper} in an isolated container */
public class JmxScraperContainer extends GenericContainer<JmxScraperContainer> {

private final String endpoint;
private final Set<String> targetSystems;
private String serviceUrl;
private int intervalMillis;
private final Set<String> customYamlFiles;

public JmxScraperContainer(String otlpEndpoint) {
super("openjdk:8u272-jre-slim");

String scraperJarPath = System.getProperty("shadow.jar.path");
assertThat(scraperJarPath).isNotNull();

this.withCopyFileToContainer(MountableFile.forHostPath(scraperJarPath), "/scraper.jar")
.waitingFor(
Wait.forLogMessage(".*JMX scraping started.*", 1)
.withStartupTimeout(Duration.ofSeconds(10)));

this.endpoint = otlpEndpoint;
this.targetSystems = new HashSet<>();
this.customYamlFiles = new HashSet<>();
this.intervalMillis = 1000;
}

@CanIgnoreReturnValue
public JmxScraperContainer withTargetSystem(String targetSystem) {
targetSystems.add(targetSystem);
return this;
}

@CanIgnoreReturnValue
public JmxScraperContainer withIntervalMillis(int intervalMillis) {
this.intervalMillis = intervalMillis;
return this;
}

@CanIgnoreReturnValue
public JmxScraperContainer withService(String host, int port) {
// TODO: adding a way to provide 'host:port' syntax would make this easier for end users
this.serviceUrl =
String.format(
Locale.getDefault(), "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", host, port);
return this;
}

@CanIgnoreReturnValue
public JmxScraperContainer withCustomYaml(String yamlPath) {
this.customYamlFiles.add(yamlPath);
return this;
}

@Override
public void start() {
// for now only configure through JVM args
List<String> arguments = new ArrayList<>();
arguments.add("java");
arguments.add("-Dotel.exporter.otlp.endpoint=" + endpoint);

if (!targetSystems.isEmpty()) {
arguments.add("-Dotel.jmx.target.system=" + String.join(",", targetSystems));
}

if (serviceUrl == null) {
throw new IllegalStateException("Missing service URL");
}
arguments.add("-Dotel.jmx.service.url=" + serviceUrl);
arguments.add("-Dotel.jmx.interval.milliseconds=" + intervalMillis);

if (!customYamlFiles.isEmpty()) {
for (String yaml : customYamlFiles) {
this.withCopyFileToContainer(MountableFile.forClasspathResource(yaml), yaml);
}
arguments.add("-Dotel.jmx.config=" + String.join(",", customYamlFiles));
}

arguments.add("-jar");
arguments.add("/scraper.jar");

this.withCommand(arguments.toArray(new String[0]));

logger().info("Starting scraper with command: " + String.join(" ", arguments));

super.start();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.jmxscraper;

import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
import javax.management.ObjectName;

@SuppressWarnings("all")
public class TestApp implements TestAppMXBean {

public static final String APP_STARTED_MSG = "app started";
public static final String OBJECT_NAME = "io.opentelemetry.test:name=TestApp";

private volatile boolean running;

public static void main(String[] args) {
TestApp app = TestApp.start();
while (app.isRunning()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

private TestApp() {}

static TestApp start() {
TestApp app = new TestApp();
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
try {
ObjectName objectName = new ObjectName(OBJECT_NAME);
mbs.registerMBean(app, objectName);
} catch (Exception e) {
throw new RuntimeException(e);
}
app.running = true;
System.out.println(APP_STARTED_MSG);
return app;
}

@Override
public int getIntValue() {
return 42;
}

@Override
public void stopApp() {
running = false;
}

boolean isRunning() {
return running;
}
}
Loading
Loading