Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #5872 - JMX DynamicMBean for jetty-slf4j-impl #5876

Merged
merged 4 commits into from
Feb 2, 2021
Merged
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
6 changes: 6 additions & 0 deletions jetty-jmx/src/main/config/etc/jetty-jmx.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
<Arg>
<Ref refid="MBeanServer" />
</Arg>
<Call name="beanAdded">
<Arg/>
<Arg>
<Get name="ILoggerFactory" class="org.slf4j.LoggerFactory"/>
</Arg>
</Call>
</New>
</Arg>
</Call>
Expand Down
1 change: 1 addition & 0 deletions jetty-slf4j-impl/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
{
exports org.eclipse.jetty.logging;

requires transitive java.management;
requires transitive org.slf4j;

provides SLF4JServiceProvider with JettyLoggingServiceProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,28 @@
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanConstructorInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.ReflectionException;

import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;

public class JettyLoggerFactory implements ILoggerFactory, JettyLoggerFactoryMBean
public class JettyLoggerFactory implements ILoggerFactory, DynamicMBean
{
private final JettyLoggerConfiguration configuration;
private final JettyLogger rootLogger;
private final ConcurrentMap<String, JettyLogger> loggerMap;
private MBeanInfo mBeanInfo;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this final and build it in the constructor (calling a method).
This would solve the problem of getMBeanInfo() possibly creating it twice when accessed concurrently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we create this MBeanInfo always? even for those users that don't actually use JMX?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!


public JettyLoggerFactory(JettyLoggerConfiguration config)
{
Expand Down Expand Up @@ -129,20 +142,17 @@ static <T> T walkParentLoggerNames(String startName, Function<String, T> nameFun
return nameFunction.apply(Logger.ROOT_LOGGER_NAME);
}

@Override
public String[] getLoggerNames()
{
TreeSet<String> names = new TreeSet<>(loggerMap.keySet());
return names.toArray(new String[0]);
}

@Override
public int getLoggerCount()
{
return loggerMap.size();
}

@Override
public String getLoggerLevel(String loggerName)
{
return walkParentLoggerNames(loggerName, key ->
Expand All @@ -154,7 +164,6 @@ public String getLoggerLevel(String loggerName)
});
}

@Override
public boolean setLoggerLevel(String loggerName, String levelName)
{
JettyLevel level = JettyLoggerConfiguration.toJettyLevel(loggerName, levelName);
Expand All @@ -166,4 +175,161 @@ public boolean setLoggerLevel(String loggerName, String levelName)
jettyLogger.setLevel(level);
return true;
}

@Override
public Object getAttribute(String name) throws AttributeNotFoundException
{
Objects.requireNonNull(name, "Attribute Name");

switch (name)
{
case "LoggerNames":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid loggerNames is also a valid value, so unfortunately we need if/else and equalsIgnoreCase().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name here and the name in MBeanInfo must match, case included.
You cannot access the MBean attribute in a case insensitive way.

Example:

MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName objectName = ObjectName.getInstance("java.lang", "type", "OperatingSystem");
System.out.printf("OSVer (normal): %s%n", mbeanServer.getAttribute(objectName, "Version"));
System.out.printf("OSVer (lowercase): %s%n", mbeanServer.getAttribute(objectName, "version"));

results in ...

OSVer (normal): 5.8.0-7630-generic
Exception in thread "main" javax.management.AttributeNotFoundException: No such attribute: version
	at java.management/com.sun.jmx.mbeanserver.PerInterface.getAttribute(PerInterface.java:81)
	at java.management/com.sun.jmx.mbeanserver.MBeanSupport.getAttribute(MBeanSupport.java:206)
	at java.management/javax.management.StandardMBean.getAttribute(StandardMBean.java:372)
	at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.getAttribute(DefaultMBeanServerInterceptor.java:641)
	at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.getAttribute(JmxMBeanServer.java:678)

return getLoggerNames();
case "LoggerCount":
return getLoggerCount();
default:
throw new AttributeNotFoundException("Cannot find " + name + " attribute in " + this.getClass().getName());
}
}

@Override
public void setAttribute(Attribute attribute) throws AttributeNotFoundException
{
Objects.requireNonNull(attribute, "attribute");
String name = attribute.getName();
// No attributes are writable
throw new AttributeNotFoundException("Cannot set attribute " + name + " because it is read-only");
}

@Override
public AttributeList getAttributes(String[] attributeNames)
{
Objects.requireNonNull(attributeNames, "attributeNames[]");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd tolerate null and return an empty list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is invalid per spec.
And no OpenJDK internal implementation supports null attributeNames.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is invalid per spec.

Where do you see this? It's not in DynamicMBean and I can point to implementations that tolerate null, for example MBeanServerDelegateImpl.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MBeanServerDelegateImpl.getAttributes(String[] attributes) returns all attributes when null is passed in.

Which is nice and all, but pointless.
As the lower level code in JMX would never even call this method if the String[] attributes is null.

MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName objectName = ObjectName.getInstance("java.lang", "type", "OperatingSystem");

AttributeList attributeList = mbeanServer.getAttributes(objectName, null);
for (Attribute attr : attributeList.asList())
{
    System.out.printf("Attribute[%s] = %s%n", attr.getName(), attr.getValue());
}

results in ...

Exception in thread "main" javax.management.RuntimeOperationsException: Exception occurred trying to invoke the getter on the MBean
	at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.getAttributes(DefaultMBeanServerInterceptor.java:660)
	at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.getAttributes(JmxMBeanServer.java:705)
	at jmx.JmxAccessDemo.main(JmxAccessDemo.java:16)
Caused by: java.lang.IllegalArgumentException: Attributes cannot be null
	... 8 more


AttributeList ret = new AttributeList();
if (attributeNames.length == 0)
return ret;

for (String name : attributeNames)
{
try
{
Object value = getAttribute(name);
ret.add(new Attribute(name, value));
}
catch (Exception e)
{
// nothing much we can do, this method has no throwables, and we cannot use logging here.
e.printStackTrace();
}
}
return ret;
}

@Override
public AttributeList setAttributes(AttributeList attributes)
{
Objects.requireNonNull(attributes, "attributes");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd tolerate null and do nothing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is supposed to error out, as null is invalid.
All implementations within OpenJDK will throw an exception with null (different exceptions, but they all throw)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MBeanServerDelegateImpl does not throw and returns an empty AttributeList.


AttributeList ret = new AttributeList();

if (attributes.isEmpty())
return ret;

for (Attribute attr : attributes.asList())
{
try
{
setAttribute(attr);
String name = attr.getName();
Object value = getAttribute(name);
ret.add(new Attribute(name, value));
}
catch (Exception e)
{
// nothing much we can do, this method has no throwables, and we cannot use logging here.
e.printStackTrace();
}
}
return ret;
}

@Override
public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, ReflectionException
{
Objects.requireNonNull(actionName, "Action Name");

switch (actionName)
{
case "setLoggerLevel":
{
String loggerName = (String)params[0];
String level = (String)params[1];
return setLoggerLevel(loggerName, level);
}
case "getLoggerLevel":
{
String loggerName = (String)params[0];
return getLoggerLevel(loggerName);
}
default:
throw new ReflectionException(
new NoSuchMethodException(actionName),
"Cannot find the operation " + actionName + " in " + this.getClass().getName());
}
}

@Override
public MBeanInfo getMBeanInfo()
{
if (mBeanInfo == null)
{
MBeanAttributeInfo[] attrs = new MBeanAttributeInfo[2];

attrs[0] = new MBeanAttributeInfo(
"LoggerCount",
"java.lang.Integer",
"Count of Registered Loggers by Name.",
true,
false,
false);
attrs[1] = new MBeanAttributeInfo(
"LoggerNames",
"java.lang.String[]",
"List of Registered Loggers by Name.",
true,
false,
false);

MBeanOperationInfo[] operations = new MBeanOperationInfo[]{
new MBeanOperationInfo(
"setLoggerLevel",
"Set the logging level at the named logger",
new MBeanParameterInfo[]{
new MBeanParameterInfo("loggerName", "java.lang.String", "The name of the logger"),
new MBeanParameterInfo("level", "java.lang.String", "The name of the level [DEBUG, INFO, WARN, ERROR]")
},
"boolean",
MBeanOperationInfo.ACTION
),
new MBeanOperationInfo(
"getLoggerLevel",
"Get the logging level at the named logger",
new MBeanParameterInfo[]{
new MBeanParameterInfo("loggerName", "java.lang.String", "The name of the logger")
},
"java.lang.String",
MBeanOperationInfo.INFO
)
};

mBeanInfo = new MBeanInfo(this.getClass().getName(),
"Jetty Slf4J Logger Factory",
attrs,
new MBeanConstructorInfo[0],
operations,
new MBeanNotificationInfo[0]);
}
return mBeanInfo;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import javax.management.JMX;
import java.util.stream.Stream;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand All @@ -35,34 +39,56 @@ public void testJMX() throws Exception
{
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();

Properties props = new Properties();
JettyLoggerConfiguration config = new JettyLoggerConfiguration(props);
JettyLoggerFactory loggerFactory = new JettyLoggerFactory(config);

ObjectName objectName = ObjectName.getInstance("org.eclipse.jetty.logging", "type", JettyLoggerFactory.class.getSimpleName().toLowerCase(Locale.ENGLISH));
mbeanServer.registerMBean(loggerFactory, objectName);
mbeanServer.registerMBean(LoggerFactory.getILoggerFactory(), objectName);

// Verify MBeanInfo
MBeanInfo beanInfo = mbeanServer.getMBeanInfo(objectName);

MBeanAttributeInfo[] attributeInfos = beanInfo.getAttributes();
assertThat("MBeanAttributeInfo count", attributeInfos.length, is(2));

JettyLoggerFactoryMBean mbean = JMX.newMBeanProxy(mbeanServer, objectName, JettyLoggerFactoryMBean.class);
MBeanAttributeInfo attr = Stream.of(attributeInfos).filter((a) -> a.getName().equals("LoggerNames")).findFirst().orElseThrow();
assertThat("attr", attr.getDescription(), is("List of Registered Loggers by Name."));

// Do some MBean attribute testing
int loggerCount;

// Only the root logger.
assertEquals(1, mbean.getLoggerCount());
loggerCount = (int)mbeanServer.getAttribute(objectName, "LoggerCount");
assertEquals(1, loggerCount);

JettyLoggerFactory loggerFactory = (JettyLoggerFactory)LoggerFactory.getILoggerFactory();
JettyLogger child = loggerFactory.getJettyLogger("org.eclipse.jetty.logging");
JettyLogger parent = loggerFactory.getJettyLogger("org.eclipse.jetty");
assertEquals(3, mbean.getLoggerCount());
loggerCount = (int)mbeanServer.getAttribute(objectName, "LoggerCount");
assertEquals(3, loggerCount);

// Names are sorted.
// Names from JMX are sorted, so lets sort our expected list too.
List<String> expected = new ArrayList<>(Arrays.asList(JettyLogger.ROOT_LOGGER_NAME, parent.getName(), child.getName()));
expected.sort(String::compareTo);
String[] loggerNames = mbean.getLoggerNames();
String[] loggerNames = (String[])mbeanServer.getAttribute(objectName, "LoggerNames");
assertEquals(expected, Arrays.asList(loggerNames));

// Do some MBean invoker testing
String operationName;
String[] signature;
Object[] params;

// Setting the parent level should propagate to the children.
parent.setLevel(JettyLevel.DEBUG);
assertEquals(parent.getLevel().toString(), mbean.getLoggerLevel(child.getName()));
operationName = "getLoggerLevel";
signature = new String[]{String.class.getName()};
params = new Object[]{child.getName()};
String levelName = (String)mbeanServer.invoke(objectName, operationName, params, signature);
assertEquals(parent.getLevel().toString(), levelName);

// Setting the level via JMX affects the logger.
assertTrue(mbean.setLoggerLevel(child.getName(), "INFO"));
operationName = "setLoggerLevel";
signature = new String[]{String.class.getName(), String.class.getName()};
params = new Object[]{child.getName(), "INFO"};
boolean result = (boolean)mbeanServer.invoke(objectName, operationName, params, signature);
assertTrue(result);
assertEquals(JettyLevel.INFO, child.getLevel());
}
}