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..ea810c2d53 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2021 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2021-2024 Huawei Technologies Co., Ltd. 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. @@ -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,24 @@ 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 external agent, such as OTEL + handleExternalAgentInstallation(instrumentation); + } + + private static void handleExternalAgentInstallation(Instrumentation instrumentation) { + AgentConfig agentConfig = ConfigManager.getConfig(AgentConfig.class); + if (agentConfig.isExternalAgentInjection()) { + try { + ExternalAgentManager.installExternalAgent(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..90735810e2 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2023 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2023-2024 Huawei Technologies Co., Ltd. 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. @@ -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..292dfc87b1 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2023 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2023-2024 Huawei Technologies Co., Ltd. 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. @@ -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..783906edfa --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/command/ExternalAgentInstallCommandExecutor.java @@ -0,0 +1,57 @@ +/* + * 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.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.Level; +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.installExternalAgent(true, args, agentPath, agentArgsMap, + CommandProcessor.getInstrumentation()); + } catch (IOException | NoSuchMethodException | ClassNotFoundException | InvocationTargetException + | IllegalAccessException e) { + LOGGER.log(Level.SEVERE, "Failed to install external agent: {0}. Error message: {1}", + new String[]{args, 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..e71dcea79e 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2021 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2021-2024 Huawei Technologies Co., Ltd. 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. @@ -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..2f567e3096 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2023 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2023-2024 Huawei Technologies Co., Ltd. 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. @@ -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..32bcef9303 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2023 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2023-2024 Huawei Technologies Co., Ltd. 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. @@ -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..57a5174b3a --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/main/java/io/sermant/core/ext/ExternalAgentManager.java @@ -0,0 +1,211 @@ +/* + * 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.event.collector.FrameworkEventCollector; +import io.sermant.core.event.collector.FrameworkEventDefinitions; +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() { + } + + /** + * 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 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 installExternalAgent(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; + } + + installAgent(isDynamic, agentName, agentPath, argsMap, instrumentation); + agentStatus.set(true); + LOGGER.log(Level.INFO, "{0} agent installed successfully.", agentName); + FrameworkEventCollector.getInstance() + .collectdHotPluggingEvent(FrameworkEventDefinitions.EXTERNAL_AGENT_INSTALL, + "Hot plugging command[INSTALL-EXTERNAL-AGENT] has been processed. Agent name is: " + agentName); + } + + static void installAgent(boolean isDynamic, String agentName, String agentPath, + Map argsMap, Instrumentation instrumentation) throws IOException, + ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + String agentClassName = loadAgentJar(agentName, agentPath, instrumentation); + Class clazz = ClassLoader.getSystemClassLoader().loadClass(agentClassName); + Method method; + if (isDynamic) { + method = clazz.getMethod(AGENTMAIN, String.class, Instrumentation.class); + setArgsToSystemProperties(argsMap); + } else { + method = clazz.getMethod(PREMAIN, String.class, Instrumentation.class); + } + + // specially support OTEL + if (OtelConstant.OTEL.equals(agentName)) { + initializeOtelArgsProperties(); + } + 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 externalAgentClassName; + try (JarFile jarFile = new JarFile(agentPath)) { + instrumentation.appendToSystemClassLoaderSearch(new JarFile(agentJarFile)); + Attributes attributes = FileUtils.getJarFileAttributes(jarFile); + setAgentVersion(agentName, attributes.getValue(IMPLEMENTATION_VERSION)); + externalAgentClassName = attributes.getValue(PREMAIN_CLASS); + } + return externalAgentClassName; + } + + /** + * 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..5182beb567 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2021-2024 Huawei Technologies Co., Ltd. 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. @@ -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..a5061df769 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2021-2024 Huawei Technologies Co., Ltd. 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. @@ -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..75696f5176 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2021-2024 Huawei Technologies Co., Ltd. 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. @@ -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..8e1c2f8ef5 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2021-2024 Huawei Technologies Co., Ltd. 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. @@ -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..634fb005d0 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2022 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2022-2024 Huawei Technologies Co., Ltd. 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. @@ -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..febf5aa6ea 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2021-2024 Huawei Technologies Co., Ltd. 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. @@ -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..4fb674ceca --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/command/ExternalAgentInstallCommandExecutorTest.java @@ -0,0 +1,121 @@ +/* + * 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; + +/** + * Unit Test for ExternalAgentInstallCommandExecutor + * + * @author lilai + * @since 2024-12-18 + */ +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 { + EventConfig eventConfig = new EventConfig(); + eventConfig.setEnable(false); + configManagerMock.when(() -> ConfigManager.getConfig(EventConfig.class)).thenReturn(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.installExternalAgent was called + otelAgentManagerMock.verify(() -> ExternalAgentManager.installExternalAgent( + 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.installExternalAgent was called + otelAgentManagerMock.verify(() -> ExternalAgentManager.installExternalAgent( + 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/event/collector/FrameworkEventCollectorTest.java b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/event/collector/FrameworkEventCollectorTest.java index af553bd575..7ce968ec8f 100644 --- a/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/event/collector/FrameworkEventCollectorTest.java +++ b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/event/collector/FrameworkEventCollectorTest.java @@ -16,12 +16,22 @@ package io.sermant.core.event.collector; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.when; + +import io.sermant.core.common.BootArgsIndexer; import io.sermant.core.config.ConfigManager; import io.sermant.core.event.Event; import io.sermant.core.event.config.EventConfig; import io.sermant.core.utils.JarFileUtils; import io.sermant.core.utils.ReflectUtils; -import org.junit.*; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -31,9 +41,6 @@ import java.util.jar.JarFile; import java.util.jar.Manifest; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - /** * Unit Tests for FrameworkEventCollector * @@ -48,19 +55,21 @@ public class FrameworkEventCollectorTest { private final MockedStatic jarFileUtilsMockedStatic = Mockito.mockStatic(JarFileUtils.class); - private static final EventConfig EVENT_CONFIG = new EventConfig(); - @Before public void setUp() throws Exception { - configManagerMockedStatic.when(() -> ConfigManager.getConfig(EventConfig.class)).thenReturn(EVENT_CONFIG); + configManagerMockedStatic.when(() -> ConfigManager.getConfig(EventConfig.class)).thenReturn(new EventConfig()); jarFileUtilsMockedStatic.when(() -> JarFileUtils.getManifestAttr(any(), anyString())).thenReturn("1.0.0"); } @Test public void testEventNotEnabled() { - EVENT_CONFIG.setEnable(false); - FrameworkEventCollector.getInstance().collectdHotPluggingEvent(FrameworkEventDefinitions.SERMANT_PLUGIN_UNINSTALL, - "Hot plugging command[INSTALL-PLUGINS] has been processed."); + Optional eventConfigOptional = ReflectUtils.getFieldValue(FrameworkEventCollector.getInstance(), + "eventConfig"); + EventConfig eventConfig = (EventConfig) (eventConfigOptional.get()); + eventConfig.setEnable(false); + FrameworkEventCollector.getInstance() + .collectdHotPluggingEvent(FrameworkEventDefinitions.SERMANT_PLUGIN_UNINSTALL, + "Hot plugging command[INSTALL-PLUGINS] has been processed."); Optional optional = ReflectUtils.getFieldValue(FrameworkEventCollector.getInstance(), "eventQueue"); Assert.assertTrue(optional.isPresent()); BlockingQueue eventQueue = (BlockingQueue) optional.get(); @@ -69,9 +78,13 @@ public void testEventNotEnabled() { @Test public void testEventEnabled() { - EVENT_CONFIG.setEnable(true); - FrameworkEventCollector.getInstance().collectdHotPluggingEvent(FrameworkEventDefinitions.SERMANT_PLUGIN_UNINSTALL, - "Hot plugging command[INSTALL-PLUGINS] has been processed."); + Optional eventConfigOptional = ReflectUtils.getFieldValue(FrameworkEventCollector.getInstance(), + "eventConfig"); + EventConfig eventConfig = (EventConfig) (eventConfigOptional.get()); + eventConfig.setEnable(true); + FrameworkEventCollector.getInstance() + .collectdHotPluggingEvent(FrameworkEventDefinitions.SERMANT_PLUGIN_UNINSTALL, + "Hot plugging command[INSTALL-PLUGINS] has been processed."); Optional optional = ReflectUtils.getFieldValue(FrameworkEventCollector.getInstance(), "eventQueue"); Assert.assertTrue(optional.isPresent()); BlockingQueue eventQueue = (BlockingQueue) optional.get(); 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..f0bbf0c740 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/ext/ExternalAgentManagerTest.java @@ -0,0 +1,124 @@ +/* + * 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.config.ConfigManager; +import io.sermant.core.event.collector.FrameworkEventCollector; +import io.sermant.core.event.config.EventConfig; +import io.sermant.core.utils.ReflectUtils; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.lang.instrument.Instrumentation; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Unit Test for ExternalAgentManager + * + * @author lilai + * @since 2024-12-18 + */ +public class ExternalAgentManagerTest { + private final MockedStatic configManagerMockedStatic = Mockito.mockStatic( + ConfigManager.class); + + private Instrumentation mockInstrumentation = mock(Instrumentation.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<>(); + + @Before + public void setUp() throws Exception { + configManagerMockedStatic.when(() -> ConfigManager.getConfig(EventConfig.class)).thenReturn(new EventConfig()); + Optional eventConfigOptional = ReflectUtils.getFieldValue(FrameworkEventCollector.getInstance(), + "eventConfig"); + EventConfig eventConfig = (EventConfig) (eventConfigOptional.get()); + eventConfig.setEnable(false); + } + + @After + public void tearDown() { + ExternalAgentManager.getExternalAgentInstallationStatus().clear(); + configManagerMockedStatic.close(); + } + + @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 testInstallExternalAgent_installedSuccessful() throws Exception { + boolean isDynamic = true; + try (MockedStatic mockedManager = mockStatic(ExternalAgentManager.class)) { + mockedManager.when( + () -> ExternalAgentManager.installExternalAgent(isDynamic, AGENT_NAME, AGENT_PATH, + ARGS_MAP, mockInstrumentation)).thenCallRealMethod(); + ExternalAgentManager.installExternalAgent(isDynamic, AGENT_NAME, AGENT_PATH, ARGS_MAP, + mockInstrumentation); + mockedManager.verify( + () -> ExternalAgentManager.installAgent(isDynamic, AGENT_NAME, AGENT_PATH, + ARGS_MAP, mockInstrumentation), times(1)); + } + } + + @Test + public void testInstallExternalAgent_alreadyInstalled() throws Exception { + String agentName = AGENT_NAME; + AtomicBoolean status = new AtomicBoolean(true); + Map externalAgentInstallationStatus = ExternalAgentManager.getExternalAgentInstallationStatus(); + externalAgentInstallationStatus.put(agentName, status); + ExternalAgentManager.installExternalAgent(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-core/src/test/java/io/sermant/core/service/heartbeat/common/ExternalAgentInfoTest.java b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/service/heartbeat/common/ExternalAgentInfoTest.java new file mode 100644 index 0000000000..e310063ab7 --- /dev/null +++ b/sermant-agentcore/sermant-agentcore-core/src/test/java/io/sermant/core/service/heartbeat/common/ExternalAgentInfoTest.java @@ -0,0 +1,65 @@ +/* + * 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; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit Test for ExternalAgentManager + * + * @author lilai + * @since 2024-12-18 + */ +public class ExternalAgentInfoTest { + private ExternalAgentInfo agentInfo; + + @Before + public void setUp() { + agentInfo = new ExternalAgentInfo("AgentName", "1.0"); + } + + @Test + public void testConstructor() { + Assert.assertNotNull(agentInfo); + Assert.assertEquals("AgentName", agentInfo.getName()); + Assert.assertEquals("1.0", agentInfo.getVersion()); + } + + @Test + public void testGetName() { + Assert.assertEquals("AgentName", agentInfo.getName()); + } + + @Test + public void testSetName() { + agentInfo.setName("NewAgentName"); + Assert.assertEquals("NewAgentName", agentInfo.getName()); + } + + @Test + public void testGetVersion() { + Assert.assertEquals("1.0", agentInfo.getVersion()); + } + + @Test + public void testSetVersion() { + agentInfo.setVersion("2.0"); + Assert.assertEquals("2.0", agentInfo.getVersion()); + } +} 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..a025a49fdf 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2021 Huawei Technologies Co., Ltd. All rights reserved. + * Copyright (C) 2021-2024 Huawei Technologies Co., Ltd. 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. @@ -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()); } diff --git a/sermant-plugins/sermant-router/router-config-service/src/test/java/io/sermant/router/config/handler/kind/AbstractKindHandlerTest.java b/sermant-plugins/sermant-router/router-config-service/src/test/java/io/sermant/router/config/handler/kind/AbstractKindHandlerTest.java index 98c388ba47..61e017f074 100644 --- a/sermant-plugins/sermant-router/router-config-service/src/test/java/io/sermant/router/config/handler/kind/AbstractKindHandlerTest.java +++ b/sermant-plugins/sermant-router/router-config-service/src/test/java/io/sermant/router/config/handler/kind/AbstractKindHandlerTest.java @@ -45,6 +45,7 @@ public abstract class AbstractKindHandlerTest { @BeforeClass public static void init() { EventConfig config = new EventConfig(); + config.setEnable(false); mockConfigManager = Mockito.mockStatic(ConfigManager.class); mockConfigManager.when(() -> ConfigManager.getConfig(EventConfig.class)).thenReturn(config); diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/registry/RegistryImplTest.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/registry/RegistryImplTest.java index 08257eac20..1a19b1bff1 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/registry/RegistryImplTest.java +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/registry/RegistryImplTest.java @@ -54,8 +54,10 @@ public void setUp() { MockitoAnnotations.openMocks(this); pluginServiceManagerMockedStatic.when(() -> PluginServiceManager.getPluginService(ZkService34.class)) .thenReturn(zkService34); + EventConfig eventConfig = new EventConfig(); + eventConfig.setEnable(false); configManagerMockedStatic.when(() -> ConfigManager.getConfig(EventConfig.class)) - .thenReturn(new EventConfig()); + .thenReturn(eventConfig); start(); }