diff --git a/sermant-agentcore/sermant-agentcore-config/config/config.properties b/sermant-agentcore/sermant-agentcore-config/config/config.properties index b6e079a72e..72375ead96 100644 --- a/sermant-agentcore/sermant-agentcore-config/config/config.properties +++ b/sermant-agentcore/sermant-agentcore-config/config/config.properties @@ -10,7 +10,7 @@ agent.config.enhancedClassesOutputPath= # Enable the host service instance class to be loaded by the thread context classloader during interceptor execution. If enabled, the host class is loaded by the context classloader during interceptor execution for service governance logic use. The default value is true. agent.config.useContextLoader=true # List of class prefixes that need be ignored when bytecode enhancement is performed. -agent.config.ignoredPrefixes=io.sermant +agent.config.ignoredPrefixes=io.sermant,io.opentelemetry # List of interfaces that need to be ignored when bytecode enhancement is used to search for a class. If all implementation classes of an interface do not want to be bytecode enhanced, you can configure this configuration item agent.config.ignoredInterfaces=org.springframework.cglib.proxy.Factory # Specifies which classes in the plugins are allowed to be bytecode enhanced (classes in the plugins are not allowed to be bytecode enhanced by default) @@ -21,6 +21,12 @@ agent.config.preFilter.enable=false agent.config.preFilter.path= # File name of unmatched class name, the default file is 'unmatched_class_name.txt' agent.config.preFilter.file= +# External agent injection +agent.config.externalAgent.injection=false +# External agent name, OTEL is tested and supported. Other agents need to be tested by developers +agent.config.externalAgent.name=OTEL +# File of external agent, example: /user/opentelemetry-javaagent.jar +agent.config.externalAgent.file= #============================= core service configuration =============================# # Heartbeat service switch agent.service.heartbeat.enable=false diff --git a/sermant-agentcore/sermant-agentcore-config/config/test/config.properties b/sermant-agentcore/sermant-agentcore-config/config/test/config.properties index bf0792b554..2fe545eb61 100644 --- a/sermant-agentcore/sermant-agentcore-config/config/test/config.properties +++ b/sermant-agentcore/sermant-agentcore-config/config/test/config.properties @@ -10,7 +10,7 @@ agent.config.enhancedClassesOutputPath= # Enable the host service instance class to be loaded by the thread context classloader during interceptor execution. If enabled, the host class is loaded by the context classloader during interceptor execution for service governance logic use. The default value is true. agent.config.useContextLoader=true # List of class prefixes that need be ignored when bytecode enhancement is performed. -agent.config.ignoredPrefixes=io.sermant +agent.config.ignoredPrefixes=io.sermant,io.opentelemetry # List of interfaces that need to be ignored when bytecode enhancement is used to search for a class. If all implementation classes of an interface do not want to be bytecode enhanced, you can configure this configuration item agent.config.ignoredInterfaces=org.springframework.cglib.proxy.Factory # Specifies which classes in the plugins are allowed to be bytecode enhanced (classes in the plugins are not allowed to be bytecode enhanced by default) @@ -21,6 +21,12 @@ agent.config.preFilter.enable=false agent.config.preFilter.path= # File name of unmatched class name, the default file is 'unmatched_class_name.txt' agent.config.preFilter.file= +# External agent injection +agent.config.externalAgent.injection=false +# External agent name, OTEL is tested and supported. Other agents need to be tested by developers +agent.config.externalAgent.name=OTEL +# File of external agent, example: /user/opentelemetry-javaagent.jar +agent.config.externalAgent.file= #============================= core service configuration =============================# # Heartbeat service switch agent.service.heartbeat.enable=true diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/AgentCoreEntrance.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/AgentCoreEntrance.java index e7d93de004..9255a9a183 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/AgentCoreEntrance.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/AgentCoreEntrance.java @@ -17,6 +17,7 @@ package io.sermant.core; import io.sermant.core.classloader.ClassLoaderManager; +import io.sermant.core.command.CommandProcessor; import io.sermant.core.common.AgentType; import io.sermant.core.common.BootArgsIndexer; import io.sermant.core.common.CommonConstant; @@ -24,6 +25,7 @@ import io.sermant.core.config.ConfigManager; import io.sermant.core.event.EventManager; import io.sermant.core.event.collector.FrameworkEventCollector; +import io.sermant.core.ext.ExternalAgentManager; import io.sermant.core.notification.NotificationInfo; import io.sermant.core.notification.NotificationManager; import io.sermant.core.notification.SermantNotificationType; @@ -33,6 +35,7 @@ import io.sermant.core.plugin.agent.ByteEnhanceManager; import io.sermant.core.plugin.agent.adviser.AdviserInterface; import io.sermant.core.plugin.agent.adviser.AdviserScheduler; +import io.sermant.core.plugin.agent.config.AgentConfig; import io.sermant.core.plugin.agent.info.EnhancementManager; import io.sermant.core.plugin.agent.template.DefaultAdviser; import io.sermant.core.service.ServiceManager; @@ -136,6 +139,20 @@ public static void install(String artifact, Map argsMap, Instrum if (NotificationManager.isEnable()) { NotificationManager.doNotify(new NotificationInfo(SermantNotificationType.LOAD_COMPLETE, null)); } + + // cache instrumentation + CommandProcessor.cacheInstrumentation(instrumentation); + + // install OpenTelemetry Agent + AgentConfig agentConfig = ConfigManager.getConfig(AgentConfig.class); + if (agentConfig.isExternalAgentInjection()) { + try { + ExternalAgentManager.handleAgentInstallation(false, agentConfig.getExternalAgentName(), + agentConfig.getExternalAgentFile(), null, instrumentation); + } catch (Exception e) { + LOGGER.severe("Failed to install external agent: " + e.getMessage()); + } + } } /** diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/Command.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/Command.java index 238bef890d..51ebc53e82 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/Command.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/Command.java @@ -43,7 +43,12 @@ public enum Command { /** * Enhancement query instruction */ - CHECK_ENHANCEMENT("CHECK-ENHANCEMENT"); + CHECK_ENHANCEMENT("CHECK-ENHANCEMENT"), + + /** + * Install external agent instruction + */ + INSTALL_EXTERNAL_AGENT("INSTALL-EXTERNAL-AGENT"); private final String value; diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/CommandProcessor.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/CommandProcessor.java index 7c746328c5..4bf44c196a 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/CommandProcessor.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/CommandProcessor.java @@ -19,6 +19,7 @@ import io.sermant.core.common.LoggerFactory; import io.sermant.core.utils.StringUtils; +import java.lang.instrument.Instrumentation; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -41,12 +42,15 @@ public class CommandProcessor { private static final String COMMAND = "command"; + private static Instrumentation instrumentation; + static { COMMAND_EXECUTOR_MAP.put(Command.INSTALL_PLUGINS.getValue(), new PluginsInstallCommandExecutor()); COMMAND_EXECUTOR_MAP.put(Command.UNINSTALL_AGENT.getValue(), new AgentUnInstallCommandExecutor()); COMMAND_EXECUTOR_MAP.put(Command.UNINSTALL_PLUGINS.getValue(), new PluginsUnInstallCommandExecutor()); COMMAND_EXECUTOR_MAP.put(Command.UPDATE_PLUGINS.getValue(), new PluginsUpdateCommandExecutor()); COMMAND_EXECUTOR_MAP.put(Command.CHECK_ENHANCEMENT.getValue(), new CheckEnhancementsCommandExecutor()); + COMMAND_EXECUTOR_MAP.put(Command.INSTALL_EXTERNAL_AGENT.getValue(), new ExternalAgentInstallCommandExecutor()); } /** @@ -81,4 +85,22 @@ public static void process(Map agentArgsMap) { DynamicAgentArgsManager.refreshAgentArgs(agentArgsMap); commandExecutor.execute(commandArgs); } + + /** + * cache instrumentation for dynamic agent installation + * + * @param inst instrumentation + */ + public static void cacheInstrumentation(Instrumentation inst) { + instrumentation = inst; + } + + /** + * get instrumentation for dynamic agent installation + * + * @return instrumentation + */ + public static Instrumentation getInstrumentation() { + return instrumentation; + } } diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/DynamicAgentArgsManager.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/DynamicAgentArgsManager.java index f19b3b0bb1..e4bced760c 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/DynamicAgentArgsManager.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/DynamicAgentArgsManager.java @@ -52,4 +52,13 @@ public static void refreshAgentArgs(Map newAgentArgs) { public static String getAgentArg(String key) { return AGENT_ARGS.get(key); } + + /** + * get AGENT_ARGS map + * + * @return dynamical args + */ + public static Map getAgentArgsMap() { + return AGENT_ARGS; + } } diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/ExternalAgentInstallCommandExecutor.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/ExternalAgentInstallCommandExecutor.java new file mode 100644 index 0000000000..c9255c58f1 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/ExternalAgentInstallCommandExecutor.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.core.command; + +import io.sermant.core.common.CommonConstant; +import io.sermant.core.common.LoggerFactory; +import io.sermant.core.event.collector.FrameworkEventCollector; +import io.sermant.core.event.collector.FrameworkEventDefinitions; +import io.sermant.core.ext.ExternalAgentManager; +import io.sermant.core.utils.StringUtils; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.logging.Logger; + +/** + * The command executor of external agent installation + * + * @author lilai + * @since 2024-12-14 + */ +public class ExternalAgentInstallCommandExecutor implements CommandExecutor { + private static final Logger LOGGER = LoggerFactory.getLogger(); + + @Override + public void execute(String args) { + Map agentArgsMap = DynamicAgentArgsManager.getAgentArgsMap(); + String agentPath = agentArgsMap.get(CommonConstant.AGENT_FILE_KEY); + if (StringUtils.isEmpty(agentPath)) { + LOGGER.severe("Failed to install external agent: AGENT_FILE in command args is empty"); + return; + } + + try { + ExternalAgentManager.handleAgentInstallation(true, args, agentPath, agentArgsMap, + CommandProcessor.getInstrumentation()); + FrameworkEventCollector.getInstance() + .collectdHotPluggingEvent(FrameworkEventDefinitions.EXTERNAL_AGENT_INSTALL, + "Hot plugging command[INSTALL-EXTERNAL-AGENT] has been processed"); + } catch (IOException | NoSuchMethodException | ClassNotFoundException | InvocationTargetException + | IllegalAccessException e) { + LOGGER.severe("Failed to install external agent: " + e.getMessage()); + } + } +} diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/common/CommonConstant.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/common/CommonConstant.java index f3e41745c8..4b9bfa0dbf 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/common/CommonConstant.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/common/CommonConstant.java @@ -142,6 +142,11 @@ public class CommonConstant { */ public static final String AGENT_PATH_KEY = "agentPath"; + /** + * The key of agent file in dynamic installation + */ + public static final String AGENT_FILE_KEY = "AGENT_FILE"; + private CommonConstant() { } } diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/event/collector/FrameworkEventCollector.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/event/collector/FrameworkEventCollector.java index d6e7dc54f3..8a3658e795 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/event/collector/FrameworkEventCollector.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/event/collector/FrameworkEventCollector.java @@ -164,4 +164,20 @@ public void collectdHotPluggingEvent(FrameworkEventDefinitions frameworkEventDef frameworkEventDefinitions.getEventType(), new EventInfo(frameworkEventDefinitions.getName(), description))); } + + /** + * Collect OpenTelemetry Agent start event + * + * @param startMethod the method name OpenTelemetry Agent starts by + */ + public void collectOtelStartEvent(String startMethod) { + if (!eventConfig.isEnable()) { + return; + } + String description = "OpenTelemetry Agent starts by " + startMethod; + offerEvent(new Event(FrameworkEventDefinitions.OTEL_START.getScope(), + FrameworkEventDefinitions.OTEL_START.getEventLevel(), + FrameworkEventDefinitions.OTEL_START.getEventType(), + new EventInfo(FrameworkEventDefinitions.OTEL_START.getName(), description))); + } } diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/event/collector/FrameworkEventDefinitions.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/event/collector/FrameworkEventDefinitions.java index 90cecca3b6..3efcc98076 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/event/collector/FrameworkEventDefinitions.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/event/collector/FrameworkEventDefinitions.java @@ -74,7 +74,17 @@ public enum FrameworkEventDefinitions { /** * Sermant plugin update event definition */ - SERMANT_PLUGIN_UPDATE("SERMANT_PLUGIN_UPDATE", EventType.OPERATION, EventLevel.NORMAL); + SERMANT_PLUGIN_UPDATE("SERMANT_PLUGIN_UPDATE", EventType.OPERATION, EventLevel.NORMAL), + + /** + * External agent install event definition + */ + EXTERNAL_AGENT_INSTALL("EXTERNAL_AGENT_INSTALL", EventType.OPERATION, EventLevel.NORMAL), + + /** + * OpenTelemetry agent start event definition + */ + OTEL_START("OTEL_START", EventType.OPERATION, EventLevel.NORMAL); /** * event name diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/ext/ExternalAgentManager.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/ext/ExternalAgentManager.java new file mode 100644 index 0000000000..dc25f97563 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/ext/ExternalAgentManager.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.core.ext; + +import com.sun.org.apache.bcel.internal.util.ClassLoader; + +import io.sermant.core.classloader.FrameworkClassLoader; +import io.sermant.core.common.CommonConstant; +import io.sermant.core.common.LoggerFactory; +import io.sermant.core.exception.SermantRuntimeException; +import io.sermant.core.ext.otel.OtelConstant; +import io.sermant.core.plugin.classloader.PluginClassLoader; +import io.sermant.core.plugin.classloader.ServiceClassLoader; +import io.sermant.core.utils.FileUtils; +import io.sermant.god.common.SermantClassLoader; + +import java.io.File; +import java.io.IOException; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The manager of external agent, mainly for installation + * + * @author lilai + * @since 2024-12-14 + */ +public class ExternalAgentManager { + private static final Logger LOGGER = LoggerFactory.getLogger(); + + private static final Map EXTERNAL_AGENT_INSTALLATION_STATUS = new HashMap<>(); + + private static final Map EXTERNAL_AGENT_VERSION = new HashMap<>(); + + private static final String AGENTMAIN = "agentmain"; + + private static final String PREMAIN = "premain"; + + private static final String PREMAIN_CLASS = "Premain-Class"; + + private static final String IMPLEMENTATION_VERSION = "Implementation-Version"; + + private static final String DEFAULT_AGENT_VERSION = "unknown"; + + private ExternalAgentManager() { + } + + /** + * Handle external agent installation. Support premain and agentmain + * + * @param isDynamic Whether the installation is dynamic + * @param agentName agent name + * @param agentPath OpenTelemetry agent file path + * @param argsMap arguments of the installation + * @param instrumentation instrumentation + * @throws IOException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + * @throws InvocationTargetException + * @throws IllegalAccessException + */ + public static void handleAgentInstallation(boolean isDynamic, String agentName, String agentPath, + Map argsMap, Instrumentation instrumentation) + throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, + IllegalAccessException { + switch (agentName) { + case OtelConstant.OTEL: + installOtelAgent(isDynamic, agentPath, argsMap, instrumentation); + break; + default: + installCommonAgent(isDynamic, agentName, agentPath, argsMap, instrumentation); + } + } + + /** + * get agent version + * + * @param agentName agent name + * @return agent version + */ + public static String getAgentVersion(String agentName) { + return EXTERNAL_AGENT_VERSION.getOrDefault(agentName, DEFAULT_AGENT_VERSION); + } + + /** + * set agent version + * + * @param agentName agent name + * @param agentVersion agent version + */ + public static void setAgentVersion(String agentName, String agentVersion) { + EXTERNAL_AGENT_VERSION.put(agentName, agentVersion); + } + + /** + * get the status of specific agent + * + * @param agentName agent name + * @return status + */ + public static boolean getInstallationStatus(String agentName) { + AtomicBoolean atomicBoolean = EXTERNAL_AGENT_INSTALLATION_STATUS.get(agentName); + return atomicBoolean != null && atomicBoolean.get(); + } + + /** + * get the status of all agents + * + * @return status map + */ + public static Map getExternalAgentInstallationStatus() { + return EXTERNAL_AGENT_INSTALLATION_STATUS; + } + + /** + * Install OpenTelemetry Agent + * + * @param isDynamic Whether the installation is dynamic + * @param agentPath OpenTelemetry agent file path + * @param argsMap arguments of the installation + * @param instrumentation instrumentation + * @throws IOException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + * @throws InvocationTargetException + * @throws IllegalAccessException + */ + static void installOtelAgent(boolean isDynamic, String agentPath, Map argsMap, + Instrumentation instrumentation) throws IOException, NoSuchMethodException, ClassNotFoundException, + InvocationTargetException, IllegalAccessException { + AtomicBoolean otelStatus = EXTERNAL_AGENT_INSTALLATION_STATUS.computeIfAbsent(OtelConstant.OTEL, + k -> new AtomicBoolean(false)); + if (otelStatus.get()) { + LOGGER.warning("OpenTelemetry agent is already installed." + + "Only one OpenTelemetry agent can be installed at a time."); + return; + } + + initializeOtelArgsProperties(); + installExternalAgent(isDynamic, OtelConstant.OTEL, agentPath, argsMap, instrumentation); + + otelStatus.set(true); + LOGGER.info("OpenTelemetry agent installed successfully."); + } + + static void installCommonAgent(boolean isDynamic, String agentName, String agentPath, + Map argsMap, Instrumentation instrumentation) throws IOException, + NoSuchMethodException, ClassNotFoundException, InvocationTargetException, IllegalAccessException { + AtomicBoolean agentStatus = EXTERNAL_AGENT_INSTALLATION_STATUS.computeIfAbsent(agentName, + k -> new AtomicBoolean(false)); + if (agentStatus.get()) { + LOGGER.log(Level.WARNING, "{0} agent is already installed. Only one agent can be installed at a time.", + agentName); + return; + } + + installExternalAgent(isDynamic, agentName, agentPath, argsMap, instrumentation); + + agentStatus.set(true); + LOGGER.log(Level.WARNING, "{0} agent installed successfully.", agentName); + } + + static void installExternalAgent(boolean isDynamic, String agentName, String agentPath, + Map argsMap, Instrumentation instrumentation) throws IOException, + ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + String otelAgentClassName = loadAgentJar(agentName, agentPath, instrumentation); + Class clazz = ClassLoader.getSystemClassLoader().loadClass(otelAgentClassName); + Method method; + if (isDynamic) { + method = clazz.getMethod(AGENTMAIN, String.class, Instrumentation.class); + setArgsToSystemProperties(argsMap); + } else { + method = clazz.getMethod(PREMAIN, String.class, Instrumentation.class); + } + method.invoke(null, "", instrumentation); + } + + /** + * Load external agent jar + * + * @param agentPath file path of external agent + * @param instrumentation instrumentation + * @return entrance class of external agent + * @throws IOException + */ + static String loadAgentJar(String agentName, String agentPath, Instrumentation instrumentation) + throws IOException { + File agentJarFile = new File(agentPath); + if (!agentJarFile.isFile()) { + throw new SermantRuntimeException("Invalid Jar file: " + agentJarFile); + } + + String otelAgentClassName; + try (JarFile jarFile = new JarFile(agentPath)) { + instrumentation.appendToSystemClassLoaderSearch(new JarFile(agentJarFile)); + Attributes attributes = FileUtils.getJarFileAttributes(jarFile); + setAgentVersion(agentName, attributes.getValue(IMPLEMENTATION_VERSION)); + otelAgentClassName = attributes.getValue(PREMAIN_CLASS); + } + return otelAgentClassName; + } + + /** + * Set system properties for external agent in dynamic installation scenario, like otel.javaagent.debug=true + * + * @param argsMap arguments of dynamic installation + */ + static void setArgsToSystemProperties(Map argsMap) { + for (String key : argsMap.keySet()) { + System.setProperty(key, argsMap.get(key)); + } + } + + /** + * Initialize necessary OpenTelemetry agent properties to avoid conflicts between Sermant and OpenTelemetry + */ + static void initializeOtelArgsProperties() { + System.setProperty(OtelConstant.OTEL_JAVAAGENT_EXCLUDE_CLASS_LOADERS, + FrameworkClassLoader.class.getName() + CommonConstant.COMMA + SermantClassLoader.class.getName() + + CommonConstant.COMMA + PluginClassLoader.class.getName() + CommonConstant.COMMA + + ServiceClassLoader.class.getName()); + System.setProperty(OtelConstant.OTEL_JAVAAGENT_EXCLUDE_CLASSES, OtelConstant.IO_SERMANT_PREFIX); + } +} diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/ext/otel/OtelConstant.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/ext/otel/OtelConstant.java new file mode 100644 index 0000000000..b3c3305525 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/ext/otel/OtelConstant.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.core.ext.otel; + +/** + * OpenTelemetry Constant + * + * @author lilai + * @since 2024-12-17 + */ +public class OtelConstant { + /** + * Name of OpenTelemetry agent for installation in Sermant + */ + public static final String OTEL = "OTEL"; + + /** + * Key of the excluded classloaders for OpenTelemetry agent + */ + public static final String OTEL_JAVAAGENT_EXCLUDE_CLASS_LOADERS = "otel.javaagent.exclude-class-loaders"; + + /** + * Key of the excluded classes for OpenTelemetry agent + */ + public static final String OTEL_JAVAAGENT_EXCLUDE_CLASSES = "otel.javaagent.exclude-classes"; + + /** + * Classes ignored in OpenTelemetry agent to avoid conflicts + */ + public static final String IO_SERMANT_PREFIX = "io.sermant.*"; + + /** + * Entrance class of OpenTelemetry agent + */ + public static final String OTEL_AGENT_CLASS = "io.opentelemetry.javaagent.OpenTelemetryAgent"; + + private OtelConstant() { + } +} diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/BufferedAgentBuilder.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/BufferedAgentBuilder.java index b9722421fa..281153bb57 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/BufferedAgentBuilder.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/BufferedAgentBuilder.java @@ -21,6 +21,7 @@ import io.sermant.core.common.LoggerFactory; import io.sermant.core.config.ConfigManager; import io.sermant.core.event.collector.FrameworkEventCollector; +import io.sermant.core.ext.otel.OtelConstant; import io.sermant.core.plugin.Plugin; import io.sermant.core.plugin.agent.config.AgentConfig; import io.sermant.core.plugin.agent.declarer.AbstractPluginDeclarer; @@ -342,6 +343,10 @@ private static class IgnoredMatcher implements AgentBuilder.RawMatcher { @Override public boolean matches(TypeDescription typeDesc, ClassLoader classLoader, JavaModule javaModule, Class classBeingRedefined, ProtectionDomain protectionDomain) { + if (OtelConstant.OTEL_AGENT_CLASS.equals(typeDesc.getActualName())) { + return false; + } + if (unMatchedClassCache.containsKey(typeDesc.getActualName())) { return true; } diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/ByteEnhanceManager.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/ByteEnhanceManager.java index 0b7b0cefca..5578549876 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/ByteEnhanceManager.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/ByteEnhanceManager.java @@ -22,6 +22,7 @@ import io.sermant.core.plugin.agent.config.AgentConfig; import io.sermant.core.plugin.agent.declarer.PluginDescription; import io.sermant.core.plugin.agent.enhance.ClassLoaderDeclarer; +import io.sermant.core.plugin.agent.enhance.OpenTelemetryAgentDeclarer; import io.sermant.core.service.ServiceConfig; import io.sermant.core.utils.FileUtils; @@ -126,6 +127,7 @@ public static void unEnhanceDynamicPlugin(Plugin plugin) { private static void enhanceForFramework() { enhanceForInjectService(); + enhanceForOtelAgent(); } /** @@ -136,4 +138,11 @@ private static void enhanceForInjectService() { builder.addEnhance(new ClassLoaderDeclarer()); } } + + /** + * An enhancement to the OpenTelemetry agent for the observability + */ + private static void enhanceForOtelAgent() { + builder.addEnhance(new OpenTelemetryAgentDeclarer()); + } } diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/collector/PluginCollector.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/collector/PluginCollector.java index 52616e245a..7afcd9cc26 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/collector/PluginCollector.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/collector/PluginCollector.java @@ -204,7 +204,7 @@ private static boolean matchTarget(ElementMatcher matcher, Type return result; } catch (Exception exception) { - LOGGER.log(Level.WARNING, "Exception occurs when math target: " + target.getActualName() + ",{0}", + LOGGER.log(Level.WARNING, "Exception occurs when match target: " + target.getActualName() + ",{0}", exception.getMessage()); return false; } diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/config/AgentConfig.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/config/AgentConfig.java index b62d5b7372..bf25a1853a 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/config/AgentConfig.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/config/AgentConfig.java @@ -79,6 +79,15 @@ public class AgentConfig implements BaseConfig { @ConfigFieldKey("preFilter.file") private String preFilterFile; + @ConfigFieldKey("externalAgent.injection") + private boolean externalAgentInjection = false; + + @ConfigFieldKey("externalAgent.name") + private String externalAgentName = "unknown"; + + @ConfigFieldKey("externalAgent.file") + private String externalAgentFile = ""; + /** * Allows classes to be loaded from the thread context, mainly used by the PluginClassLoader to load the classes of * the host instance through the thread context, if not allowed can be specified during the interceptor call @@ -172,4 +181,28 @@ public String getPreFilterFile() { public void setPreFilterFile(String preFilterFile) { this.preFilterFile = preFilterFile; } + + public boolean isExternalAgentInjection() { + return externalAgentInjection; + } + + public void setExternalAgentInjection(boolean externalAgentInjection) { + this.externalAgentInjection = externalAgentInjection; + } + + public String getExternalAgentName() { + return externalAgentName; + } + + public void setExternalAgentName(String externalAgentName) { + this.externalAgentName = externalAgentName; + } + + public String getExternalAgentFile() { + return externalAgentFile; + } + + public void setExternalAgentFile(String externalAgentFile) { + this.externalAgentFile = externalAgentFile; + } } diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/enhance/OpenTelemetryAgentDeclarer.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/enhance/OpenTelemetryAgentDeclarer.java new file mode 100644 index 0000000000..867c215005 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/enhance/OpenTelemetryAgentDeclarer.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.core.plugin.agent.enhance; + +import io.sermant.core.ext.otel.OtelConstant; +import io.sermant.core.plugin.agent.declarer.AbstractPluginDeclarer; +import io.sermant.core.plugin.agent.declarer.InterceptDeclarer; +import io.sermant.core.plugin.agent.matcher.ClassMatcher; +import io.sermant.core.plugin.agent.matcher.MethodMatcher; + +/** + * OpenTelemetry agent enhancement declarer + * + * @author lilai + * @since 2024-12-16 + */ +public class OpenTelemetryAgentDeclarer extends AbstractPluginDeclarer { + @Override + public ClassMatcher getClassMatcher() { + return ClassMatcher.nameEquals(OtelConstant.OTEL_AGENT_CLASS); + } + + @Override + public InterceptDeclarer[] getInterceptDeclarers(ClassLoader classLoader) { + return new InterceptDeclarer[]{ + InterceptDeclarer.build(MethodMatcher.nameContains("premain", "agentmain"), + new OpenTelemetryAgentInterceptor())}; + } +} diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/enhance/OpenTelemetryAgentInterceptor.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/enhance/OpenTelemetryAgentInterceptor.java new file mode 100644 index 0000000000..a55f656885 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/plugin/agent/enhance/OpenTelemetryAgentInterceptor.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.core.plugin.agent.enhance; + +import io.sermant.core.event.collector.FrameworkEventCollector; +import io.sermant.core.plugin.agent.entity.ExecuteContext; +import io.sermant.core.plugin.agent.interceptor.Interceptor; + +/** + * premain and agentmain of OpenTelemetryAgent interceptor + * + * @author lilai + * @since 2024-12-16 + */ +public class OpenTelemetryAgentInterceptor implements Interceptor { + @Override + public ExecuteContext before(ExecuteContext context) throws Exception { + return context; + } + + @Override + public ExecuteContext after(ExecuteContext context) throws Exception { + String methodName = context.getMethod().getName(); + FrameworkEventCollector.getInstance().collectOtelStartEvent(methodName); + return context; + } + + @Override + public ExecuteContext onThrow(ExecuteContext context) throws Exception { + return context; + } +} diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/service/heartbeat/common/ExternalAgentInfo.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/service/heartbeat/common/ExternalAgentInfo.java new file mode 100644 index 0000000000..059d853fc5 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/service/heartbeat/common/ExternalAgentInfo.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.core.service.heartbeat.common; + +/** + * Information of external agent + * + * @author lilai + * @since 2024-12-18 + */ +public class ExternalAgentInfo { + private String name; + + private String version; + + /** + * constructor + * + * @param name agent name + * @param version agent version + */ + public ExternalAgentInfo(String name, String version) { + this.name = name; + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/service/heartbeat/common/HeartbeatMessage.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/service/heartbeat/common/HeartbeatMessage.java index 7f756c47f7..006e2f1b0f 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/service/heartbeat/common/HeartbeatMessage.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/service/heartbeat/common/HeartbeatMessage.java @@ -56,6 +56,8 @@ public class HeartbeatMessage { private final Map pluginInfoMap = new HashMap<>(); + private final Map externalAgentInfoMap = new HashMap<>(); + /** * constructor */ @@ -135,4 +137,8 @@ public String getProcessId() { public boolean isDynamicInstall() { return dynamicInstall; } + + public Map getExternalAgentInfoMap() { + return externalAgentInfoMap; + } } diff --git a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/utils/FileUtils.java b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/utils/FileUtils.java index c5f6ba8bf5..ce2ec2c0de 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/utils/FileUtils.java +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/utils/FileUtils.java @@ -34,6 +34,9 @@ import java.nio.file.Paths; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; @@ -299,4 +302,16 @@ private static String buildUnmatchedFileString() { public static void setAgentPath(String path) { agentPath = path; } + + /** + * get attributes from jar file + * + * @param jarFile jar file + * @return attributes of the given jar file + * @throws IOException + */ + public static Attributes getJarFileAttributes(JarFile jarFile) throws IOException { + Manifest manifest = jarFile.getManifest(); + return manifest != null ? manifest.getMainAttributes() : new Attributes(); + } } diff --git a/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/command/ExternalAgentInstallCommandExecutorTest.java b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/command/ExternalAgentInstallCommandExecutorTest.java new file mode 100644 index 0000000000..6f34494862 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/command/ExternalAgentInstallCommandExecutorTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.core.command; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; + +import io.sermant.core.config.ConfigManager; +import io.sermant.core.event.collector.FrameworkEventCollector; +import io.sermant.core.event.config.EventConfig; +import io.sermant.core.ext.ExternalAgentManager; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.lang.instrument.Instrumentation; +import java.util.HashMap; +import java.util.Map; + +public class ExternalAgentInstallCommandExecutorTest { + private MockedStatic dynamicAgentArgsManagerMockedStatic = mockStatic( + DynamicAgentArgsManager.class); + + private Instrumentation mockInstrumentation = mock(Instrumentation.class); + + private MockedStatic commandProcessorMock = mockStatic(CommandProcessor.class); + + private MockedStatic otelAgentManagerMock = mockStatic(ExternalAgentManager.class); + + private MockedStatic frameworkEventCollectorMock = mockStatic( + FrameworkEventCollector.class); + + private FrameworkEventCollector mockEventCollector = mock(FrameworkEventCollector.class); + + private Map mockArgsMap = new HashMap<>(); + + private MockedStatic configManagerMock = mockStatic(ConfigManager.class); + + @Before + public void setUp() throws Exception { + configManagerMock.when(() -> ConfigManager.getConfig(EventConfig.class)).thenReturn(new EventConfig()); + dynamicAgentArgsManagerMockedStatic.when(DynamicAgentArgsManager::getAgentArgsMap).thenReturn(mockArgsMap); + commandProcessorMock.when(CommandProcessor::getInstrumentation).thenReturn(mockInstrumentation); + frameworkEventCollectorMock.when(FrameworkEventCollector::getInstance).thenReturn(mockEventCollector); + } + + @After + public void tearDown() throws Exception { + commandProcessorMock.close(); + otelAgentManagerMock.close(); + frameworkEventCollectorMock.close(); + mockArgsMap.clear(); + configManagerMock.close(); + dynamicAgentArgsManagerMockedStatic.close(); + } + + @Test + public void testExecute_Success() { + mockArgsMap.put("AGENT_FILE", "/path/to/otel-agent.jar"); + + // Test instance + ExternalAgentInstallCommandExecutor executor = new ExternalAgentInstallCommandExecutor(); + + // Execute test + executor.execute("OTEL"); + + // Verify ExternalAgentManager.handleAgentInstallation was called + otelAgentManagerMock.verify(() -> ExternalAgentManager.handleAgentInstallation( + true, + "OTEL", + "/path/to/otel-agent.jar", + mockArgsMap, + mockInstrumentation + ), times(1)); + } + + @Test + public void testExecute_failure() { + mockArgsMap.put("WRONG_FILE", "/path/to/otel-agent.jar"); + + // Test instance + ExternalAgentInstallCommandExecutor executor = new ExternalAgentInstallCommandExecutor(); + + // Execute test + executor.execute("OTEL"); + + // Verify ExternalAgentManager.handleAgentInstallation was called + otelAgentManagerMock.verify(() -> ExternalAgentManager.handleAgentInstallation( + true, + "OTEL", + "/path/to/otel-agent.jar", + mockArgsMap, + mockInstrumentation + ), times(0)); + } +} diff --git a/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/ext/ExternalAgentManagerTest.java b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/ext/ExternalAgentManagerTest.java new file mode 100644 index 0000000000..2c19fe7fb8 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/ext/ExternalAgentManagerTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * 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.sermant.core.ext; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.sermant.core.ext.otel.OtelConstant; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.io.File; +import java.lang.instrument.Instrumentation; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +public class ExternalAgentManagerTest { + private Instrumentation mockInstrumentation = mock(Instrumentation.class); + + private JarFile mockJarFile = mock(JarFile.class); + + private Manifest mockManifest = mock(Manifest.class); + + private Attributes mockAttributes = mock(Attributes.class); + + private File mockFile = mock(File.class); + + private static final String AGENT_NAME = "demo-agent"; + + private static final String AGENT_PATH = "/path/to/demo-agent.jar"; + + private static final Map ARGS_MAP = new HashMap<>(); + + @After + public void tearDown() { + ExternalAgentManager.getExternalAgentInstallationStatus().clear(); + } + + @Test + public void testHandleAgentInstallation_otel() throws Exception { + boolean isDynamic = true; + try (MockedStatic mockedManager = mockStatic(ExternalAgentManager.class)) { + mockedManager.when( + () -> ExternalAgentManager.handleAgentInstallation(isDynamic, OtelConstant.OTEL, AGENT_PATH, + ARGS_MAP, mockInstrumentation)).thenCallRealMethod(); + ExternalAgentManager.handleAgentInstallation(isDynamic, OtelConstant.OTEL, AGENT_PATH, ARGS_MAP, + mockInstrumentation); + mockedManager.verify( + () -> ExternalAgentManager.installOtelAgent(isDynamic, AGENT_PATH, ARGS_MAP, mockInstrumentation), + times(1)); + } + } + + @Test + public void testHandleAgentInstallation_commonAgent() throws Exception { + boolean isDynamic = true; + try (MockedStatic mockedManager = mockStatic(ExternalAgentManager.class)) { + mockedManager.when( + () -> ExternalAgentManager.handleAgentInstallation(isDynamic, AGENT_NAME, AGENT_PATH, + ARGS_MAP, mockInstrumentation)).thenCallRealMethod(); + ExternalAgentManager.handleAgentInstallation(isDynamic, AGENT_NAME, AGENT_PATH, ARGS_MAP, + mockInstrumentation); + mockedManager.verify( + () -> ExternalAgentManager.installCommonAgent(isDynamic, AGENT_NAME, AGENT_PATH, ARGS_MAP, + mockInstrumentation), + times(1)); + } + } + + @Test + public void testGetAgentVersion() { + String agentName = AGENT_NAME; + ExternalAgentManager.setAgentVersion(agentName, "1.0.0"); + String version = ExternalAgentManager.getAgentVersion(agentName); + Assert.assertEquals("1.0.0", version); + } + + @Test + public void testGetInstallationStatus() { + Map externalAgentInstallationStatus = ExternalAgentManager.getExternalAgentInstallationStatus(); + externalAgentInstallationStatus.put(AGENT_NAME, new AtomicBoolean(true)); + boolean installationStatus = ExternalAgentManager.getInstallationStatus(AGENT_NAME); + Assert.assertTrue(installationStatus); + } + + @Test + public void testInstallOtelAgent_alreadyInstalled() throws Exception { + Map externalAgentInstallationStatus = ExternalAgentManager.getExternalAgentInstallationStatus(); + externalAgentInstallationStatus.put(OtelConstant.OTEL, new AtomicBoolean(true)); + ExternalAgentManager.installOtelAgent(true, AGENT_PATH, ARGS_MAP, mockInstrumentation); + verify(mockInstrumentation, never()).appendToSystemClassLoaderSearch(any()); + } + + @Test + public void testInstallOtelAgent_installedSuccessful() throws Exception { + boolean isDynamic = true; + try (MockedStatic mockedManager = mockStatic(ExternalAgentManager.class)) { + mockedManager.when( + () -> ExternalAgentManager.installOtelAgent(isDynamic, AGENT_PATH, + ARGS_MAP, mockInstrumentation)).thenCallRealMethod(); + ExternalAgentManager.installOtelAgent(isDynamic, AGENT_PATH, ARGS_MAP, + mockInstrumentation); + mockedManager.verify(ExternalAgentManager::initializeOtelArgsProperties, times(1)); + mockedManager.verify( + () -> ExternalAgentManager.installExternalAgent(isDynamic, OtelConstant.OTEL, AGENT_PATH, + ARGS_MAP, mockInstrumentation), times(1)); + } + } + + @Test + public void testInstallCommonAgent_installedSuccessful() throws Exception { + boolean isDynamic = true; + try (MockedStatic mockedManager = mockStatic(ExternalAgentManager.class)) { + mockedManager.when( + () -> ExternalAgentManager.installCommonAgent(isDynamic, AGENT_NAME, AGENT_PATH, + ARGS_MAP, mockInstrumentation)).thenCallRealMethod(); + ExternalAgentManager.installCommonAgent(isDynamic, AGENT_NAME, AGENT_PATH, ARGS_MAP, + mockInstrumentation); + mockedManager.verify( + () -> ExternalAgentManager.installExternalAgent(isDynamic, AGENT_NAME, AGENT_PATH, + ARGS_MAP, mockInstrumentation), times(1)); + } + } + + @Test + public void testInstallCommonAgent_alreadyInstalled() throws Exception { + String agentName = AGENT_NAME; + AtomicBoolean status = new AtomicBoolean(true); + Map externalAgentInstallationStatus = ExternalAgentManager.getExternalAgentInstallationStatus(); + externalAgentInstallationStatus.put(agentName, status); + ExternalAgentManager.installCommonAgent(true, AGENT_NAME, AGENT_PATH, ARGS_MAP, mockInstrumentation); + verify(mockInstrumentation, never()).appendToSystemClassLoaderSearch(any()); + } + + @Test + public void testSetArgsToSystemProperties() { + ARGS_MAP.put("otel.javaagent.debug", "true"); + ExternalAgentManager.setArgsToSystemProperties(ARGS_MAP); + Assert.assertEquals("true", System.getProperty("otel.javaagent.debug")); + } +} diff --git a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/heartbeat/HeartbeatServiceImpl.java b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/heartbeat/HeartbeatServiceImpl.java index f383aa441c..f2f6ecf842 100644 --- a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/heartbeat/HeartbeatServiceImpl.java +++ b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/heartbeat/HeartbeatServiceImpl.java @@ -21,10 +21,12 @@ import io.sermant.core.common.CommonConstant; import io.sermant.core.common.LoggerFactory; import io.sermant.core.config.ConfigManager; +import io.sermant.core.ext.ExternalAgentManager; import io.sermant.core.plugin.common.PluginConstant; import io.sermant.core.plugin.common.PluginSchemaValidator; import io.sermant.core.service.heartbeat.api.ExtInfoProvider; import io.sermant.core.service.heartbeat.api.HeartbeatService; +import io.sermant.core.service.heartbeat.common.ExternalAgentInfo; import io.sermant.core.service.heartbeat.common.HeartbeatConstant; import io.sermant.core.service.heartbeat.common.HeartbeatMessage; import io.sermant.core.service.heartbeat.common.PluginInfo; @@ -110,6 +112,15 @@ private void execute() { new PluginInfo(entry.getKey(), entry.getValue())); addExtInfo(entry.getKey(), heartbeatMessage.getPluginInfoMap().get(entry.getKey())); } + + // add external agent installation information + for (String agentName : ExternalAgentManager.getExternalAgentInstallationStatus().keySet()) { + if (ExternalAgentManager.getInstallationStatus(agentName)) { + heartbeatMessage.getExternalAgentInfoMap().putIfAbsent(agentName, + new ExternalAgentInfo(agentName, ExternalAgentManager.getAgentVersion(agentName))); + } + } + heartbeatMessage.updateHeartbeatVersion(); if (nettyClient == null) { LOGGER.warning("Netty client is null when send heartbeat message."); diff --git a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/hotplugging/entity/HotPluggingConfig.java b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/hotplugging/entity/HotPluggingConfig.java index c31bd4cc63..375e0be4f6 100644 --- a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/hotplugging/entity/HotPluggingConfig.java +++ b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/hotplugging/entity/HotPluggingConfig.java @@ -33,6 +33,11 @@ public class HotPluggingConfig { */ private String pluginNames; + /** + * name of external agent + */ + private String externalAgentName; + /** * Instance ID,generate by UUID */ @@ -87,4 +92,12 @@ public String getParams() { public void setParams(String params) { this.params = params; } + + public String getExternalAgentName() { + return externalAgentName; + } + + public void setExternalAgentName(String externalAgentName) { + this.externalAgentName = externalAgentName; + } } diff --git a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/hotplugging/listener/HotPluggingListener.java b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/hotplugging/listener/HotPluggingListener.java index 3216cd1ba5..cca2c4783e 100644 --- a/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/hotplugging/listener/HotPluggingListener.java +++ b/sermant-agentcore/sermant-agentcore-implement/src/main/java/io/sermant/implement/service/hotplugging/listener/HotPluggingListener.java @@ -18,6 +18,7 @@ import io.sermant.core.command.CommandProcessor; import io.sermant.core.common.BootArgsIndexer; +import io.sermant.core.common.CommonConstant; import io.sermant.core.operation.OperationManager; import io.sermant.core.operation.converter.api.YamlConverter; import io.sermant.core.service.dynamicconfig.common.DynamicConfigEvent; @@ -59,8 +60,11 @@ public void process(DynamicConfigEvent event) { } Map argsMap = new HashMap<>(); parseParams(config, argsMap); + argsMap.put(CommonConstant.AGENT_FILE_KEY, config.getAgentPath()); if (!StringUtils.isEmpty(config.getPluginNames())) { argsMap.put(COMMAND, config.getCommandType() + ":" + config.getPluginNames().replace(",", "/")); + } else if (!StringUtils.isEmpty(config.getExternalAgentName())) { + argsMap.put(COMMAND, config.getCommandType() + ":" + config.getExternalAgentName()); } else { argsMap.put(COMMAND, config.getCommandType()); }