This is an automated email from the ASF dual-hosted git repository.
madhan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new 346fe34a4 RANGER-5266: [Generated by Gemini AI]: (security-admin) REST
endpoints to set log level while service is running (#620)
346fe34a4 is described below
commit 346fe34a41446cf29abefb627b61ab01376da4f3
Author: Fateh Singh <[email protected]>
AuthorDate: Sat Sep 20 10:05:09 2025 -0700
RANGER-5266: [Generated by Gemini AI]: (security-admin) REST endpoints to
set log level while service is running (#620)
---
.../main/java/org/apache/ranger/RangerClient.java | 20 +++
.../python/apache_ranger/client/ranger_client.py | 19 ++-
.../apache/ranger/biz/RangerLogLevelService.java | 115 ++++++++++++++++
.../java/org/apache/ranger/rest/AdminREST.java | 147 +++++++++++++++++++++
4 files changed, 299 insertions(+), 2 deletions(-)
diff --git a/intg/src/main/java/org/apache/ranger/RangerClient.java
b/intg/src/main/java/org/apache/ranger/RangerClient.java
index f75a9143f..3d18e007c 100644
--- a/intg/src/main/java/org/apache/ranger/RangerClient.java
+++ b/intg/src/main/java/org/apache/ranger/RangerClient.java
@@ -105,6 +105,7 @@ public class RangerClient {
private static final String URI_PLUGIN_INFO = URI_BASE +
"/plugins/info";
private static final String URI_POLICY_DELTAS = URI_BASE +
"/server/policydeltas";
private static final String URI_PURGE_RECORDS = URI_BASE +
"/server/purge/records";
+ private static final String URI_LOGGERS_SET_LEVEL =
"/service/admin/set-logger-level";
// APIs
public static final API CREATE_SERVICEDEF = new
API(URI_SERVICEDEF, HttpMethod.POST, Response.Status.OK);
@@ -165,6 +166,8 @@ public class RangerClient {
public static final API DELETE_POLICY_DELTAS = new API(URI_POLICY_DELTAS,
HttpMethod.DELETE, Response.Status.NO_CONTENT);
public static final API PURGE_RECORDS = new API(URI_PURGE_RECORDS,
HttpMethod.DELETE, Response.Status.OK);
+ public static final API SET_LOG_LEVEL = new
API(URI_LOGGERS_SET_LEVEL, HttpMethod.POST, Response.Status.OK);
+
private static final TypeReference<Void>
TYPE_VOID = new TypeReference<Void>() {};
private static final TypeReference<Set<String>>
TYPE_SET_STRING = new TypeReference<Set<String>>() {};
private static final TypeReference<List<String>>
TYPE_LIST_STRING = new TypeReference<List<String>>() {};
@@ -478,6 +481,23 @@ public List<RangerPurgeResult> purgeRecords(String
recordType, int retentionDays
return callAPI(PURGE_RECORDS, queryParams, null,
TYPE_LIST_PURGE_RESULT);
}
+ /**
+ * Sets the log level for a specific class or package.
+ * This operation requires ROLE_SYS_ADMIN role.
+ *
+ * @param loggerName The name of the logger (class or package name)
+ * @param logLevel The log level to set (TRACE, DEBUG, INFO, WARN, ERROR,
OFF)
+ * @return A message indicating the result of the operation
+ * @throws RangerServiceException if the operation fails
+ */
+ public String setLogLevel(String loggerName, String logLevel) throws
RangerServiceException {
+ Map<String, Object> requestData = new HashMap<>();
+ requestData.put("loggerName", loggerName);
+ requestData.put("logLevel", logLevel);
+
+ return callAPI(SET_LOG_LEVEL, null, requestData, new
TypeReference<String>() {});
+ }
+
private void authInit(String authType, String username, String password) {
if (AUTH_KERBEROS.equalsIgnoreCase(authType)) {
isSecureMode = true;
diff --git a/intg/src/main/python/apache_ranger/client/ranger_client.py
b/intg/src/main/python/apache_ranger/client/ranger_client.py
index f6e865107..405e80141 100644
--- a/intg/src/main/python/apache_ranger/client/ranger_client.py
+++ b/intg/src/main/python/apache_ranger/client/ranger_client.py
@@ -355,8 +355,21 @@ def delete_policy_deltas(self, days,
reloadServicePoliciesCache):
def purge_records(self, record_type, retention_days):
return self.client_http.call_api(RangerClient.PURGE_RECORDS, { 'type':
record_type, 'retentionDays': retention_days})
-
-
+ def set_log_level(self, logger_name, log_level):
+ """
+ Sets the log level for a specific class or package.
+ This operation requires ROLE_SYS_ADMIN role.
+
+ :param logger_name: The name of the logger (class or package name)
+ :param log_level: The log level to set (TRACE, DEBUG, INFO, WARN,
ERROR, OFF)
+ :return: A message indicating the result of the operation
+ :raises: RangerServiceException if the operation fails
+ """
+ request_data = {
+ 'loggerName': logger_name,
+ 'logLevel': log_level
+ }
+ return self.client_http.call_api(RangerClient.SET_LOG_LEVEL,
request_data=request_data)
# URIs
@@ -401,6 +414,7 @@ def purge_records(self, record_type, retention_days):
URI_PLUGIN_INFO = URI_BASE + "/plugins/info"
URI_POLICY_DELTAS = URI_BASE + "/server/policydeltas"
URI_PURGE_RECORDS = URI_BASE + "/server/purge/records"
+ URI_LOGGERS_SET_LEVEL = "service/admin/set-logger-level"
# APIs
CREATE_SERVICEDEF = API(URI_SERVICEDEF, HttpMethod.POST,
HTTPStatus.OK)
@@ -469,6 +483,7 @@ def purge_records(self, record_type, retention_days):
DELETE_POLICY_DELTAS = API(URI_POLICY_DELTAS, HttpMethod.DELETE,
HTTPStatus.NO_CONTENT)
PURGE_RECORDS = API(URI_PURGE_RECORDS, HttpMethod.DELETE,
HTTPStatus.OK)
+ SET_LOG_LEVEL = API(URI_LOGGERS_SET_LEVEL, HttpMethod.POST,
HTTPStatus.OK)
class HadoopSimpleAuth(AuthBase):
diff --git
a/security-admin/src/main/java/org/apache/ranger/biz/RangerLogLevelService.java
b/security-admin/src/main/java/org/apache/ranger/biz/RangerLogLevelService.java
new file mode 100644
index 000000000..b428755e6
--- /dev/null
+++
b/security-admin/src/main/java/org/apache/ranger/biz/RangerLogLevelService.java
@@ -0,0 +1,115 @@
+/*
+ * 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.ranger.biz;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * Service class to handle log level management operations.
+ * This class only supports Logback as the logging mechanism.
+ */
+@Component
+public class RangerLogLevelService {
+ private static final org.slf4j.Logger LOG =
LoggerFactory.getLogger(RangerLogLevelService.class);
+
+ private static final String LOGBACK_CLASSIC_PREFIX =
"ch.qos.logback.classic";
+
+ /**
+ * Sets the log level for a specific class or package.
+ * @param loggerName The name of the logger (class or package name)
+ * @param logLevel The log level to set (TRACE, DEBUG, INFO, WARN, ERROR,
OFF)
+ * @return A message indicating the result of the operation
+ * @throws IllegalArgumentException if the log level is invalid
+ * @throws UnsupportedOperationException if Logback is not the active
logging framework
+ */
+ public String setLogLevel(String loggerName, String logLevel) {
+ ILoggerFactory iLoggerFactory =
LoggerFactory.getILoggerFactory();
+ String loggerFactoryClassName =
iLoggerFactory.getClass().getName();
+
+ if (loggerFactoryClassName.startsWith(LOGBACK_CLASSIC_PREFIX)) {
+ LOG.info("Setting log level for logger '{}' to '{}'", loggerName,
logLevel);
+
+ return setLogbackLogLevel(loggerName, logLevel);
+ } else {
+ String message = "Logback is the only supported logging mechanism.
Detected unsupported SLF4J binding: " + loggerFactoryClassName;
+
+ LOG.error(message);
+
+ throw new UnsupportedOperationException(message);
+ }
+ }
+
+ /**
+ * Sets the Logback log level for a specific logger.
+ */
+ private String setLogbackLogLevel(String loggerName, String logLevel) {
+ try {
+ Level level = validateAndParseLogLevel(logLevel);
+ Logger logger = getLogger(loggerName);
+
+ logger.setLevel(level);
+
+ LOG.info("Successfully set log level for logger '{}' to '{}'",
loggerName, level);
+
+ return String.format("Log level for logger '%s' has been set to
'%s'", loggerName, level);
+ } catch (Exception e) {
+ LOG.error("Failed to set log level for logger '{}' to '{}'",
loggerName, logLevel, e);
+
+ throw new RuntimeException("Failed to set log level for logger '"
+ loggerName + "' to '" + logLevel + "'", e);
+ }
+ }
+
+ private static Logger getLogger(String loggerName) {
+ ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory();
+
+ if (!(iLoggerFactory instanceof LoggerContext)) {
+ throw new IllegalStateException("Expected ILoggerFactory to be an
instance of LoggerContext, but found " + iLoggerFactory.getClass().getName() +
". Is Logback configured as the SLF4J backend?");
+ }
+
+ LoggerContext context = (LoggerContext) iLoggerFactory;
+
+ // Get or create the logger
+ return context.getLogger(loggerName);
+ }
+
+ /**
+ * Validates and parses the log level string.
+ * @param logLevel The log level string to validate
+ * @return The corresponding Logback Level object
+ * @throws IllegalArgumentException if the log level is invalid
+ */
+ private Level validateAndParseLogLevel(String logLevel) {
+ if (StringUtils.isBlank(logLevel)) {
+ throw new IllegalArgumentException("Log level cannot be null or
empty");
+ }
+
+ try {
+ return Level.valueOf(logLevel.trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid log level: '" +
logLevel + "'. " + "Valid levels are: TRACE, DEBUG, INFO, WARN, ERROR, OFF");
+ }
+ }
+}
diff --git a/security-admin/src/main/java/org/apache/ranger/rest/AdminREST.java
b/security-admin/src/main/java/org/apache/ranger/rest/AdminREST.java
new file mode 100644
index 000000000..78ab34626
--- /dev/null
+++ b/security-admin/src/main/java/org/apache/ranger/rest/AdminREST.java
@@ -0,0 +1,147 @@
+/*
+ * 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.ranger.rest;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.ranger.biz.RangerLogLevelService;
+import org.apache.ranger.common.MessageEnums;
+import org.apache.ranger.common.RESTErrorUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Scope;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+
+@Path("admin")
+@Component
+@Scope("singleton")
+public class AdminREST {
+ private static final Logger LOG = LoggerFactory.getLogger(AdminREST.class);
+
+ @Inject
+ RangerLogLevelService logLevelService;
+
+ @Inject
+ RESTErrorUtil restErrorUtil;
+
+ /**
+ * An endpoint to set the log level for a specific class or package.
+ * This operation requires ROLE_SYS_ADMIN role as it affects system
logging behavior
+ * and can impact performance and security monitoring.
+ *
+ * @param request The request containing loggerName and logLevel
+ * @return An HTTP response indicating success or failure.
+ */
+ @POST
+ @Path("/set-logger-level")
+ @Consumes("application/json")
+ @Produces("application/json")
+ @PreAuthorize("hasRole('ROLE_SYS_ADMIN')")
+ public Response setLogLevel(LogLevelRequest request) {
+ try {
+ // Validate input parameters
+ if (request == null) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("Request body is required")
+ .build();
+ }
+
+ if (StringUtils.isBlank(request.getLoggerName())) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("loggerName is required")
+ .build();
+ }
+
+ if (StringUtils.isBlank(request.getLogLevel())) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("logLevel is required")
+ .build();
+ }
+
+ LOG.info("Setting log level for logger '{}' to '{}'",
request.getLoggerName(), request.getLogLevel());
+
+ String result =
logLevelService.setLogLevel(request.getLoggerName().trim(),
request.getLogLevel().trim());
+
+ return Response.ok(result).build();
+ } catch (IllegalArgumentException e) {
+ LOG.error("Invalid parameters for setting log level:", e);
+
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("Invalid parameters: " + e.getMessage())
+ .build();
+ } catch (UnsupportedOperationException e) {
+ LOG.error("Unsupported operation for setting log level:", e);
+
+ return Response.status(Response.Status.SERVICE_UNAVAILABLE)
+ .entity("Service not available: " + e.getMessage())
+ .build();
+ } catch (Exception e) {
+ LOG.error("Error setting log level for request: {}", request, e);
+
+ throw restErrorUtil.createRESTException(e.getMessage(),
MessageEnums.ERROR_SYSTEM);
+ }
+ }
+
+ /**
+ * Request class for JSON payload.
+ */
+ public static class LogLevelRequest {
+ private String loggerName;
+ private String logLevel;
+
+ public LogLevelRequest() {
+ // Default constructor for JSON deserialization
+ }
+
+ public LogLevelRequest(String loggerName, String logLevel) {
+ this.loggerName = loggerName;
+ this.logLevel = logLevel;
+ }
+
+ public String getLoggerName() {
+ return loggerName;
+ }
+
+ public void setLoggerName(String loggerName) {
+ this.loggerName = loggerName;
+ }
+
+ public String getLogLevel() {
+ return logLevel;
+ }
+
+ public void setLogLevel(String logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ @Override
+ public String toString() {
+ return "LogLevelRequest{" +
+ "loggerName='" + loggerName + '\'' +
+ ", logLevel='" + logLevel + '\'' +
+ '}';
+ }
+ }
+}