You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2023/04/14 15:35:34 UTC
[solr] branch branch_9x updated: SOLR-16458: Migrate v2 logging APIs to JAX-RS (#1160)
This is an automated email from the ASF dual-hosted git repository.
gerlowskija pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new 245f7938e26 SOLR-16458: Migrate v2 logging APIs to JAX-RS (#1160)
245f7938e26 is described below
commit 245f7938e26e3eb02a31918d3c87b4df16a2d342
Author: Calvince Otieno <85...@users.noreply.github.com>
AuthorDate: Tue Apr 11 19:53:41 2023 +0300
SOLR-16458: Migrate v2 logging APIs to JAX-RS (#1160)
This commit splits up the various functional APIs in LoggingHandler and
gives them separate JAX-RS endpoints.
v2 APIs are now available for:
- list logger levels (GET /api/node/logging/levels)
- bulk update logger levels (PUT /api/node/logging/levels)
- fetch log messages (GET /api/node/logging/messages)
- tweak log-listener level (PUT /api/node/logging/messages/threshold)
Co-authored-by: Calvince Otieno <si...@yahoo.com>
Co-authored-by: Jason Gerlowski <ge...@apache.org>
---
solr/CHANGES.txt | 6 +
.../org/apache/solr/handler/admin/InfoHandler.java | 12 +
.../apache/solr/handler/admin/LoggingHandler.java | 86 ++-----
.../solr/handler/admin/api/NodeLoggingAPI.java | 260 +++++++++++++++++++--
.../{NodeLoggingAPI.java => V2NodeLoggingAPI.java} | 41 ++--
.../solr/handler/admin/LoggingHandlerTest.java | 22 +-
.../solr/handler/admin/api/NodeLoggingAPITest.java | 202 ++++++++++++++++
.../handler/admin/api/V2NodeAPIMappingTest.java | 22 --
.../pages/configuring-logging.adoc | 25 +-
9 files changed, 543 insertions(+), 133 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 909d3146bff..fd5fd430ec2 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -42,6 +42,12 @@ Improvements
* SOLR-16394: The path of the v2 "collection backup" API has been tweaked slightly to be more intuitive, and is now available at
`POST /api/collections/backups/backupName/versions`. (Jason Gerlowski)
+* SOLR-16458: Solr's v2 "logging" APIs have been tweaked slightly to be more intuitive. The single v2 endpoint that existing previously
+ (GET /api/node/logging) has been split into several different APIs. Logger levels can be fetched or modified at
+ `GET /api/node/logging/levels` and `PUT /api/node/logging/levels`, respectively. Log messages can be returned from a "listener" at
+ `GET /api/node/logging/messages`. And the threshold for the log "listener" can be modified with
+ `PUT /api/node/logging/messages/threshold`. (Jason Gerlowski, Calvince Otieno)
+
Optimizations
---------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/InfoHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/InfoHandler.java
index 1b44c63dc6e..b982b468935 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/InfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/InfoHandler.java
@@ -25,6 +25,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.solr.api.Api;
+import org.apache.solr.api.JerseyResource;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.CoreContainer;
@@ -169,6 +170,17 @@ public class InfoHandler extends RequestHandlerBase {
return List.copyOf(list);
}
+ @Override
+ public Collection<Class<? extends JerseyResource>> getJerseyResources() {
+ final var apis = new ArrayList<Class<? extends JerseyResource>>();
+ apis.addAll(handlers.get("threads").getJerseyResources());
+ apis.addAll(handlers.get("properties").getJerseyResources());
+ apis.addAll(handlers.get("logging").getJerseyResources());
+ apis.addAll(handlers.get("system").getJerseyResources());
+ apis.addAll(handlers.get("health").getJerseyResources());
+ return apis;
+ }
+
@Override
public Name getPermissionName(AuthorizationContext request) {
// Delegate permission to the actual handler
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java
index 9d280ecde02..67dbe69cd9c 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/LoggingHandler.java
@@ -19,21 +19,17 @@ package org.apache.solr.handler.admin;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-import org.apache.solr.api.AnnotatedApi;
import org.apache.solr.api.Api;
-import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.api.JerseyResource;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.admin.api.NodeLoggingAPI;
+import org.apache.solr.handler.api.V2ApiUtils;
import org.apache.solr.logging.LogWatcher;
-import org.apache.solr.logging.LoggerInfo;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthorizationContext;
@@ -58,42 +54,26 @@ public class LoggingHandler extends RequestHandlerBase {
@Override
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- // Don't do anything if the framework is unknown
- if (watcher == null) {
- rsp.add("error", "Logging Not Initialized");
- return;
- }
- rsp.add("watcher", watcher.getName());
+ final NodeLoggingAPI loggingApi = new NodeLoggingAPI(cc);
SolrParams params = req.getParams();
if (params.get("threshold") != null) {
- watcher.setThreshold(params.get("threshold"));
+ squashV2Response(
+ rsp,
+ loggingApi.setMessageThreshold(
+ new NodeLoggingAPI.SetThresholdRequestBody(params.get("threshold"))));
}
// Write something at each level
if (params.get("test") != null) {
- log.trace("trace message");
- log.debug("debug message");
- RuntimeException exc = new RuntimeException("test");
- log.info("info (with exception) INFO", exc);
- log.warn("warn (with exception) WARN", exc);
- log.error("error (with exception) ERROR", exc);
+ NodeLoggingAPI.writeLogsForTesting();
}
String[] set = params.getParams("set");
if (set != null) {
- for (String pair : set) {
- String[] split = pair.split(":");
- if (split.length != 2) {
- throw new SolrException(
- SolrException.ErrorCode.SERVER_ERROR,
- "Invalid format, expected level:value, got " + pair);
- }
- String category = split[0];
- String level = split[1];
-
- watcher.setLogLevel(category, level);
- }
+ final List<NodeLoggingAPI.LogLevelChange> changes =
+ NodeLoggingAPI.LogLevelChange.createRequestBodyFromV1Params(set);
+ squashV2Response(rsp, loggingApi.modifyLocalLogLevel(changes));
}
String since = req.getParams().get("since");
@@ -104,44 +84,21 @@ public class LoggingHandler extends RequestHandlerBase {
} catch (Exception ex) {
throw new SolrException(ErrorCode.BAD_REQUEST, "invalid timestamp: " + since);
}
- AtomicBoolean found = new AtomicBoolean(false);
- SolrDocumentList docs = watcher.getHistory(time, found);
- if (docs == null) {
- rsp.add("error", "History not enabled");
- return;
- } else {
- SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
- if (time > 0) {
- info.add("since", time);
- info.add("found", found.get());
- } else {
- info.add("levels", watcher.getAllLevels()); // show for the first request
- }
- info.add("last", watcher.getLastEvent());
- info.add("buffer", watcher.getHistorySize());
- info.add("threshold", watcher.getThreshold());
-
- rsp.add("info", info);
- rsp.add("history", docs);
- }
+ squashV2Response(rsp, loggingApi.fetchLocalLogMessages(time));
} else {
- rsp.add("levels", watcher.getAllLevels());
-
- List<LoggerInfo> loggers = new ArrayList<>(watcher.getAllLoggers());
- Collections.sort(loggers);
-
- List<SimpleOrderedMap<?>> info = new ArrayList<>();
- for (LoggerInfo wrap : loggers) {
- info.add(wrap.getInfo());
- }
- rsp.add("loggers", info);
+ squashV2Response(rsp, loggingApi.listAllLoggersAndLevels());
}
+
rsp.setHttpCaching(false);
if (cc != null && AdminHandlersProxy.maybeProxyToNodes(req, rsp, cc)) {
return; // Request was proxied to other node
}
}
+ private void squashV2Response(SolrQueryResponse rsp, NodeLoggingAPI.LoggingResponse response) {
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response);
+ }
+
// ////////////////////// SolrInfoMBeans methods //////////////////////
@Override
@@ -156,7 +113,12 @@ public class LoggingHandler extends RequestHandlerBase {
@Override
public Collection<Api> getApis() {
- return AnnotatedApi.getApis(new NodeLoggingAPI(this));
+ return new ArrayList<>();
+ }
+
+ @Override
+ public Collection<Class<? extends JerseyResource>> getJerseyResources() {
+ return List.of(NodeLoggingAPI.class);
}
@Override
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/NodeLoggingAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/NodeLoggingAPI.java
index a65da6d1ac7..742a53d2d95 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/NodeLoggingAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/NodeLoggingAPI.java
@@ -17,33 +17,257 @@
package org.apache.solr.handler.admin.api;
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_EDIT_PERM;
+import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.admin.LoggingHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.logging.LogWatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+// TODO Add support for 'nodes' param once SOLR-16738 is completed.
/**
- * V2 API for getting or setting log levels on an individual node.
+ * V2 APIs for getting or setting log levels on an individual node.
*
- * <p>This API (GET /v2/node/logging) is analogous to the v1 /admin/info/logging.
+ * <p>These APIs ('/api/node/logging' and descendants) are analogous to the v1 /admin/info/logging.
*/
-public class NodeLoggingAPI {
+@Path("/node/logging")
+public class NodeLoggingAPI extends JerseyResource {
- private final LoggingHandler handler;
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
- public NodeLoggingAPI(LoggingHandler handler) {
- this.handler = handler;
+ private final CoreContainer coreContainer;
+ private final LogWatcher<?> watcher;
+
+ @Inject
+ public NodeLoggingAPI(CoreContainer coreContainer) {
+ this.coreContainer = coreContainer;
+ this.watcher = coreContainer.getLogging();
+ }
+
+ @Path("/levels")
+ @GET
+ @PermissionName(CONFIG_READ_PERM)
+ @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+ public ListLevelsResponse listAllLoggersAndLevels() {
+ ensureLogWatcherEnabled();
+ final ListLevelsResponse response = instantiateLoggingResponse(ListLevelsResponse.class);
+
+ response.levels = watcher.getAllLevels();
+
+ final List<LogLevelInfo> loggerInfo =
+ watcher.getAllLoggers().stream()
+ .sorted()
+ .map(li -> new LogLevelInfo(li.getName(), li.getLevel(), li.isSet()))
+ .collect(Collectors.toList());
+
+ response.loggers = loggerInfo;
+
+ return response;
+ }
+
+ @Path("/levels")
+ @PUT
+ @PermissionName(CONFIG_EDIT_PERM)
+ @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+ public LoggingResponse modifyLocalLogLevel(List<LogLevelChange> requestBody) {
+ ensureLogWatcherEnabled();
+ final LoggingResponse response = instantiateLoggingResponse(LoggingResponse.class);
+
+ if (requestBody == null) {
+ throw new SolrException(BAD_REQUEST, "Missing request body");
+ }
+
+ for (LogLevelChange change : requestBody) {
+ watcher.setLogLevel(change.logger, change.level);
+ }
+ return response;
+ }
+
+ @Path("/messages")
+ @GET
+ @PermissionName(CONFIG_READ_PERM)
+ @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+ public LogMessagesResponse fetchLocalLogMessages(@QueryParam("since") Long boundingTimeMillis) {
+ ensureLogWatcherEnabled();
+ final LogMessagesResponse response = instantiateLoggingResponse(LogMessagesResponse.class);
+ if (boundingTimeMillis == null) {
+ throw new SolrException(BAD_REQUEST, "Missing required parameter, 'since'.");
+ }
+
+ AtomicBoolean found = new AtomicBoolean(false);
+ SolrDocumentList docs = watcher.getHistory(boundingTimeMillis, found);
+ if (docs == null) {
+ throw new SolrException(BAD_REQUEST, "History not enabled");
+ }
+
+ final LogMessageInfo info = new LogMessageInfo();
+ if (boundingTimeMillis > 0) {
+ info.boundingTimeMillis = boundingTimeMillis;
+ info.found = found.get();
+ } else {
+ info.levels = watcher.getAllLevels(); // show for the first request
+ }
+ info.lastRecordTimestampMillis = watcher.getLastEvent();
+ info.buffer = watcher.getHistorySize();
+
+ response.info = info;
+ response.docs = docs;
+
+ return response;
+ }
+
+ @Path("/messages/threshold")
+ @PUT
+ @PermissionName(CONFIG_EDIT_PERM)
+ @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+ public LoggingResponse setMessageThreshold(SetThresholdRequestBody requestBody) {
+ ensureLogWatcherEnabled();
+ final LoggingResponse response = instantiateLoggingResponse(LoggingResponse.class);
+
+ if (requestBody == null || requestBody.level == null) {
+ throw new SolrException(BAD_REQUEST, "Required parameter 'level' missing");
+ }
+ watcher.setThreshold(requestBody.level);
+
+ return response;
+ }
+
+ // A hacky testing-only parameter used to test the v1 LoggingHandler
+ public static void writeLogsForTesting() {
+ log.trace("trace message");
+ log.debug("debug message");
+ RuntimeException exc = new RuntimeException("test");
+ log.info("info (with exception) INFO", exc);
+ log.warn("warn (with exception) WARN", exc);
+ log.error("error (with exception) ERROR", exc);
+ }
+
+ private void ensureLogWatcherEnabled() {
+ if (watcher == null) {
+ throw new SolrException(BAD_REQUEST, "Logging Not Initialized");
+ }
+ }
+
+ private <T extends LoggingResponse> T instantiateLoggingResponse(Class<T> clazz) {
+ final T response = instantiateJerseyResponse(clazz);
+ response.watcherName = watcher.getName();
+ return response;
}
- // TODO See SOLR-15823 for background on the (less than ideal) permission chosen here.
- @EndPoint(
- path = {"/node/logging"},
- method = GET,
- permission = CONFIG_EDIT_PERM)
- public void getOrSetLogLevels(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- handler.handleRequestBody(req, rsp);
+ /** Generic logging response that includes the name of the log watcher (e.g. "Log4j2") */
+ public static class LoggingResponse extends SolrJerseyResponse {
+ @JsonProperty("watcher")
+ public String watcherName;
+ }
+
+ /** A user-requested modification in the level that a specified logger reports at. */
+ public static class LogLevelChange implements JacksonReflectMapWriter {
+ public LogLevelChange() {}
+
+ public LogLevelChange(String logger, String level) {
+ this.logger = logger;
+ this.level = level;
+ }
+
+ @JsonProperty public String logger;
+ @JsonProperty public String level;
+
+ public static List<LogLevelChange> createRequestBodyFromV1Params(String[] rawChangeValues) {
+ final List<LogLevelChange> changes = new ArrayList<>();
+
+ for (String rawChange : rawChangeValues) {
+ String[] split = rawChange.split(":");
+ if (split.length != 2) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Invalid format, expected level:value, got " + rawChange);
+ }
+ changes.add(new LogLevelChange(split[0], split[1]));
+ }
+
+ return changes;
+ }
+ }
+
+ /** The request body for the 'PUT /api/node/logging/messages/threshold' API. */
+ public static class SetThresholdRequestBody implements JacksonReflectMapWriter {
+ public SetThresholdRequestBody() {}
+
+ public SetThresholdRequestBody(String level) {
+ this.level = level;
+ }
+
+ @JsonProperty(required = true)
+ public String level;
+ }
+
+ /** Response format for the 'GET /api/node/logging/messages' API. */
+ public static class LogMessagesResponse extends LoggingResponse {
+ @JsonProperty public LogMessageInfo info;
+
+ @JsonProperty("history")
+ public SolrDocumentList docs;
+ }
+
+ /** Metadata about the log messages returned by the 'GET /api/node/logging/messages' API */
+ public static class LogMessageInfo implements JacksonReflectMapWriter {
+ @JsonProperty("since")
+ public Long boundingTimeMillis;
+
+ @JsonProperty public Boolean found;
+ @JsonProperty public List<String> levels;
+
+ @JsonProperty("last")
+ public long lastRecordTimestampMillis;
+
+ @JsonProperty public int buffer;
+ @JsonProperty public String threshold;
+ }
+
+ /** Response format for the 'GET /api/node/logging/levels' API. */
+ public static class ListLevelsResponse extends LoggingResponse {
+ @JsonProperty public List<String> levels;
+ @JsonProperty public List<LogLevelInfo> loggers;
+ }
+
+ /** Representation of a single logger and its current state. */
+ public static class LogLevelInfo implements JacksonReflectMapWriter {
+ public LogLevelInfo() {}
+
+ public LogLevelInfo(String name, String level, boolean set) {
+ this.name = name;
+ this.level = level;
+ this.set = set;
+ }
+
+ @JsonProperty("name")
+ public String name;
+
+ @JsonProperty("level")
+ public String level;
+
+ @JsonProperty("set")
+ public boolean set;
}
}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/NodeLoggingAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/V2NodeLoggingAPI.java
similarity index 52%
copy from solr/core/src/java/org/apache/solr/handler/admin/api/NodeLoggingAPI.java
copy to solr/core/src/java/org/apache/solr/handler/admin/api/V2NodeLoggingAPI.java
index a65da6d1ac7..e89ad6d5a31 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/NodeLoggingAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/V2NodeLoggingAPI.java
@@ -17,33 +17,36 @@
package org.apache.solr.handler.admin.api;
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_EDIT_PERM;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.admin.LoggingHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
+import java.util.List;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.logging.LogWatcher;
/**
- * V2 API for getting or setting log levels on an individual node.
+ * V2 API for getting log levels on an individual node.
*
- * <p>This API (GET /v2/node/logging) is analogous to the v1 /admin/info/logging.
+ * <p>The endpoint is defined as api/node/logging/levels
*/
-public class NodeLoggingAPI {
+@Path("/api/node/logging")
+public class V2NodeLoggingAPI {
+ private final LogWatcher<?> watcher;
- private final LoggingHandler handler;
-
- public NodeLoggingAPI(LoggingHandler handler) {
- this.handler = handler;
+ @Inject
+ public V2NodeLoggingAPI(LogWatcher<?> watcher) {
+ this.watcher = watcher;
}
- // TODO See SOLR-15823 for background on the (less than ideal) permission chosen here.
- @EndPoint(
- path = {"/node/logging"},
- method = GET,
- permission = CONFIG_EDIT_PERM)
- public void getOrSetLogLevels(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- handler.handleRequestBody(req, rsp);
+ @GET
+ @Path("/levels")
+ @PermissionName(CONFIG_EDIT_PERM)
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<String> getAllLevels() {
+ return this.watcher.getAllLevels();
}
}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/LoggingHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/LoggingHandlerTest.java
index 2e2ded7d248..097306e2088 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/LoggingHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/LoggingHandlerTest.java
@@ -16,7 +16,9 @@
*/
package org.apache.solr.handler.admin;
-import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.ObjectUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
@@ -79,7 +81,7 @@ public class LoggingHandlerTest extends SolrTestCaseJ4 {
new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/info/logging", mparams));
@SuppressWarnings({"unchecked"})
- ArrayList<NamedList<Object>> loggers = (ArrayList<NamedList<Object>>) rsp._get("loggers", null);
+ List<Map<String, Object>> loggers = (List<Map<String, Object>>) rsp._get("loggers", null);
// check log levels
assertTrue(checkLoggerLevel(loggers, PARENT_LOGGER_NAME, ""));
@@ -92,8 +94,8 @@ public class LoggingHandlerTest extends SolrTestCaseJ4 {
new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/info/logging", mparams));
@SuppressWarnings({"unchecked"})
- ArrayList<NamedList<Object>> updatedLoggerLevel =
- (ArrayList<NamedList<Object>>) rsp._get("loggers", null);
+ List<Map<String, Object>> updatedLoggerLevel =
+ (List<Map<String, Object>>) rsp._get("loggers", null);
// check new parent logger level
assertTrue(checkLoggerLevel(updatedLoggerLevel, PARENT_LOGGER_NAME, "TRACE"));
@@ -109,8 +111,8 @@ public class LoggingHandlerTest extends SolrTestCaseJ4 {
new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/info/logging", mparams));
@SuppressWarnings({"unchecked"})
- ArrayList<NamedList<Object>> removedLoggerLevel =
- (ArrayList<NamedList<Object>>) rsp._get("loggers", null);
+ List<Map<String, Object>> removedLoggerLevel =
+ (List<Map<String, Object>>) rsp._get("loggers", null);
assertTrue(checkLoggerLevel(removedLoggerLevel, PARENT_LOGGER_NAME, "OFF"));
@@ -119,10 +121,10 @@ public class LoggingHandlerTest extends SolrTestCaseJ4 {
}
private boolean checkLoggerLevel(
- ArrayList<NamedList<Object>> properties, String logger, String level) {
- for (NamedList<Object> property : properties) {
- String loggerProperty = property._get("name", "").toString();
- String levelProperty = property._get("level", "").toString();
+ List<Map<String, Object>> properties, String logger, String level) {
+ for (Map<String, Object> property : properties) {
+ String loggerProperty = ObjectUtils.firstNonNull(property.get("name"), "").toString();
+ String levelProperty = ObjectUtils.firstNonNull(property.get("level"), "").toString();
if (loggerProperty.equals(logger) && levelProperty.equals(level)) {
return true;
}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/NodeLoggingAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/NodeLoggingAPITest.java
new file mode 100644
index 00000000000..667e30f3555
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/NodeLoggingAPITest.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import javax.inject.Singleton;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.jersey.InjectionFactories;
+import org.apache.solr.jersey.SolrJacksonMapper;
+import org.apache.solr.logging.LogWatcher;
+import org.apache.solr.logging.LoggerInfo;
+import org.glassfish.hk2.utilities.binding.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Unit tests for the functionality offered in {@link NodeLoggingAPI} */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class NodeLoggingAPITest extends JerseyTest {
+
+ private CoreContainer mockCoreContainer;
+ private LogWatcher mockLogWatcher;
+
+ @BeforeClass
+ public static void ensureWorkingMockito() {
+ assumeWorkingMockito();
+ }
+
+ @Override
+ protected Application configure() {
+ setUpMocks();
+ final ResourceConfig config = new ResourceConfig();
+ config.register(NodeLoggingAPI.class);
+ config.register(SolrJacksonMapper.class);
+ config.register(
+ new AbstractBinder() {
+ @Override
+ protected void configure() {
+ bindFactory(new InjectionFactories.SingletonFactory<>(mockCoreContainer))
+ .to(CoreContainer.class)
+ .in(Singleton.class);
+ }
+ });
+
+ return config;
+ }
+
+ private void setUpMocks() {
+ mockCoreContainer = mock(CoreContainer.class);
+ mockLogWatcher = mock(LogWatcher.class);
+ when(mockCoreContainer.getLogging()).thenReturn(mockLogWatcher);
+ }
+
+ @Test
+ public void testReliesOnLogWatcherToListLogLevels() {
+ when(mockLogWatcher.getAllLevels())
+ .thenReturn(List.of("ERROR", "WARN", "INFO", "DEBUG", "TRACE"));
+ when(mockLogWatcher.getAllLoggers())
+ .thenReturn(List.of(logInfo("org.a.s.Foo", "WARN", true), logInfo("org", null, false)));
+ final Response response = target("/node/logging/levels").request().get();
+ final var responseBody = response.readEntity(NodeLoggingAPI.ListLevelsResponse.class);
+
+ assertEquals(5, responseBody.levels.size());
+ assertThat(responseBody.levels, containsInAnyOrder("ERROR", "WARN", "INFO", "DEBUG", "TRACE"));
+
+ assertEquals(2, responseBody.loggers.size());
+ final var firstLogger = responseBody.loggers.get(0);
+ assertEquals("org", firstLogger.name);
+ assertEquals(null, firstLogger.level);
+ assertFalse("Expected logger info to report 'unset'", firstLogger.set);
+ final var secondLogger = responseBody.loggers.get(1);
+ assertEquals("org.a.s.Foo", secondLogger.name);
+ assertEquals("WARN", secondLogger.level);
+ assertTrue("Expected logger info to report 'set'", secondLogger.set);
+ }
+
+ @Test
+ public void testReliesOnLogWatcherToModifyLogLevels() {
+ final Response response =
+ target("/node/logging/levels")
+ .request()
+ .put(Entity.json("[{\"logger\": \"o.a.s.Foo\", \"level\": \"WARN\"}]"));
+ final var responseBody = response.readEntity(NodeLoggingAPI.LoggingResponse.class);
+
+ assertNotNull(responseBody);
+ assertNull("Expected error to be null but was " + responseBody.error, responseBody.error);
+ verify(mockLogWatcher).setLogLevel("o.a.s.Foo", "WARN");
+ }
+
+ private SolrDocumentList logMessageDocList(String... logMessages) {
+ final var docList = new SolrDocumentList();
+ for (String logMessage : logMessages) {
+ final var doc = new SolrDocument();
+ doc.addField("message_s", logMessage);
+ docList.add(doc);
+ }
+ return docList;
+ }
+
+ @Test
+ public void testReliesOnLogWatcherToFetchLogMessages() {
+ when(mockLogWatcher.getHistory(eq(123L), any()))
+ .thenReturn(logMessageDocList("logmsg1", "logmsg2"));
+ when(mockLogWatcher.getLastEvent()).thenReturn(123456L);
+ when(mockLogWatcher.getHistorySize()).thenReturn(321);
+
+ final var response = target("/node/logging/messages").queryParam("since", 123L).request().get();
+ final var responseBody = response.readEntity(NodeLoggingAPI.LogMessagesResponse.class);
+
+ assertNotNull(responseBody);
+ assertNull("Expected error to be null but was " + responseBody.error, responseBody.error);
+ assertNotNull("Expected 'info' subobject to be set, but was null", responseBody.info);
+ assertEquals(Long.valueOf(123), responseBody.info.boundingTimeMillis);
+ assertEquals(123456, responseBody.info.lastRecordTimestampMillis);
+ assertEquals(321, responseBody.info.buffer);
+ assertEquals(2, responseBody.docs.size());
+ assertEquals("logmsg1", responseBody.docs.get(0).getFieldValue("message_s"));
+ assertEquals("logmsg2", responseBody.docs.get(1).getFieldValue("message_s"));
+ }
+
+ @Test
+ public void testReliesOnLogWatcherToSetMessageThreshold() {
+ final var response =
+ target("/node/logging/messages/threshold")
+ .request()
+ .put(Entity.json("{\"level\": \"WARN\"}"));
+ final var responseBody = response.readEntity(NodeLoggingAPI.LoggingResponse.class);
+
+ assertNotNull(responseBody);
+ assertNull("Expected error to be null but was " + responseBody.error, responseBody.error);
+ verify(mockLogWatcher).setThreshold("WARN");
+ }
+
+ // The v1 logging APIs accept log level changes in the form set=logger:level&set=logger2:level2...
+ // This test ensures that we can convert that format to that used by our v2 APIs.
+ @Test
+ public void testCorrectlyParsesV1LogLevelChanges() {
+ final var levelChanges =
+ NodeLoggingAPI.LogLevelChange.createRequestBodyFromV1Params(
+ new String[] {"o.a.s.Foo:WARN", "o.a.s.Bar:INFO"});
+
+ assertEquals(2, levelChanges.size());
+ assertEquals("o.a.s.Foo", levelChanges.get(0).logger);
+ assertEquals("WARN", levelChanges.get(0).level);
+ assertEquals("o.a.s.Bar", levelChanges.get(1).logger);
+ assertEquals("INFO", levelChanges.get(1).level);
+ }
+
+ private static class StubLoggerInfo extends LoggerInfo {
+ private final boolean set;
+
+ public StubLoggerInfo(String logger, String level, boolean set) {
+ super(logger);
+ this.level = level;
+ this.set = set;
+ }
+
+ @Override
+ public boolean isSet() {
+ return set;
+ }
+ }
+
+ private LoggerInfo logInfo(String logger, String level, boolean set) {
+ return new StubLoggerInfo(logger, level, set);
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
index 6e1181a5709..9f6af14d534 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
@@ -151,22 +151,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
assertEquals("anyParamValue", v1Params.get("anyParamName"));
}
- @Test
- public void testLogLevelsApiAllProperties() throws Exception {
- final ModifiableSolrParams solrParams = new ModifiableSolrParams();
- solrParams.add("since", "12345678");
- solrParams.add("threshold", "someThresholdValue");
- solrParams.add("test", "someTestValue");
- solrParams.add("set", "SomeClassName");
- final SolrParams v1Params = captureConvertedLoggingV1Params("/node/logging", "GET", solrParams);
-
- // All parameters are passed through to v1 API as-is.
- assertEquals("12345678", v1Params.get("since"));
- assertEquals("someThresholdValue", v1Params.get("threshold"));
- assertEquals("someTestValue", v1Params.get("test"));
- assertEquals("SomeClassName", v1Params.get("set"));
- }
-
@Test
public void testSystemInfoApiAllProperties() throws Exception {
final ModifiableSolrParams solrParams = new ModifiableSolrParams();
@@ -201,11 +185,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
return doCaptureParams(path, method, inputParams, null, mockSystemInfoHandler);
}
- private SolrParams captureConvertedLoggingV1Params(
- String path, String method, SolrParams inputParams) throws Exception {
- return doCaptureParams(path, method, inputParams, null, mockLoggingHandler);
- }
-
private SolrParams captureConvertedPropertiesV1Params(
String path, String method, SolrParams inputParams) throws Exception {
return doCaptureParams(path, method, inputParams, null, mockPropertiesHandler);
@@ -268,7 +247,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
apiBag.registerObject(new RejoinLeaderElectionAPI(coreHandler));
apiBag.registerObject(new NodePropertiesAPI(infoHandler.getPropertiesHandler()));
apiBag.registerObject(new NodeThreadsAPI(infoHandler.getThreadDumpHandler()));
- apiBag.registerObject(new NodeLoggingAPI(infoHandler.getLoggingHandler()));
apiBag.registerObject(new NodeSystemInfoAPI(infoHandler.getSystemInfoHandler()));
apiBag.registerObject(new NodeHealthAPI(infoHandler.getHealthCheckHandler()));
}
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/configuring-logging.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/configuring-logging.adoc
index d2b8a4cf39b..0dca9a4fa9d 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/configuring-logging.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/configuring-logging.adoc
@@ -76,11 +76,32 @@ There is also a way of sending REST commands to the `admin/info/logging` endpoin
Example:
+[.dynamic-tabs]
+--
+[example.tab-pane#v1createcollection]
+====
+[.tab-label]*V1 API*
+
+[source,bash]
+----
+curl -X GET "http://localhost:8983/solr/admin/info/logging?set=root:WARN"
+----
+====
+
+[example.tab-pane#v2createcollection]
+====
+[.tab-label]*V2 API*
+
[source,bash]
----
-# Set the root logger to level WARN
-curl -s http://localhost:8983/solr/admin/info/logging --data-binary "set=root:WARN"
+curl -X PUT http://localhost:8983/api/node/logging/levels -H 'Content-Type: application/json' -d '
+ [
+ {"logger": "root", "level": "WARN"}
+ ]
+'
----
+====
+--
=== Choosing Log Level at Startup