Skip to content

Commit

Permalink
Support reporting of custom Log4J2 log levels from the LoggersEndpoint
Browse files Browse the repository at this point in the history
Support custom Log4J2 log levels by changing `LoggerConfiguration` so
that it can now report levels using a `LevelConfiguration` object
rather than the limited `LogLevel` enum.

The `Log4J2LoggingSystem` class now uses `LevelConfiguration.ofCustom`
for custom logging levels, rather than throwing an exception.

The `LoggersEndpoint` has also been updated so that it can return the
custom logger name.

Fixes gh-35227
  • Loading branch information
philwebb committed Jun 15, 2023
1 parent e779fb0 commit 137f4ee
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -31,6 +31,8 @@
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggerConfiguration;
import org.springframework.boot.logging.LoggerConfiguration.ConfigurationScope;
import org.springframework.boot.logging.LoggerConfiguration.LevelConfiguration;
import org.springframework.boot.logging.LoggerGroup;
import org.springframework.boot.logging.LoggerGroups;
import org.springframework.boot.logging.LoggingSystem;
Expand Down Expand Up @@ -124,10 +126,14 @@ private Map<String, LoggerLevels> getLoggers(Collection<LoggerConfiguration> con
*/
public static class LoggerLevels {

private String configuredLevel;
private final String configuredLevel;

public LoggerLevels(LogLevel configuredLevel) {
this.configuredLevel = getName(configuredLevel);
this.configuredLevel = (configuredLevel != null) ? configuredLevel.name() : null;
}

LoggerLevels(LevelConfiguration directConfiguration) {
this.configuredLevel = (directConfiguration != null) ? directConfiguration.getName() : null;
}

protected final String getName(LogLevel level) {
Expand All @@ -140,6 +146,9 @@ public String getConfiguredLevel() {

}

/**
* Levels configured for given logger group exposed in a JSON friendly way.
*/
public static class GroupLoggerLevels extends LoggerLevels {

private List<String> members;
Expand All @@ -155,13 +164,16 @@ public List<String> getMembers() {

}

/**
* Levels configured for single logger group exposed in a JSON friendly way.
*/
public static class SingleLoggerLevels extends LoggerLevels {

private String effectiveLevel;
private final String effectiveLevel;

public SingleLoggerLevels(LoggerConfiguration configuration) {
super(configuration.getConfiguredLevel());
this.effectiveLevel = getName(configuration.getEffectiveLevel());
super(configuration.getLevelConfiguration(ConfigurationScope.DIRECT));
this.effectiveLevel = configuration.getLevelConfiguration().getName();
}

public String getEffectiveLevel() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.springframework.boot.actuate.logging.LoggersEndpoint.SingleLoggerLevels;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggerConfiguration;
import org.springframework.boot.logging.LoggerConfiguration.LevelConfiguration;
import org.springframework.boot.logging.LoggerGroups;
import org.springframework.boot.logging.LoggingSystem;

Expand Down Expand Up @@ -109,6 +110,16 @@ void loggerLevelsWhenNameSpecifiedShouldReturnLevels() {
assertThat(levels.getEffectiveLevel()).isEqualTo("DEBUG");
}

@Test // gh-35227
void loggerLevelsWhenCustomLevelShouldReturnLevels() {
given(this.loggingSystem.getLoggerConfiguration("ROOT"))
.willReturn(new LoggerConfiguration("ROOT", null, LevelConfiguration.ofCustom("FINEST")));
SingleLoggerLevels levels = (SingleLoggerLevels) new LoggersEndpoint(this.loggingSystem, this.loggerGroups)
.loggerLevels("ROOT");
assertThat(levels.getConfiguredLevel()).isNull();
assertThat(levels.getEffectiveLevel()).isEqualTo("FINEST");
}

@Test
void groupNameSpecifiedShouldReturnConfiguredLevelAndMembers() {
GroupLoggerLevels levels = (GroupLoggerLevels) new LoggersEndpoint(this.loggingSystem, this.loggerGroups)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,22 +16,25 @@

package org.springframework.boot.logging;

import java.util.Objects;

import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
* Immutable class that represents the configuration of a {@link LoggingSystem}'s logger.
*
* @author Ben Hale
* @author Phillip Webb
* @since 1.5.0
*/
public final class LoggerConfiguration {

private final String name;

private final LogLevel configuredLevel;
private final LevelConfiguration levelConfiguration;

private final LogLevel effectiveLevel;
private final LevelConfiguration inheritedLevelConfiguration;

/**
* Create a new {@link LoggerConfiguration instance}.
Expand All @@ -43,67 +46,204 @@ public LoggerConfiguration(String name, LogLevel configuredLevel, LogLevel effec
Assert.notNull(name, "Name must not be null");
Assert.notNull(effectiveLevel, "EffectiveLevel must not be null");
this.name = name;
this.configuredLevel = configuredLevel;
this.effectiveLevel = effectiveLevel;
this.levelConfiguration = (configuredLevel != null) ? LevelConfiguration.of(configuredLevel) : null;
this.inheritedLevelConfiguration = LevelConfiguration.of(effectiveLevel);
}

/**
* Create a new {@link LoggerConfiguration instance}.
* @param name the name of the logger
* @param levelConfiguration the level configuration
* @param inheritedLevelConfiguration the inherited level configuration
* @since 2.7.13
*/
public LoggerConfiguration(String name, LevelConfiguration levelConfiguration,
LevelConfiguration inheritedLevelConfiguration) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(inheritedLevelConfiguration, "EffectiveLevelConfiguration must not be null");
this.name = name;
this.levelConfiguration = levelConfiguration;
this.inheritedLevelConfiguration = inheritedLevelConfiguration;
}

/**
* Returns the name of the logger.
* @return the name of the logger
*/
public String getName() {
return this.name;
}

/**
* Returns the configured level of the logger.
* @return the configured level of the logger
* @see #getLevelConfiguration(ConfigurationScope)
*/
public LogLevel getConfiguredLevel() {
return this.configuredLevel;
LevelConfiguration configuration = getLevelConfiguration(ConfigurationScope.DIRECT);
return (configuration != null) ? configuration.getLevel() : null;
}

/**
* Returns the effective level of the logger.
* @return the effective level of the logger
* @see #getLevelConfiguration(ConfigurationScope)
*/
public LogLevel getEffectiveLevel() {
return this.effectiveLevel;
return getLevelConfiguration().getLevel();
}

/**
* Returns the name of the logger.
* @return the name of the logger
* Return the level configuration, considering inherited loggers.
* @return the level configuration
* @since 2.7.13
*/
public String getName() {
return this.name;
public LevelConfiguration getLevelConfiguration() {
return getLevelConfiguration(ConfigurationScope.INHERITED);
}

/**
* Return the level configuration for the given scope.
* @param scope the configuration scope
* @return the level configuration or {@code null} for
* {@link ConfigurationScope#DIRECT direct scope} results without applied
* configuration
* @since 2.7.13
*/
public LevelConfiguration getLevelConfiguration(ConfigurationScope scope) {
return (scope != ConfigurationScope.DIRECT) ? this.inheritedLevelConfiguration : this.levelConfiguration;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
if (obj instanceof LoggerConfiguration) {
LoggerConfiguration other = (LoggerConfiguration) obj;
boolean rtn = true;
rtn = rtn && ObjectUtils.nullSafeEquals(this.name, other.name);
rtn = rtn && ObjectUtils.nullSafeEquals(this.configuredLevel, other.configuredLevel);
rtn = rtn && ObjectUtils.nullSafeEquals(this.effectiveLevel, other.effectiveLevel);
return rtn;
}
return super.equals(obj);
LoggerConfiguration other = (LoggerConfiguration) obj;
return ObjectUtils.nullSafeEquals(this.name, other.name)
&& ObjectUtils.nullSafeEquals(this.levelConfiguration, other.levelConfiguration)
&& ObjectUtils.nullSafeEquals(this.inheritedLevelConfiguration, other.inheritedLevelConfiguration);
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ObjectUtils.nullSafeHashCode(this.name);
result = prime * result + ObjectUtils.nullSafeHashCode(this.configuredLevel);
result = prime * result + ObjectUtils.nullSafeHashCode(this.effectiveLevel);
return result;
return Objects.hash(this.name, this.levelConfiguration, this.inheritedLevelConfiguration);
}

@Override
public String toString() {
return "LoggerConfiguration [name=" + this.name + ", configuredLevel=" + this.configuredLevel
+ ", effectiveLevel=" + this.effectiveLevel + "]";
return "LoggerConfiguration [name=" + this.name + ", levelConfiguration=" + this.levelConfiguration
+ ", inheritedLevelConfiguration=" + this.inheritedLevelConfiguration + "]";
}

/**
* Supported logger configurations scopes.
*
* @since 2.7.13
*/
public enum ConfigurationScope {

/**
* Only return configuration that has been applied directly applied. Often
* referred to as 'configured' or 'assigned' configuration.
*/
DIRECT,

/**
* May return configuration that has been applied to a parent logger. Often
* referred to as 'effective' configuration.
*/
INHERITED

}

/**
* Logger level configuration.
*
* @since 2.7.13
*/
public static final class LevelConfiguration {

private final String name;

private final LogLevel logLevel;

private LevelConfiguration(String name, LogLevel logLevel) {
this.name = name;
this.logLevel = logLevel;
}

/**
* Return the name of the level.
* @return the level name
*/
public String getName() {
return this.name;
}

/**
* Return the actual level value if possible.
* @return the level value
* @throws IllegalStateException if this is a {@link #isCustom() custom} level
*/
public LogLevel getLevel() {
Assert.state(this.logLevel != null, "Unable to provide LogLevel for '" + this.name + "'");
return this.logLevel;
}

/**
* Return if this is a custom level and cannot be represented by {@link LogLevel}.
* @return if this is a custom level
*/
public boolean isCustom() {
return this.logLevel == null;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
LevelConfiguration other = (LevelConfiguration) obj;
return this.logLevel == other.logLevel && ObjectUtils.nullSafeEquals(this.name, other.name);
}

@Override
public int hashCode() {
return Objects.hash(this.logLevel, this.name);
}

@Override
public String toString() {
return "LevelConfiguration [name=" + this.name + ", logLevel=" + this.logLevel + "]";
}

/**
* Create a new {@link LevelConfiguration} instance of the given {@link LogLevel}.
* @param logLevel the log level
* @return a new {@link LevelConfiguration} instance
*/
public static LevelConfiguration of(LogLevel logLevel) {
Assert.notNull(logLevel, "LogLevel must not be null");
return new LevelConfiguration(logLevel.name(), logLevel);
}

/**
* Create a new {@link LevelConfiguration} instance for a custom level name.
* @param name the log level name
* @return a new {@link LevelConfiguration} instance
*/
public static LevelConfiguration ofCustom(String name) {
Assert.hasText(name, "Name must not be empty");
return new LevelConfiguration(name, null);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
*
* @author HaiTao Zhang
* @author Phillip Webb
* @since 2.2.0 #see {@link LoggerGroup}
* @since 2.2.0
* @see LoggerGroup
*/
public final class LoggerGroups implements Iterable<LoggerGroup> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggerConfiguration;
import org.springframework.boot.logging.LoggerConfiguration.LevelConfiguration;
import org.springframework.boot.logging.LoggingInitializationContext;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemFactory;
Expand Down Expand Up @@ -362,13 +363,18 @@ private LoggerConfiguration convertLoggerConfig(String name, LoggerConfig logger
if (loggerConfig == null) {
return null;
}
LogLevel level = LEVELS.convertNativeToSystem(loggerConfig.getLevel());
LevelConfiguration effectiveLevelConfiguration = getLevelConfiguration(loggerConfig.getLevel());
if (!StringUtils.hasLength(name) || LogManager.ROOT_LOGGER_NAME.equals(name)) {
name = ROOT_LOGGER_NAME;
}
boolean isLoggerConfigured = loggerConfig.getName().equals(name);
LogLevel configuredLevel = (isLoggerConfigured) ? level : null;
return new LoggerConfiguration(name, configuredLevel, level);
boolean isAssigned = loggerConfig.getName().equals(name);
LevelConfiguration assignedLevelConfiguration = (!isAssigned) ? null : effectiveLevelConfiguration;
return new LoggerConfiguration(name, assignedLevelConfiguration, effectiveLevelConfiguration);
}

private LevelConfiguration getLevelConfiguration(Level level) {
LogLevel logLevel = LEVELS.convertNativeToSystem(level);
return (logLevel != null) ? LevelConfiguration.of(logLevel) : LevelConfiguration.ofCustom(level.name());
}

@Override
Expand Down
Loading

0 comments on commit 137f4ee

Please sign in to comment.