A simple MBean Server Facade

This service is a small wrapper around the platform MBean server within a JVM. The use case is to have a server side component which can query for the names within the MBeanServer for a given pattern and to retrieve the MBean Information for a given object name. All JMX specific data shall be mapped to appropriate classes, so that the data can be used later on with a simple read-only JMX REST service.

This leads to the following, simple interface definition:

Service Definition
trait Service {
/**
* Retrieve the information for a MBean by it's object name. Any JMX related information will be
* passed to the error channel without modification. If successful, A [[JmxBeanInfo]] will be
* returned.
*/
def mbeanInfo(objName: JmxObjectName): ZIO[Any, Throwable, JmxBeanInfo]
/**
* Retrieve the list of all MBeanNames known to the underlying MBean Server.
* Any JMX exception that might occur will be passed onwards in the error
* channel. If successful, a list of [[JmxObjectName]]s will be returned.
*/
def allMbeanNames(): ZIO[Any, Throwable, List[JmxObjectName]] = mbeanNames(None)
/**
* Retrieve the list of all object names know to the underlying MBean Server.
* Any JMX exception that might occur will be passed onwards in the error
* channel. If successful, a list of [[JmxObjectName]]s will be returned.
* @param objName If non-empty, the result will contain all object names that
* are in the same JMX domain and have all properties set within
* the parameter as additional name properties.
* If empty, no filtering will be applied.
*/
def mbeanNames(objName: Option[JmxObjectName]): ZIO[Any, Throwable, List[JmxObjectName]]
}
note

Even though the interface is defined without any environment restrictions, the actual live service requires that a Logging service is available. We have decided to push the requirement for a Logging service into the instantiation of the live service as we might come up with test instances at some point that should just mock up the interface and does not require any logging at all.

We will use the zio-logging API to perform the actual logging. See this post for more details on injecting different logging back-ends into the live service instance.

Querying for MBean Names#

To query for a set of MBean names with an optional is fairly straight forward wrapper around the original JMX API. We just have to translate from the case class we want to use in our API to a JMX search pattern and call the API.

def mbeanNames(objName: Option[JmxObjectName]): ZIO[Logging, Throwable, List[JmxObjectName]] = for {
pattern <- optionalPattern(objName)
_ <- doLog(LogLevel.Info)(s"Querying object names with [$pattern]")
names <- queryNames(pattern)
res = names.map(JmxObjectName.fromObjectName)
} yield res

In order to make the code a bit more readable, we encapsulate the translation within some helper methods abstracting over the case that the pattern may be optional. It might be that a single helper method to translate the pattern would have been sufficient, but in this case it seemed to improve the code's readability to have two helper methods.

The queryNames method performs the actual JMX call and translates the resulting Java object into a List of JmxObjectName

// Helper method to query the MBean Server for existing MBean names. If a pattern is given this will be used
// as a search pattern, otherwise all names will be returned
private def queryNames(pattern: Option[ObjectName]): ZIO[Any, Throwable, List[ObjectName]] = ZIO.effect {
val names: mutable.ListBuffer[ObjectName] = mutable.ListBuffer.empty[ObjectName]
svr.queryNames(pattern.orNull, null).forEach(n => names.append(n))
names.toList
}
// Helper method create an optional pattern
private def optionalPattern(name: Option[JmxObjectName]): ZIO[Any, Throwable, Option[ObjectName]] = name match {
case None => ZIO.none
case Some(n) => toPattern(n).map(Some(_))
}
// helper method to create a JMX search pattern from a given object name
private def toPattern(name: JmxObjectName): ZIO[Any, Throwable, ObjectName] =
ZIO.effect {
val props = name.sortedProps
new ObjectName(s"${name.domain}:${props.mkString(",")},*")
}

Retrieving MBean information#

The complicated part retrieving MBean information is to translate the attributes within the MBean information to an actual case class. We assume that the MBeans do have properties which are allowed for OpenMBeans.

If we encounter an attribute that is not valid as an attribute in the sense of the Open MBean specification, we will ignore that attribute in our mapping rather than throw an exception. As a result, some attributes may be missing for certain MBean Info objects.

The mapping between attributes and their case class representation happens within the JmxAttributeCompanion object. For the simple types this is straight forward:

case _: Unit => ZIO.effectTotal(UnitAttributeValue())
case s: String => ZIO.effectTotal(StringAttributeValue(s))
case i: java.lang.Integer => ZIO.effectTotal(IntAttributeValue(i))
case l: java.lang.Long => ZIO.effectTotal(LongAttributeValue(l))
case b: java.lang.Boolean => ZIO.effectTotal(BooleanAttributeValue(b))
case b: java.lang.Byte => ZIO.effectTotal(ByteAttributeValue(b))
case s: java.lang.Short => ZIO.effectTotal(ShortAttributeValue(s))
case f: java.lang.Float => ZIO.effectTotal(FloatAttributeValue(f))
case d: java.lang.Double => ZIO.effectTotal(DoubleAttributeValue(d))
case bi: java.math.BigInteger => ZIO.effectTotal(BigIntegerAtrributeValue(bi))
case bd: java.math.BigDecimal => ZIO.effectTotal(BigDecimalAtrributeValue(bd))

To map the complex data we will rely on the ZIO collectPar operator:

case t: TabularData =>
for {
values <- ZIO.collectPar(t.values().asScala)(v => make(v).mapError(t => Option(t)))
} yield TabularAttributeValue(values.toList)
case cd: CompositeData =>
for {
attrs <- ZIO.collectPar(cd.getCompositeType().keySet().asScala.toList) { k =>
ZIO.tupled(ZIO.succeed(k), make(cd.get(k))).mapError(t => Option(t))
}
} yield CompositeAttributeValue(attrs.toMap)

Note, that within JmxAttributeCompanion the overall signature is

def make(v: Any): ZIO[Any, IllegalArgumentException, AttributeValue[_]]

This means that the error handling is in the responsibility of the user of the make effect.

// Helper method to create a single JmxAttribute
private def mapAttribute(
on: ObjectName,
info: MBeanAttributeInfo
): ZIO[Any, Nothing, Map[String, AttributeValue[_]]] = (for {
attr <- ZIO.fromTry(Try {
svr.getAttribute(on, info.getName)
})
av <- JmxAttributeCompanion.make(attr)
} yield (Map(info.getName -> av))).orElse(ZIO.succeed(Map.empty[String, AttributeValue[_]]))

Here, the orElse operator will handle the error by just ignoring the attribute that was faulty.

Representation of an entire MBean Info#

The entire MBean Info object contains the JmxObjectName it belongs to and a Map of attribute names to their corresponding values. In other words, the attributes of a MBean Info can be represented by an instance of CompositeAttributeValue.

final case class JmxBeanInfo(
objName: JmxObjectName,
attributes: CompositeAttributeValue
)