diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/LiveProcessLoggersSummary.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/LiveProcessLoggersSummary.java new file mode 100644 index 0000000000..745be89705 --- /dev/null +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/LiveProcessLoggersSummary.java @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 2023 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.protocol; + +public record LiveProcessLoggersSummary(String processType, String processKey, String processName, String processID, + String packageName, String effectiveLevel, String configuredLevel) { + +} diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/STS4LanguageClient.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/STS4LanguageClient.java index 70b16991b1..e674ad6df0 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/STS4LanguageClient.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/STS4LanguageClient.java @@ -51,6 +51,9 @@ public interface STS4LanguageClient extends LanguageClient, SpringIndexLanguageC @JsonNotification("sts/liveprocess/gcpauses/metrics/updated") void liveProcessGcPausesMetricsDataUpdated(LiveProcessSummary processKey); + + @JsonNotification("sts/liveprocess/loglevel/updated") + void liveProcessLogLevelUpdated(LiveProcessLoggersSummary liveProcessLoggersSummary); @JsonNotification("sts/highlight") void highlight(HighlightParams highlights); diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 450d81408c..8287f9370a 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -114,6 +114,7 @@ import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.protocol.CursorMovement; import org.springframework.ide.vscode.commons.protocol.HighlightParams; +import org.springframework.ide.vscode.commons.protocol.LiveProcessLoggersSummary; import org.springframework.ide.vscode.commons.protocol.LiveProcessSummary; import org.springframework.ide.vscode.commons.protocol.STS4LanguageClient; import org.springframework.ide.vscode.commons.protocol.java.ClasspathListenerParams; @@ -437,6 +438,10 @@ public void indexUpdated() { receiveIndexUpdated(); } + @Override + public void liveProcessLogLevelUpdated(LiveProcessLoggersSummary processKey) { + } + }); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/ActuatorConnection.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/ActuatorConnection.java index 012759cab8..72840286b0 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/ActuatorConnection.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/ActuatorConnection.java @@ -33,4 +33,8 @@ public interface ActuatorConnection { Map getStartup() throws IOException; String getLiveMetrics(String metricName, String tags) throws IOException; + + String getLoggers() throws IOException; + + String configureLogLevel(Map args) throws IOException; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/HttpActuatorConnection.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/HttpActuatorConnection.java index bfa174bd9a..eac499dc07 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/HttpActuatorConnection.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/HttpActuatorConnection.java @@ -80,6 +80,21 @@ public String getBeans() throws IOException { return restTemplate.getForObject("/beans", String.class); } + @Override + public String getLoggers() throws IOException { + return restTemplate.getForObject("/loggers", String.class); + } + + @Override + public String configureLogLevel(Map args) throws IOException { + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromPath("/loggers/"+args.get("packageName")); + if (args != null) { + uriBuilder.queryParam("configuredLevel", args.get("configuredLevel")); + } + String url = actuatorUrl + uriBuilder.toUriString(); + return restTemplate.postForObject(URI.create(url), null, String.class); + } + @Override public String getLiveMetrics(String metricName, String tags) throws IOException { UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromPath("/metrics/"+metricName); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LoggerInfo.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LoggerInfo.java new file mode 100644 index 0000000000..e730a32cc5 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LoggerInfo.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2023 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +/** + * @author Udayani V + */ +public class LoggerInfo { + + private String configuredLevel; + + private String effectiveLevel; + + public String getConfiguredLevel() { + return configuredLevel; + } + public String getEffectiveLevel() { + return effectiveLevel; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/Loggers.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/Loggers.java new file mode 100644 index 0000000000..5cb565af00 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/Loggers.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2023 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * @author Udayani V + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Loggers { + + private List levels; + private Map loggers; + + public List getLevels() { + return levels; + } + + public Map getLoggers() { + return loggers; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessCommandHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessCommandHandler.java index d248c30fc3..ddfea9957d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessCommandHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessCommandHandler.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -43,6 +44,8 @@ public class SpringProcessCommandHandler { private static final String COMMAND_LIST_CONNECTED = "sts/livedata/listConnected"; private static final String COMMAND_GET_METRICS = "sts/livedata/get/metrics"; private static final String COMMAND_GET_REFRESH_METRICS = "sts/livedata/refresh/metrics"; + private static final String COMMAND_GET_LOGGERS = "sts/livedata/getLoggers"; + private static final String COMMAND_CONFIGURE_LOGLEVEL = "sts/livedata/configure/logLevel"; private final SpringProcessConnectorService connectorService; private final SpringProcessConnectorLocal localProcessConnector; @@ -88,6 +91,16 @@ public SpringProcessCommandHandler(SimpleLanguageServer server, SpringProcessCon return refreshMetrics(params); }); log.info("Registered command handler: {}",COMMAND_GET_METRICS); + + server.onCommand(COMMAND_GET_LOGGERS, (params) -> { + return getLoggers(params); + }); + log.info("Registered command handler: {}",COMMAND_GET_LOGGERS); + + server.onCommand(COMMAND_CONFIGURE_LOGLEVEL, (params) -> { + return configureLogLevel(params); + }); + log.info("Registered command handler: {}",COMMAND_CONFIGURE_LOGLEVEL); server.onCommand(COMMAND_LIST_CONNECTED, (params) -> { List result = new ArrayList<>(); @@ -320,4 +333,34 @@ private CompletableFuture handleLiveMetricsProcessRequest(ExecuteCommand return CompletableFuture.completedFuture(null); } + + private CompletableFuture getLoggers(ExecuteCommandParams params) { + SpringProcessLoggersData loggersData = null; + SpringProcessParams springProcessParams = new SpringProcessParams(); + springProcessParams.setProcessKey(getProcessKey(params)); + springProcessParams.setEndpoint(getArgumentByKey(params, "endpoint")); + if (springProcessParams.getProcessKey() != null) { + loggersData = connectorService.getLoggers(springProcessParams); + return CompletableFuture.completedFuture(loggersData); + } + + return CompletableFuture.completedFuture(loggersData); + } + + private CompletableFuture configureLogLevel(ExecuteCommandParams params) { + Map args = new HashMap<>(); + args.put("packageName", getArgumentByKey(params, "packageName")); + args.put("configuredLevel", getArgumentByKey(params, "configuredLevel")); + args.put("effectiveLevel", getArgumentByKey(params, "effectiveLevel")); + SpringProcessParams springProcessParams = new SpringProcessParams(); + springProcessParams.setProcessKey(getProcessKey(params)); + springProcessParams.setArgs(args); + + if (springProcessParams.getProcessKey() != null) { + connectorService.configureLogLevel(springProcessParams); + } + + return CompletableFuture.completedFuture(null); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnector.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnector.java index 3e8d22babe..c395a55c1c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnector.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnector.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.livehover.v2; +import java.util.Map; + /** * @author Martin Lippert */ @@ -29,4 +31,6 @@ public interface SpringProcessConnector { String getProcessName(); SpringProcessGcPausesMetricsLiveData refreshGcPausesMetrics(SpringProcessLiveData current, String metricName, String tags) throws Exception; SpringProcessMemoryMetricsLiveData refreshMemoryMetrics(SpringProcessLiveData current, String metricName, String tags) throws Exception; + SpringProcessLoggersData getLoggers(SpringProcessLiveData currentData) throws Exception; + SpringProcessUpdatedLogLevelData configureLogLevel(SpringProcessLiveData currentData, Map args) throws Exception; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverHttp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverHttp.java index f562ce9d45..94ab648067 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverHttp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverHttp.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.livehover.v2; +import java.util.Map; + public class SpringProcessConnectorOverHttp implements SpringProcessConnector { private final ProcessType processType; @@ -157,6 +159,49 @@ public SpringProcessMemoryMetricsLiveData refreshMemoryMetrics(SpringProcessLive throw new Exception("no live memory metrics data received, lets try again"); } + + + @Override + public SpringProcessLoggersData getLoggers(SpringProcessLiveData currentData) + throws Exception { + if (actuatorConnection != null) { + SpringProcessLoggersData loggersData = new SpringProcessLiveDataExtractorOverHttp().retrieveLoggersData(getProcessType(), actuatorConnection, processID, processName, currentData); + + if (this.processID == null) { + this.processID = loggersData.getProcessID(); + } + + if (this.processName == null) { + this.processName = loggersData.getProcessName(); + } + + if (loggersData != null && loggersData.getLoggers() != null) { + return loggersData; + } + } + + throw new Exception("no loggers data received, lets try again"); + } + + + @Override + public SpringProcessUpdatedLogLevelData configureLogLevel(SpringProcessLiveData currentData, Map args) throws Exception { + if (actuatorConnection != null) { + SpringProcessUpdatedLogLevelData springProcessUpdatedLoggersData = new SpringProcessLiveDataExtractorOverHttp().configureLogLevel(getProcessType(), actuatorConnection, processID, processName, currentData, args); + + if (this.processID == null) { + this.processID = springProcessUpdatedLoggersData.getProcessID(); + } + + if (this.processName == null) { + this.processName = springProcessUpdatedLoggersData.getProcessName(); + return springProcessUpdatedLoggersData; + } + + } + + throw new Exception("configure log levels failed, lets try again"); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverJMX.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverJMX.java index 66179848a6..932c029f17 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverJMX.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverJMX.java @@ -210,6 +210,50 @@ public void disconnect() throws Exception { log.error("error closing the JMX connection for: " + jmxURL, e); } } + + @Override + public SpringProcessLoggersData getLoggers(SpringProcessLiveData currentData) throws Exception { + log.info("try to open JMX connection to: " + jmxURL); + + if (jmxConnection != null) { + try { + SpringProcessLiveDataExtractorOverJMX springJMXConnector = new SpringProcessLiveDataExtractorOverJMX(); + + log.info("retrieve live data from: " + jmxURL); + SpringProcessLoggersData loggersData = springJMXConnector.retrieveLoggersData(getProcessType(), jmxConnection, processID, processName, currentData); + + if (loggersData != null) { + return loggersData; + } + } + catch (Exception e) { + log.error("exception while connecting to jmx: " + jmxURL, e); + } + } + + throw new Exception("no loggers data received, lets try again"); + } + + @Override + public SpringProcessUpdatedLogLevelData configureLogLevel(SpringProcessLiveData currentData, Map args) throws Exception { + log.info("try to open JMX connection to: " + jmxURL); + + if (jmxConnection != null) { + try { + SpringProcessLiveDataExtractorOverJMX springJMXConnector = new SpringProcessLiveDataExtractorOverJMX(); + + log.info("retrieve live data from: " + jmxURL); + SpringProcessUpdatedLogLevelData springProcessUpdatedLoggersData = springJMXConnector.configureLogLevel(getProcessType(), jmxConnection, processID, processName, currentData, args); + + return springProcessUpdatedLoggersData; + } + catch (Exception e) { + log.error("exception while connecting to jmx: " + jmxURL, e); + } + } + + throw new Exception("configure log level failed, lets try again"); + } @Override public void addConnectorChangeListener(SpringProcessConnectionChangeListener listener) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java index 8a7b2d552f..044280b2cf 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java @@ -10,8 +10,10 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.livehover.v2; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -34,6 +36,7 @@ public class SpringProcessConnectorService { public static final String MEMORY = "memory"; public static final String HEAP_MEMORY = "heapMemory"; public static final String NON_HEAP_MEMORY = "nonHeapMemory"; + private static final String LOGGERS = "loggers"; private static final Logger log = LoggerFactory.getLogger(SpringProcessConnectorService.class); @@ -292,4 +295,129 @@ private void scheduleRefresh(IndefiniteProgressTask progressTask, SpringProcessP private IndefiniteProgressTask getProgressTask(String prefixId, String title, String message) { return this.progressService.createIndefiniteProgressTask(prefixId + progressIdKey++, title, message); } + + public SpringProcessLoggersData getLoggers(SpringProcessParams springProcessParams) { + log.info("get loggers data: " + springProcessParams.getProcessKey()); + CompletableFuture loggerData = new CompletableFuture<>(); + SpringProcessConnector connector = this.connectors.get(springProcessParams.getProcessKey()); + if (connector != null) { + final IndefiniteProgressTask progressTask = getProgressTask( + "spring-process-connector-service-get-loggers-data" + springProcessParams.getProcessKey(), "Loggers", null); + getLoggersData(progressTask, springProcessParams, connector, loggerData, 0, TimeUnit.SECONDS, 0); + try { + return loggerData.get(); + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to fetch loggers data for the process "+ springProcessParams.getProcessKey()); + } + } + return null; + } + + public void getLoggersData(IndefiniteProgressTask progressTask, SpringProcessParams springProcessParams, SpringProcessConnector connector, CompletableFuture loggerData, long delay, TimeUnit unit, int retryNo) { + String processKey = springProcessParams.getProcessKey(); + String endpoint = springProcessParams.getEndpoint(); + + String progressMessage = "Get loggers for Spring process: " + processKey + " - retry no: " + retryNo; + log.info(progressMessage); + + this.scheduler.schedule(() -> { + + try { + progressTask.progressEvent(progressMessage); + if(LOGGERS.equals(endpoint)) { + SpringProcessLoggersData loggersData = connector.getLoggers(this.liveDataProvider.getCurrent(processKey)); + + if (loggersData != null) { + loggerData.complete(loggersData); + this.connectedSuccess.put(processKey, true); + } + + } + progressTask.done(); + } + catch (Exception e) { + + log.info("problem occured during process live data refresh", e); + + if (retryNo < maxRetryCount && isKnownProcessKey(processKey)) { + getLoggersData(progressTask, springProcessParams, connector, loggerData, retryDelayInSeconds, TimeUnit.SECONDS, + retryNo + 1); + } + else { + progressTask.done(); + + // Send message to client if maximum retries reached on error + if (isKnownProcessKey(processKey)) { + diagnosticService.diagnosticEvent(ShowMessageException + .error("Failed to refresh live data from process " + processKey + " after retries: " + retryNo, e)); + + if (!connectedSuccess.containsKey(connector.getProcessKey())) { + loggerData.complete(null); + disconnectProcess(processKey); + } + } + } + } + + }, 0, TimeUnit.SECONDS); + + } + + public void configureLogLevel(SpringProcessParams springProcessParams) { + log.info("change log level: " + springProcessParams.getProcessKey()); + + SpringProcessConnector connector = this.connectors.get(springProcessParams.getProcessKey()); + if (connector != null) { + final IndefiniteProgressTask progressTask = getProgressTask( + "spring-process-connector-service-configure-log-level" + springProcessParams.getProcessKey(), "Loggers", null); + configureLogLevel(progressTask, springProcessParams, connector, 0, TimeUnit.SECONDS, 0); + } + } + + private void configureLogLevel(IndefiniteProgressTask progressTask, SpringProcessParams springProcessParams, + SpringProcessConnector connector, long delay, TimeUnit unit, int retryNo) { + String processKey = springProcessParams.getProcessKey(); + + String progressMessage = "configure log level for Spring process: " + processKey + " - retry no: " + retryNo; + log.info(progressMessage); + + this.scheduler.schedule(() -> { + + try { + progressTask.progressEvent(progressMessage); + SpringProcessUpdatedLogLevelData springProcessUpdatedLoggersData = connector.configureLogLevel(this.liveDataProvider.getCurrent(processKey), springProcessParams.getArgs()); + + if(springProcessUpdatedLoggersData != null) { + this.liveDataProvider.updateLogLevel(processKey, springProcessUpdatedLoggersData); + this.connectedSuccess.put(processKey, true); + } + + progressTask.done(); + } + catch (Exception e) { + + log.info("problem occured during process live data refresh", e); + + if (retryNo < maxRetryCount && isKnownProcessKey(processKey)) { + configureLogLevel(progressTask, springProcessParams, connector, retryDelayInSeconds, TimeUnit.SECONDS, + retryNo + 1); + } + else { + progressTask.done(); + + // Send message to client if maximum retries reached on error + if (isKnownProcessKey(processKey)) { + diagnosticService.diagnosticEvent(ShowMessageException + .error("Failed to refresh live data from process " + processKey + " after retries: " + retryNo, e)); + + if (!connectedSuccess.containsKey(connector.getProcessKey())) { + disconnectProcess(processKey); + } + } + } + } + + }, 0, TimeUnit.SECONDS); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverHttp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverHttp.java index d1a9b14971..78d3de1573 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverHttp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverHttp.java @@ -269,6 +269,114 @@ public RequestMappingMetrics getRequestMappingMetrics(String[] paths, String[] r }; } + /** + * @param processType + * @param processID if null, will be determined searching existing mbeans for that information (for remote processes via platform beans runtime name) + * @param processName if null, will be determined searching existing mbeans for that information (for remote processes inferring the java command from the system properties) + * @param currentData currently stored live data + * @param metricName + * @param tags + */ + public SpringProcessLoggersData retrieveLoggersData(ProcessType processType, ActuatorConnection connection, String processID, String processName, + SpringProcessLiveData currentData) { + + try { + + if (processID == null) { + processID = connection.getProcessID(); + } + + if (processName == null) { + Properties systemProperties = connection.getSystemProperties(); + if (systemProperties != null) { + String javaCommand = getJavaCommand(systemProperties); + processName = getProcessName(javaCommand); + } + } + + Loggers loggers = getLoggers(connection); + + return new SpringProcessLoggersData( + processType, + processName, + processID, + loggers + ); + } + catch (Exception e) { + log.error("error reading live metrics data from: " + processID + " - " + processName, e); + } + + return null; + + } + + public Loggers getLoggers(ActuatorConnection connection) { + try { + String result = connection.getLoggers(); + + if (result instanceof String) { + return gson.fromJson((String)result, Loggers.class); + } else if(result != null){ + ObjectMapper mapper = new ObjectMapper(); + return mapper.convertValue(result, Loggers.class); + } + } catch (IOException e) { + // ignore + } catch (Exception e) { + log.error("Error parsing loggers", e); + } + return null; + } + + public SpringProcessUpdatedLogLevelData configureLogLevel(ProcessType processType, ActuatorConnection connection, String processID, String processName, + SpringProcessLiveData currentData, Map args) { + + try { + + if (processID == null) { + processID = connection.getProcessID(); + } + + if (processName == null) { + Properties systemProperties = connection.getSystemProperties(); + if (systemProperties != null) { + String javaCommand = getJavaCommand(systemProperties); + processName = getProcessName(javaCommand); + } + } + + configureLogLevel(connection, args); + return new SpringProcessUpdatedLogLevelData( + processType, + processName, + processID, + args.get("packageName"), + args.get("effectiveLevel"), + args.get("configuredLevel") + ); + + } + catch (Exception e) { + log.error("error reading live metrics data from: " + processID + " - " + processName + " : "+ args.get("packageName"), e); + } + return null; + + } + + public void configureLogLevel(ActuatorConnection connection, Map args) { + try { + connection.configureLogLevel(args); + + } catch (IOException e) { + // ignore + } catch (Exception e) { + log.error("Error parsing response", e); + throw e; + } + return; + } + private StartupMetricsModel getStartupMetrics(ActuatorConnection connection, StartupMetricsModel currentStartup) { if (currentStartup != null) { return currentStartup; @@ -415,7 +523,7 @@ public LiveMemoryMetricsModel getLiveMetrics(ActuatorConnection connection, Stri } catch (IOException e) { // ignore } catch (Exception e) { - log.error("Error parsing beans", e); + log.error("Error parsing live metrics", e); } return null; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java index 655d0dbe46..d00ded11e6 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java @@ -299,6 +299,124 @@ public RequestMappingMetrics getRequestMappingMetrics(String[] paths, String[] r }; } + /** + * @param processType + * @param processID if null, will be determined searching existing mbeans for that information (for remote processes via platform beans runtime name) + * @param processName if null, will be determined searching existing mbeans for that information (for remote processes inferring the java command from the system properties) + * @param currentData currently stored live data + */ + public SpringProcessLoggersData retrieveLoggersData(ProcessType processType, JMXConnector jmxConnector, String processID, String processName, + SpringProcessLiveData currentData) { + + try { + MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); + String domain = getDomainForActuator(connection); + + if (processID == null) { + processID = getProcessID(connection); + } + + if (processName == null) { + Properties systemProperties = getSystemProperties(connection); + if (systemProperties != null) { + String javaCommand = getJavaCommand(systemProperties); + processName = getProcessName(javaCommand); + } + } + + Loggers loggers = getLoggers(connection, domain); + + return new SpringProcessLoggersData( + processType, + processName, + processID, + loggers + ); + } + catch (Exception e) { + log.error("error reading live metrics data from: " + processID + " - " + processName, e); + } + + return null; + } + + public Loggers getLoggers(MBeanServerConnection connection, String domain) { + + Object[] params1 = new Object[] {}; + String[] signature = new String[] {String.class.getName(), List.class.getName()}; + + try { + Object loggersData = getActuatorDataFromOperation(connection, + getObjectName(domain, "type=Endpoint,name=Loggers"), + "loggers", + params1, + signature); + if (loggersData instanceof String) { + return gson.fromJson((String)loggersData, Loggers.class); + } else if(loggersData != null){ + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.convertValue(loggersData, Loggers.class); + } + } catch (Exception e) { + log.error("", e); + } + return null; + } + + public SpringProcessUpdatedLogLevelData configureLogLevel(ProcessType processType, JMXConnector jmxConnector, + String processID, String processName, SpringProcessLiveData currentData, Map args) { + try { + MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); + String domain = getDomainForActuator(connection); + + if (processID == null) { + processID = getProcessID(connection); + } + + if (processName == null) { + Properties systemProperties = getSystemProperties(connection); + if (systemProperties != null) { + String javaCommand = getJavaCommand(systemProperties); + processName = getProcessName(javaCommand); + } + } + + changeLogLevel(connection, domain, args); + return new SpringProcessUpdatedLogLevelData( + processType, + processName, + processID, + args.get("packageName"), + args.get("effectiveLevel"), + args.get("configuredLevel") + ); + + } + catch (Exception e) { + log.error("error changing log level : " + processID + " - " + processName + " : "+args.get("packageName"), e); + } + + return null; + } + + public void changeLogLevel(MBeanServerConnection connection, String domain, Map args) throws Exception { + + Object[] params = new Object[] {args.get("packageName"), args.get("configuredLevel")}; + String[] signature = new String[] {String.class.getName(), List.class.getName()}; + + try { + getActuatorDataFromOperation(connection, + getObjectName(domain, "type=Endpoint,name=Loggers"), + "configureLogLevel", + params, + signature); + } catch (Exception e) { + log.error("", e); + throw e; + } + return; + } + private StartupMetricsModel getStartupMetrics(MBeanServerConnection connection, String domain, StartupMetricsModel currentStartup) { if (currentStartup != null) { return currentStartup; @@ -751,6 +869,4 @@ private String getPortViaTomcatBean(MBeanServerConnection connection) throws Exc return null; } - - } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataProvider.java index b1f2a46dcd..1e26c0bde0 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataProvider.java @@ -17,6 +17,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.protocol.LiveProcessLoggersSummary; import org.springframework.ide.vscode.commons.protocol.LiveProcessSummary; import org.springframework.ide.vscode.commons.protocol.STS4LanguageClient; import org.springframework.ide.vscode.commons.util.Assert; @@ -29,6 +30,7 @@ public class SpringProcessLiveDataProvider { private final ConcurrentMap liveData; private final ConcurrentMap memoryMetricsLiveData; private final ConcurrentMap gcPausesMetricsLiveData; + private final ConcurrentMap loggersData; private final List listeners; private final SimpleLanguageServer server; @@ -37,6 +39,7 @@ public SpringProcessLiveDataProvider(SimpleLanguageServer server) { this.liveData = new ConcurrentHashMap<>(); this.memoryMetricsLiveData = new ConcurrentHashMap<>(); this.gcPausesMetricsLiveData = new ConcurrentHashMap<>(); + this.loggersData = new ConcurrentHashMap<>(); this.listeners = new CopyOnWriteArrayList<>(); } @@ -104,6 +107,11 @@ public void updateGcPausesMetrics(String processKey, SpringProcessGcPausesMetric getClient().liveProcessGcPausesMetricsDataUpdated(createGcPausesMetricsSummary(processKey, liveData)); } + public void updateLogLevel(String processKey, SpringProcessUpdatedLogLevelData updatedLogLevelData) { + getClient().liveProcessLogLevelUpdated(createUpdatedLogLevelSummary(processKey, updatedLogLevelData)); + } + + public void addLiveDataChangeListener(SpringProcessLiveDataChangeListener listener) { this.listeners.add(listener); } @@ -131,6 +139,10 @@ public SpringProcessMemoryMetricsLiveData getMemoryMetrics(String processKey) { public SpringProcessGcPausesMetricsLiveData getGcPausesMetrics(String processKey) { return this.gcPausesMetricsLiveData.get(processKey); } + + public SpringProcessLoggersData getLoggersData(String processKey) { + return this.loggersData.get(processKey); + } public static LiveProcessSummary createProcessSummary(String processKey, SpringProcessLiveData liveData) { LiveProcessSummary p = new LiveProcessSummary(); @@ -159,4 +171,11 @@ public static LiveProcessSummary createGcPausesMetricsSummary(String processKey, return p; } + public static LiveProcessLoggersSummary createUpdatedLogLevelSummary(String processKey, SpringProcessUpdatedLogLevelData updatedLogLevelData) { + LiveProcessLoggersSummary p = new LiveProcessLoggersSummary(updatedLogLevelData.getProcessType().jsonName(), + processKey, updatedLogLevelData.getProcessName(), updatedLogLevelData.getProcessID(), + updatedLogLevelData.getPackageName(), updatedLogLevelData.getEffectiveLevel(), + updatedLogLevelData.getConfiguredLevel()); + return p; + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLoggersData.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLoggersData.java new file mode 100644 index 0000000000..5c48cdc429 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLoggersData.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2023 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +/** + * @author Udayani V + */ +public class SpringProcessLoggersData { + + private final ProcessType processType; + private final String processName; + private final String processID; + private final Loggers loggers; + + public SpringProcessLoggersData(ProcessType processType, String processName, String processID, + Loggers loggers) { + super(); + this.processType = processType; + this.processName = processName; + this.processID = processID; + this.loggers = loggers; + + } + + public ProcessType getProcessType() { + return processType; + } + + public String getProcessName() { + return processName; + } + + public String getProcessID() { + return processID; + } + + public Loggers getLoggers() { + return loggers; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessParams.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessParams.java index 4a6a373a00..c9a37ebb16 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessParams.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessParams.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.livehover.v2; +import java.util.Map; + /** * @author V Udayani */ @@ -19,7 +21,7 @@ public class SpringProcessParams { private String endpoint; private String metricName; private String tags; - + private Map args; public SpringProcessParams() { } @@ -62,4 +64,12 @@ public void setTags(String tags) { this.tags = tags; } + public Map getArgs() { + return args; + } + + public void setArgs(Map args) { + this.args = args; + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessUpdatedLogLevelData.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessUpdatedLogLevelData.java new file mode 100644 index 0000000000..16071d3957 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessUpdatedLogLevelData.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2023 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +public class SpringProcessUpdatedLogLevelData { + + private final ProcessType processType; + private final String processName; + private final String processID; + private final String packageName; + private final String effectiveLevel; + private final String configuredLevel; + + public SpringProcessUpdatedLogLevelData(ProcessType processType, String processName, String processID, + String packageName, String effectiveLevel, String configuredLevel) { + super(); + this.processType = processType; + this.processName = processName; + this.processID = processID; + this.packageName = packageName; + this.effectiveLevel = effectiveLevel; + this.configuredLevel = configuredLevel; + + } + + public ProcessType getProcessType() { + return processType; + } + + public String getProcessName() { + return processName; + } + + public String getProcessID() { + return processID; + } + + public String getPackageName() { + return packageName; + } + + public String getEffectiveLevel() { + return effectiveLevel; + } + + public String getConfiguredLevel() { + return configuredLevel; + } + + +} diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index 138605d60f..089673929c 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -13,6 +13,7 @@ import { ApiManager } from "./apiManager"; import { ExtensionAPI } from "./api"; import {registerClasspathService} from "@pivotal-tools/commons-vscode/lib/classpath"; import {registerJavaDataService} from "@pivotal-tools/commons-vscode/lib/java-data"; +import * as setLogLevelUi from './set-log-levels-ui'; const PROPERTIES_LANGUAGE_ID = "spring-boot-properties"; const YAML_LANGUAGE_ID = "spring-boot-properties-yaml"; @@ -146,6 +147,7 @@ export function activate(context: VSCode.ExtensionContext): Thenable client.stop()); liveHoverUi.activate(client, options, context); rewrite.activate(client, options, context); + setLogLevelUi.activate(client, options, context); VSCode.commands.registerCommand('vscode-spring-boot.spring.modulith.metadata.refresh', async () => { const modulithProjects = await VSCode.commands.executeCommand('sts/modulith/projects'); diff --git a/vscode-extensions/vscode-spring-boot/lib/apiManager.ts b/vscode-extensions/vscode-spring-boot/lib/apiManager.ts index 662c8e5b21..8462c75997 100644 --- a/vscode-extensions/vscode-spring-boot/lib/apiManager.ts +++ b/vscode-extensions/vscode-spring-boot/lib/apiManager.ts @@ -8,9 +8,8 @@ import { LiveProcessUpdatedNotification, LiveProcessGcPausesMetricsUpdatedNotification, LiveProcessMemoryMetricsUpdatedNotification, - SpringIndexUpdatedNotification + SpringIndexUpdatedNotification, } from "./notification"; -import VSCode from "vscode"; import {RequestType} from "vscode-languageclient"; export class ApiManager { @@ -29,6 +28,7 @@ export class ApiManager { const onDidLiveProcessGcPausesMetricsUpdate = this.onDidLiveProcessGcPausesMetricsUpdateEmitter.event; const onDidLiveProcessMemoryMetricsUpdate = this.onDidLiveProcessMemoryMetricsUpdateEmitter.event; const onSpringIndexUpdated = this.onSpringIndexUpdateEmitter.event; + // const onDidLiveProcessLoggersUpdate = this.onDidLiveProcessLoggersUpdateEmitter.event; const COMMAND_LIVEDATA_GET = "sts/livedata/get"; const getLiveProcessData = async (query) => { diff --git a/vscode-extensions/vscode-spring-boot/lib/notification.ts b/vscode-extensions/vscode-spring-boot/lib/notification.ts index c82119ee44..2f01718195 100644 --- a/vscode-extensions/vscode-spring-boot/lib/notification.ts +++ b/vscode-extensions/vscode-spring-boot/lib/notification.ts @@ -10,6 +10,18 @@ export interface LiveProcess { processName: string; } +/** + * Information returned by notification for updated log level for the live process + */ +export interface LiveProcessUpdatedLogLevel { + type: string; + processKey: string; + processName: string; + packageName: string; + effectiveLevel: string; + configuredLevel: string; +} + /** * Specialized interface for type 'local' LiveProcess. */ @@ -40,4 +52,8 @@ export namespace LiveProcessMemoryMetricsUpdatedNotification { export namespace SpringIndexUpdatedNotification { export const type = new NotificationType('spring/index/updated'); +} + +export namespace LiveProcessLogLevelUpdatedNotification { + export const type = new NotificationType('sts/liveprocess/loglevel/updated'); } \ No newline at end of file diff --git a/vscode-extensions/vscode-spring-boot/lib/set-log-levels-ui.ts b/vscode-extensions/vscode-spring-boot/lib/set-log-levels-ui.ts new file mode 100644 index 0000000000..d9927f6952 --- /dev/null +++ b/vscode-extensions/vscode-spring-boot/lib/set-log-levels-ui.ts @@ -0,0 +1,133 @@ +'use strict'; + +import * as VSCode from 'vscode'; +import { LanguageClient } from "vscode-languageclient/node"; +import { ActivatorOptions } from '@pivotal-tools/commons-vscode'; +import { LiveProcess, LiveProcessLogLevelUpdatedNotification, LiveProcessUpdatedLogLevel } from './notification'; + +interface ProcessCommandInfo { + processKey : string; + label: string; + action: string; + projectName: string; +} + +export interface Logger { + configuredLevel: string; + effectiveLevel: string; +} + +export interface Loggers { + [propName: string]: Logger; +} + +export interface LoggersData { + levels: string[]; + loggers: Loggers; +} + +export interface ProcessLoggersData { + loggers: LoggersData; + processID: string; + processName: string; + processType: number; +} + +export interface LoggerItem { + logger: Logger; + name: string; +} + +async function setLogLevelHandler() { + + const processData : ProcessCommandInfo[] = await VSCode.commands.executeCommand('sts/livedata/listProcesses'); + const choiceMap = new Map(); + const choices : string[] = []; + processData.forEach(p => { + const slash = p.action.lastIndexOf('/'); + if (slash>=0) { + const choiceLabel = p.label; + choiceMap.set(choiceLabel, p); + choices.push(choiceLabel); + } + }); + if (choices) { + const picked = await VSCode.window.showQuickPick(choices); + if (picked) { + const chosen = choiceMap.get(picked); + try { + const loggers: ProcessLoggersData = await getLoggers(chosen); + await displayLoggers(loggers, chosen.processKey); + } catch (error) { + VSCode.window.showErrorMessage("Failed to fetch loggers for the process " + chosen.processKey); + } + } + } +} + +async function getLoggers(processInfo: ProcessCommandInfo): Promise { + + return new Promise(async (resolve, reject) => { + await VSCode.window.withProgress({ + location: VSCode.ProgressLocation.Window, + title: "Fetching Loggers Data for process "+processInfo.processKey, + cancellable: false + }, async (progress) => { + try { + const loggers: ProcessLoggersData = await VSCode.commands.executeCommand('sts/livedata/getLoggers', processInfo, {"endpoint": "loggers"}); + progress.report({}); + resolve(loggers); + } catch (error) { + reject(error); + } + }); + }); +} + +async function displayLoggers(processLoggersData: ProcessLoggersData, processKey: string) { + let items; + const loggersData = processLoggersData.loggers; + if(loggersData.loggers) { + items = Object.keys(loggersData.loggers).map(packageName => { + const logger: Logger = loggersData.loggers[packageName]; + const effectiveLevel = logger.effectiveLevel; + const label = packageName + ' (' + effectiveLevel + ')'; + return { + packageName, + effectiveLevel, + label + }; + }); + } + if(items) { + const chosenPackage = await VSCode.window.showQuickPick(items); + if (chosenPackage) { + const chosenlogLevel = await VSCode.window.showQuickPick(loggersData.levels); + await VSCode.commands.executeCommand('sts/livedata/configure/logLevel', {"processKey": processKey}, chosenPackage, {"configuredLevel":chosenlogLevel}); + } + } + +} + +async function logLevelUpdated(process: LiveProcessUpdatedLogLevel) { + VSCode.window.showInformationMessage("The Log level for " + process.packageName + " has been updated from " + + process.effectiveLevel + " to " + process.configuredLevel); +} + +/** Called when extension is activated */ +export function activate( + client: LanguageClient, + options: ActivatorOptions, + context: VSCode.ExtensionContext +) { + client.onNotification(LiveProcessLogLevelUpdatedNotification.type, logLevelUpdated) + context.subscriptions.push( + VSCode.commands.registerCommand('vscode-spring-boot.set.log-levels', () => { + if (client.isRunning()) { + return setLogLevelHandler(); + } else { + VSCode.window.showErrorMessage("No Spring Boot project found. Action is only available for Spring Boot Projects"); + } + }) + ); +} \ No newline at end of file diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index 7485ae2624..efbe43c0c5 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -129,6 +129,11 @@ "command": "vscode-spring-boot.spring.modulith.metadata.refresh", "title": "Refresh Modulith Metadata", "category": "Spring Boot" + }, + { + "command": "vscode-spring-boot.set.log-levels", + "title": "Set Log Levels", + "category": "Spring Boot" } ], "configuration": [