Skip to content

Commit

Permalink
feat(insights): optionally support Red Hat Insights agent (#249)
Browse files Browse the repository at this point in the history
* feat(registration): receive platform env map from server on registration

* feat(insights): support Red Hat Insights agent

Signed-off-by: Elliott Baron <ebaron@redhat.com>

* Update for Insights proxy

* Suppress FindBugs warning

* Add unit tests

* Use released Insights Agent, update for shading

---------

Signed-off-by: Elliott Baron <ebaron@redhat.com>
Co-authored-by: Andrew Azores <aazores@redhat.com>
  • Loading branch information
ebaron and andrewazores authored Nov 23, 2023
1 parent c29a160 commit ffcc28d
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 3 deletions.
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<io.smallrye.config.version>2.12.3</io.smallrye.config.version>
<org.slf4j.version>2.0.7</org.slf4j.version>
<org.projectnessie.cel.bom.version>0.3.21</org.projectnessie.cel.bom.version>
<com.redhat.insights.agent.version>0.9.0</com.redhat.insights.agent.version>

<com.github.spotbugs.version>4.8.1</com.github.spotbugs.version>
<com.github.spotbugs.plugin.version>4.8.1.0</com.github.spotbugs.plugin.version>
Expand Down Expand Up @@ -167,6 +168,18 @@
<version>${io.smallrye.config.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.redhat.insights</groupId>
<artifactId>runtimes-agent</artifactId>
<version>${com.redhat.insights.agent.version}</version>
<classifier>shaded</classifier>
<exclusions>
<exclusion>
<groupId>jdk.tools</groupId>
<artifactId>jdk.tools</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- test deps -->
<dependency>
Expand Down
28 changes: 25 additions & 3 deletions src/main/java/io/cryostat/agent/Agent.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.cryostat.agent;

import java.lang.instrument.Instrumentation;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
Expand All @@ -31,6 +32,8 @@

import io.cryostat.agent.ConfigModule.URIRange;
import io.cryostat.agent.harvest.Harvester;
import io.cryostat.agent.insights.InsightsAgentHelper;
import io.cryostat.agent.model.PluginInfo;
import io.cryostat.agent.triggers.TriggerEvaluator;

import dagger.Component;
Expand All @@ -44,6 +47,8 @@ public class Agent {
private static Logger log = LoggerFactory.getLogger(Agent.class);
private static final AtomicBoolean needsCleanup = new AtomicBoolean(true);

private static InsightsAgentHelper insights;

public static void main(String[] args) {
AgentExitHandler agentExitHandler = null;
try {
Expand Down Expand Up @@ -90,6 +95,10 @@ public static void main(String[] args) {
evt -> {
switch (evt.state) {
case REGISTERED:
log.info("Registration state: {}", evt.state);
// If Red Hat Insights support is enabled, set it up
setupInsightsIfEnabled(insights, registration.getPluginInfo());
break;
case UNREGISTERED:
case REFRESHING:
case REFRESHED:
Expand Down Expand Up @@ -135,7 +144,8 @@ private static AgentExitHandler installSignalHandlers(
return agentExitHandler;
}

public static void agentmain(String args) {
public static void agentmain(String args, Instrumentation instrumentation) {
insights = new InsightsAgentHelper(instrumentation);
Thread t =
new Thread(
() -> {
Expand All @@ -147,8 +157,20 @@ public static void agentmain(String args) {
t.start();
}

public static void premain(String args) {
agentmain(args);
public static void premain(String args, Instrumentation instrumentation) {
agentmain(args, instrumentation);
}

private static void setupInsightsIfEnabled(
InsightsAgentHelper insights, PluginInfo pluginInfo) {
if (insights != null && insights.isInsightsEnabled(pluginInfo)) {
try {
insights.runInsightsAgent(pluginInfo);
log.info("Started Red Hat Insights client");
} catch (Throwable e) {
log.error("Unable to start Red Hat Insights client", e);
}
}
}

@Singleton
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/cryostat/agent/Registration.java
Original file line number Diff line number Diff line change
Expand Up @@ -375,4 +375,8 @@ public enum State {
this.state = state;
}
}

PluginInfo getPluginInfo() {
return pluginInfo;
}
}
147 changes: 147 additions & 0 deletions src/main/java/io/cryostat/agent/insights/InsightsAgentHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright The Cryostat Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cryostat.agent.insights;

import java.lang.instrument.Instrumentation;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Supplier;

import io.cryostat.agent.ConfigModule;
import io.cryostat.agent.model.PluginInfo;

import com.redhat.insights.agent.AgentBasicReport;
import com.redhat.insights.agent.AgentConfiguration;
import com.redhat.insights.agent.ClassNoticer;
import com.redhat.insights.agent.InsightsAgentHttpClient;
import com.redhat.insights.agent.shaded.InsightsReportController;
import com.redhat.insights.agent.shaded.http.InsightsHttpClient;
import com.redhat.insights.agent.shaded.jars.JarInfo;
import com.redhat.insights.agent.shaded.logging.InsightsLogger;
import com.redhat.insights.agent.shaded.reports.InsightsReport;
import com.redhat.insights.agent.shaded.tls.PEMSupport;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class InsightsAgentHelper {

private static final String INSIGHTS_SVC = "INSIGHTS_SVC";

private static final InsightsLogger log =
new SLF4JWrapper(LoggerFactory.getLogger(InsightsAgentHelper.class));
private static final BlockingQueue<JarInfo> jarsToSend = new LinkedBlockingQueue<>();

@SuppressFBWarnings("EI_EXPOSE_REP2")
private final Instrumentation instrumentation;

private final Config config;

public InsightsAgentHelper(Instrumentation instrumentation) {
this.instrumentation = instrumentation;
this.config = ConfigProvider.getConfig();
}

public boolean isInsightsEnabled(PluginInfo pluginInfo) {
return pluginInfo.getEnv().containsKey(INSIGHTS_SVC);
}

public void runInsightsAgent(PluginInfo pluginInfo) {
log.info("Starting Red Hat Insights client");
String server = pluginInfo.getEnv().get(INSIGHTS_SVC);
Objects.requireNonNull(server, "Insights server is missing");
String appName = config.getValue(ConfigModule.CRYOSTAT_AGENT_APP_NAME, String.class);

// Add Insights instrumentation
instrument(instrumentation);

Map<String, String> out = new HashMap<>();
out.put("name", appName);
out.put("base_url", server);
out.put("is_ocp", "true");
// If the user's application already contains Insights support,
// use this agent instead as it has the proper configuration
// for OpenShift.
out.put("should_defer", "false");
// Will be replaced by the Insights Proxy
out.put("token", "dummy");
AgentConfiguration config = new AgentConfiguration(out);

final InsightsReport simpleReport = AgentBasicReport.of(log, config);
final PEMSupport pem = new PEMSupport(log, config);

final Supplier<InsightsHttpClient> httpClientSupplier =
() -> new InsightsAgentHttpClient(log, config, () -> pem.createTLSContext());
final InsightsReportController controller =
InsightsReportController.of(
log, config, simpleReport, httpClientSupplier, jarsToSend);
controller.generate();
}

private static void instrument(Instrumentation instrumentation) {
ClassNoticer noticer = new ClassNoticer(log, jarsToSend);
instrumentation.addTransformer(noticer);
}

static class SLF4JWrapper implements InsightsLogger {

private final Logger delegate;

SLF4JWrapper(Logger delegate) {
this.delegate = delegate;
}

@Override
public void debug(String message) {
delegate.debug(message);
}

@Override
public void debug(String message, Throwable err) {
delegate.debug(message, err);
}

@Override
public void error(String message) {
delegate.error(message);
}

@Override
public void error(String message, Throwable err) {
delegate.error(message, err);
}

@Override
public void info(String message) {
delegate.info(message);
}

@Override
public void warning(String message) {
delegate.warn(message);
}

@Override
public void warning(String message, Throwable err) {
delegate.warn(message, err);
}
}
}
141 changes: 141 additions & 0 deletions src/test/java/io/cryostat/agent/insights/InsightsAgentHelperTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright The Cryostat Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cryostat.agent.insights;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.lang.instrument.Instrumentation;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

import io.cryostat.agent.ConfigModule;
import io.cryostat.agent.insights.InsightsAgentHelper.SLF4JWrapper;
import io.cryostat.agent.model.PluginInfo;

import com.redhat.insights.agent.AgentBasicReport;
import com.redhat.insights.agent.AgentConfiguration;
import com.redhat.insights.agent.ClassNoticer;
import com.redhat.insights.agent.InsightsAgentHttpClient;
import com.redhat.insights.agent.shaded.InsightsReportController;
import com.redhat.insights.agent.shaded.http.InsightsHttpClient;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class InsightsAgentHelperTest {

@Mock Instrumentation instrumentation;
@Mock PluginInfo pluginInfo;
@Mock Config config;
@Mock AgentBasicReport report;
@Mock InsightsReportController controller;
@Captor ArgumentCaptor<AgentConfiguration> configCaptor;
@Captor ArgumentCaptor<Supplier<InsightsHttpClient>> clientSupplierCaptor;
private MockedStatic<ConfigProvider> providerStatic;
MockedStatic<AgentBasicReport> reportStatic;
MockedStatic<InsightsReportController> controllerStatic;
InsightsAgentHelper helper;

@BeforeEach
void setupEach() {
providerStatic = Mockito.mockStatic(ConfigProvider.class);
providerStatic.when(() -> ConfigProvider.getConfig()).thenReturn(config);

reportStatic = Mockito.mockStatic(AgentBasicReport.class);
reportStatic.when(() -> AgentBasicReport.of(any(), any())).thenReturn(report);

controllerStatic = Mockito.mockStatic(InsightsReportController.class);
controllerStatic
.when(() -> InsightsReportController.of(any(), any(), any(), any(), any()))
.thenReturn(controller);

Map<String, String> env =
Collections.singletonMap("INSIGHTS_SVC", "http://insights-proxy.example.com:8080");
when(pluginInfo.getEnv()).thenReturn(env);

this.helper = new InsightsAgentHelper(instrumentation);
}

@AfterEach
void teardownEach() {
providerStatic.close();
reportStatic.close();
controllerStatic.close();
}

@Test
void testInsightsEnabled() {
Assertions.assertTrue(helper.isInsightsEnabled(pluginInfo));
}

@Test
void testInsightsDisabled() {
when(pluginInfo.getEnv()).thenReturn(Collections.emptyMap());
Assertions.assertFalse(helper.isInsightsEnabled(pluginInfo));
}

@Test
void testRunInsightsAgent() {
when(config.getValue(ConfigModule.CRYOSTAT_AGENT_APP_NAME, String.class))
.thenReturn("test");

helper.runInsightsAgent(pluginInfo);

verify(instrumentation).addTransformer(any(ClassNoticer.class));

reportStatic.verify(
() -> AgentBasicReport.of(any(SLF4JWrapper.class), configCaptor.capture()));

AgentConfiguration agentConfig = configCaptor.getValue();
Assertions.assertEquals("test", agentConfig.getIdentificationName());
Assertions.assertEquals(
"http://insights-proxy.example.com:8080", agentConfig.getUploadBaseURL());
Assertions.assertEquals(Optional.of("dummy"), agentConfig.getMaybeAuthToken());
Assertions.assertEquals(true, agentConfig.isOCP());
Assertions.assertEquals(false, agentConfig.shouldDefer());

controllerStatic.verify(
() ->
InsightsReportController.of(
any(SLF4JWrapper.class),
eq(agentConfig),
eq(report),
clientSupplierCaptor.capture(),
isNotNull()));

InsightsHttpClient client = clientSupplierCaptor.getValue().get();
Assertions.assertInstanceOf(InsightsAgentHttpClient.class, client);

verify(controller).generate();
}
}

0 comments on commit ffcc28d

Please sign in to comment.