Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions intg/src/main/java/org/apache/ranger/RangerClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ 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 = "/service/loggers";
private static final String URI_LOGGERS_RELOAD = URI_LOGGERS + "/reload";
private static final String URI_LOGGERS_SET_LEVEL = URI_LOGGERS + "/set-level";

// APIs
public static final API CREATE_SERVICEDEF = new API(URI_SERVICEDEF, HttpMethod.POST, Response.Status.OK);
Expand Down Expand Up @@ -165,6 +168,9 @@ 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 RELOAD_LOG_CONFIGURATION = new API(URI_LOGGERS_RELOAD, HttpMethod.POST, 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>>() {};
Expand Down Expand Up @@ -478,6 +484,34 @@ public List<RangerPurgeResult> purgeRecords(String recordType, int retentionDays
return callAPI(PURGE_RECORDS, queryParams, null, TYPE_LIST_PURGE_RESULT);
}

/**
* Reloads the logging configuration from the logback properties file.
* This operation requires ROLE_SYS_ADMIN role.
*
* @return A message indicating the result of the operation
* @throws RangerServiceException if the operation fails
*/
public String reloadLogConfiguration() throws RangerServiceException {
return callAPI(RELOAD_LOG_CONFIGURATION, null, null, new TypeReference<String>() {});
}

/**
* 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;
Expand Down
30 changes: 29 additions & 1 deletion intg/src/main/python/apache_ranger/client/ranger_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,31 @@ 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 reload_log_configuration(self):
"""
Reloads the logging configuration from the logback properties file.
This operation requires ROLE_SYS_ADMIN role.

:return: A message indicating the result of the operation
:raises: RangerServiceException if the operation fails
"""
return self.client_http.call_api(RangerClient.RELOAD_LOG_CONFIGURATION)


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
Expand Down Expand Up @@ -401,6 +424,9 @@ 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 = "service/loggers"
URI_LOGGERS_RELOAD = URI_LOGGERS + "/reload"
URI_LOGGERS_SET_LEVEL = URI_LOGGERS + "/set-level"

# APIs
CREATE_SERVICEDEF = API(URI_SERVICEDEF, HttpMethod.POST, HTTPStatus.OK)
Expand Down Expand Up @@ -469,6 +495,8 @@ 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)

RELOAD_LOG_CONFIGURATION = API(URI_LOGGERS_RELOAD, HttpMethod.POST, HTTPStatus.OK)
SET_LOG_LEVEL = API(URI_LOGGERS_SET_LEVEL, HttpMethod.POST, HTTPStatus.OK)


class HadoopSimpleAuth(AuthBase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* 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 ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.net.URL;

/**
* Service class to handle logging-related 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);

/**
* Reloads the logging configuration using only Logback.
* Any other logging implementation will result in an error.
*/
public void reloadLogConfiguration() {
ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory();
String loggerFactoryClassName = iLoggerFactory.getClass().getName();

LOG.info("Detected SLF4J binding: {}", loggerFactoryClassName);

if (loggerFactoryClassName.startsWith("ch.qos.logback.classic")) {
reloadLogbackConfiguration();
} else {
String message = "Logback is the only supported logging mechanism. Detected unsupported SLF4J binding: " + loggerFactoryClassName;
LOG.error(message);
throw new UnsupportedOperationException(message);
}
}

/**
* 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();

LOG.info("Setting log level for logger '{}' to '{}'", loggerName, logLevel);

if (loggerFactoryClassName.startsWith("ch.qos.logback.classic")) {
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 {
// Validate log level
Level level = validateAndParseLogLevel(logLevel);

// Get the Logback LoggerContext
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

// Get or create the logger
Logger logger = context.getLogger(loggerName);

// Set the log level
logger.setLevel(level);

// Log the change
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);
}
}

/**
* 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 (logLevel == null || logLevel.trim().isEmpty()) {
throw new IllegalArgumentException("Log level cannot be null or empty");
}

String normalizedLevel = logLevel.trim().toUpperCase();

try {
return Level.valueOf(normalizedLevel);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid log level: '" + logLevel + "'. " +
"Valid levels are: TRACE, DEBUG, INFO, WARN, ERROR, OFF");
}
}

/**
* Reloads the Logback configuration using direct API calls.
*/
private void reloadLogbackConfiguration() {
try {
LOG.debug("Attempting to reload Logback configuration.");

LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
context.reset();

// Find the configuration file URL (e.g., logback.xml)
URL configUrl = this.getClass().getClassLoader().getResource("logback.xml");
if (configUrl == null) {
configUrl = this.getClass().getClassLoader().getResource("logback-test.xml");
}
if (configUrl == null) {
throw new RuntimeException("Could not find logback.xml or logback-test.xml on the classpath.");
}

// Configure using the found configuration file
configurator.doConfigure(configUrl);

LOG.info("Successfully triggered Logback configuration reload from {}.", configUrl);
} catch (JoranException e) {
// Logback-specific error handling
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
LOG.error("Failed to reload Logback configuration", e);
throw new RuntimeException("Failed to reload Logback configuration", e);
} catch (Exception e) {
LOG.error("Failed to reload Logback configuration", e);
throw new RuntimeException("Failed to reload Logback configuration", e);
}
}
}
Loading
Loading