This is an automated email from the ASF dual-hosted git repository.

madhan pushed a commit to branch ranger-2.8
in repository https://gitbox.apache.org/repos/asf/ranger.git

commit 9491343680664bfaa74f2e1e77c78331fb82cc59
Author: Fateh Singh <fateh...@gmail.com>
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)
    
    (cherry picked from commit 346fe34a41446cf29abefb627b61ab01376da4f3)
---
 .../main/java/org/apache/ranger/RangerClient.java  |  41 ++++--
 .../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, 309 insertions(+), 13 deletions(-)

diff --git a/intg/src/main/java/org/apache/ranger/RangerClient.java 
b/intg/src/main/java/org/apache/ranger/RangerClient.java
index a61c13fd2..eb38b4ac0 100644
--- a/intg/src/main/java/org/apache/ranger/RangerClient.java
+++ b/intg/src/main/java/org/apache/ranger/RangerClient.java
@@ -92,6 +92,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
@@ -153,6 +154,7 @@ 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>>() {};
@@ -170,17 +172,6 @@ public class RangerClient {
     private final RangerRESTClient restClient;
     private boolean isSecureMode     = false;
 
-    private void authInit(String authType, String username, String password) {
-        if (AUTH_KERBEROS.equalsIgnoreCase(authType)) {
-            isSecureMode = true;
-            MiscUtil.loginWithKeyTab(password, username, null);
-            UserGroupInformation ugi = MiscUtil.getUGILoginUser();
-            LOG.info("RangerClient.authInit() UGI user: " + ugi.getUserName() 
+ " principal: " + username);
-        } else {
-            restClient.setBasicAuthInfo(username, password);
-        }
-    }
-
     public RangerClient(String hostName, String authType, String username, 
String password, String configFile) {
         restClient = new RangerRESTClient(hostName, configFile, new 
Configuration());
         authInit(authType, username, password);
@@ -483,6 +474,34 @@ 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;
+            MiscUtil.loginWithKeyTab(password, username, null);
+            UserGroupInformation ugi = MiscUtil.getUGILoginUser();
+            LOG.info("RangerClient.authInit() UGI user: {} principal: {}", 
ugi.getUserName(), username);
+        } else {
+            restClient.setBasicAuthInfo(username, password);
+        }
+    }
+
     private ClientResponse invokeREST(API api, Map<String, String> params, 
Object request) throws RangerServiceException {
         final ClientResponse clientResponse;
         try {
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 + '\'' +
+                    '}';
+        }
+    }
+}

Reply via email to