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

JMX Metrics: Improved MBean and Instrument helpers #16

Merged
merged 4 commits into from
Oct 27, 2020
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
51 changes: 40 additions & 11 deletions contrib/jmx-metrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,11 @@ otel.jmx.password = my-password
##### `script.groovy` example

```groovy
import io.opentelemetry.common.Labels

def loadMatches = otel.queryJmx("org.apache.cassandra.metrics:type=Storage,name=Load")
def load = loadMatches.first()

def lvr = otel.longValueRecorder(
"cassandra.storage.load",
def storageLoadMBean = otel.mbean("org.apache.cassandra.metrics:type=Storage,name=Load")
otel.instrument(storageLoadMBean, "cassandra.storage.load",
"Size, in bytes, of the on disk data size this node manages",
"By", [myConstantLabelKey:"myConstantLabelValue"]
"By", "Count", otel.&longValueRecorder
)

lvr.record(load.Count, Labels.of("myKey", "myVal"))
```

As configured in the example, this metric gatherer will configure an otlp gRPC metric exporter
Expand All @@ -49,7 +42,7 @@ via `otel.jmx.groovy.script`, it will then run the script on the specified

- `otel.queryJmx(String objectNameStr)`
- This method will query the connected JMX application for the given `objectName`, which can
include wildcards. The return value will be a `List<GroovyMBean>` of zero or more
include wildcards. The return value will be a sorted `List<GroovyMBean>` of zero or more
[`GroovyMBean` objects](http://docs.groovy-lang.org/latest/html/api/groovy/jmx/GroovyMBean.html),
which are conveniently wrapped to make accessing attributes on the MBean simple.
See http://groovy-lang.org/jmx.html for more information about their usage.
Expand All @@ -58,6 +51,41 @@ via `otel.jmx.groovy.script`, it will then run the script on the specified
- This helper has the same functionality as its other signature, but takes an `ObjectName`
instance if constructing raw names is undesired.

### JMX `MBeanHelper` and `InstrumentHelper` Access Methods

- `otel.mbean(String objectNameStr)`
- This method will query for the given `objectNameStr` using `otel.queryJmx()` as previously described,
but returns an `MBeanHelper` instance representing the alphabetically first matching MBean for usage by
subsequent `InstrumentHelper` instances (available via `otel.instrument()`) as described below.

- `otel.mbeans(String objectNameStr)`
- This method will query for the given `objectNameStr` using `otel.queryJmx()` as previously described,
but returns an `MBeanHelper` instance representing all matching MBeans for usage by subsequent `InstrumentHelper`
instances (available via `otel.instrument()`) as described below.

- `otel.instrument(MBeanHelper mBeanHelper, String instrumentName, String description, String unit, Map<String, Closure> labelFuncs, String attribute, Closure instrument)`
- This method provides the ability to easily create and automatically update instrument instances from an
`MBeanHelper`'s underlying MBean instances via an OpenTelemetry instrument helper method pointer as described below.
- The method parameters map to those of the instrument helpers, while the new `Map<String, Closure> labelFuncs` will
be used to specify updated instrument labels that have access to the inspected MBean:

```groovy
// This example's resulting datapoint(s) will have Labels consisting of the specified key
// and a dynamically evaluated value from the GroovyMBean being examined.
[ "myLabelKey": { mbean -> mbean.name().getKeyProperty("myObjectNameProperty") } ]
```

- If the underlying MBean(s) held by the provided MBeanHelper are
[`CompositeData`](https://docs.oracle.com/javase/7/docs/api/javax/management/openmbean/CompositeData.html) instances,
each key of their `CompositeType` `keySet` will be `.`-appended to the specified `instrumentName`, whose resulting
instrument will be updated for each respective value.

`otel.instrument()` provides additional signatures to obtain and update the returned `InstrumentHelper`:

- `otel.instrument(MBeanHelper mBeanHelper, String name, String description, String unit, String attribute, Closure instrument)` - `labelFuncs` are empty map.
- `otel.instrument(MBeanHelper mBeanHelper, String name, String description, String attribute, Closure instrument)` - `unit` is "1" and `labelFuncs` are empty map.
- `otel.instrument(MBeanHelper mBeanHelper, String name, String attribute, Closure instrument)` - `description` is empty string, `unit` is "1" and `labelFuncs` are empty map.

### OpenTelemetry Synchronous Instrument Helpers

- `otel.doubleCounter(String name, String description, String unit, Map<String, String> labels)`
Expand Down Expand Up @@ -95,6 +123,7 @@ The currently available target systems are:

| `otel.jmx.target.system` |
| ------------------------ |
| [`jvm`](./docs/target-systems/jvm.md) |
| [`cassandra`](./docs/target-systems/cassandra.md) |

### Configuration
Expand Down
88 changes: 88 additions & 0 deletions contrib/jmx-metrics/docs/target-systems/jvm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# JVM Metrics

The JMX Metric Gatherer provides built in JVM metric gathering capabilities.
These metrics are sourced from Cassandra's exposed Dropwizard Metrics for each node: https://cassandra.apache.org/doc/latest/operating/metrics.html.

## Metrics

### Client Request Metrics

* Name: `jvm.classes.loaded`
* Description: The number of loaded classes
* Unit: `1`
* Instrument Type: LongUpDownCounter

* Name: `jvm.gc.collections.count`
* Description: The total number of garbage collections that have occurred
* Unit: `1`
* Instrument Type: LongCounter

* Name: `jvm.gc.collections.elapsed`
* Description: The approximate accumulated collection elapsed time
* Unit: `ms`
* Instrument Type: LongCounter

* Name: `jvm.memory.heap.init`
* Description: The initial amount of memory that the JVM requests from the operating system for the heap
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.heap.max`
* Description: The maximum amount of memory can be used for the heap
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.heap.used`
* Description: The current heap memory usage
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.heap.committed`
* Description: The amount of memory that is guaranteed to be available for the heap
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.nonheap.init`
* Description: The initial amount of memory that the JVM requests from the operating system for non-heap purposes
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.nonheap.max`
* Description: The maximum amount of memory can be used for non-heap purposes
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.nonheap.used`
* Description: The current non-heap memory usage
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.nonheap.committed`
* Description: The amount of memory that is guaranteed to be available for non-heap purposes
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.pool.init`
* Description: The initial amount of memory that the JVM requests from the operating system for the memory pool
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.pool.max`
* Description: The maximum amount of memory can be used for the memory pool
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.pool.used`
* Description: The current memory pool memory usage
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.memory.pool.committed`
* Description: The amount of memory that is guaranteed to be available for the memory pool
* Unit: `by`
* Instrument Type: LongUpDownCounter

* Name: `jvm.threads.count`
* Description: The current number of threads
* Unit: `1`
* Instrument Type: LongUpDownCounter
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed 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 io.opentelemetry.contrib.jmxmetrics

import io.opentelemetry.metrics.DoubleCounter
import io.opentelemetry.metrics.DoubleUpDownCounter
import io.opentelemetry.metrics.LongCounter
import io.opentelemetry.metrics.LongUpDownCounter
import java.util.logging.Logger
import javax.management.openmbean.CompositeData

/**
* A helper for easy instrument creation and updates based on an
* {@link MBeanHelper} attribute's value and passed {@link OtelHelper}
* instrument creator method pointer (e.g. &longCounter).
*
* Intended to be used via the script-bound `otel` {@link OtelHelper} instance methods:
*
* def threadCount = otel.instrument(myThreadingMBeanHelper,
* "jvm.threads.count", "number of threads",
* "1", [
* "myLabel": { mbean -> mbean.name().getKeyProperty("myObjectNameProperty") },
* "myOtherLabel": { "myLabelValue" }
* ], "ThreadCount", otel.&longUpDownCounter)
*
* threadCount.update()
*
* If the underlying MBean(s) held by the MBeanHelper are
* {@link CompositeData} instances, each key of their CompositeType's
* keySet will be .-appended to the specified instrumentName and
* updated for each respective value.
*/
class InstrumentHelper {
private static final Logger logger = Logger.getLogger(InstrumentHelper.class.getName());

private final MBeanHelper mBeanHelper
private final String instrumentName
private final String description
private final String unit
private final String attribute
private final Map<String, Closure> labelFuncs
private final Closure instrument

/**
* An InstrumentHelper provides the ability to easily create and update {@link io.opentelemetry.metrics.Instrument}
* instances from an MBeanHelper's underlying {@link GroovyMBean} instances via an {@link OtelHelper}'s instrument
* method pointer.
*
* @param mBeanHelper - the single or multiple {@link GroovyMBean} instance from which to access attribute values
* @param instrumentName - the resulting instruments' name to register.
* @param description - the resulting instruments' description to register.
* @param unit - the resulting instruments' unit to register.
* @param labelFuncs - A {@link Map<String, Closure>} of label names and values to be determined by custom
* {@link GroovyMBean}-provided Closures: (e.g. [ "myLabelName" : { mbean -> "myLabelValue"} ]). The
* resulting Label instances will be used for each individual update.
* @param attribute - The {@link GroovyMBean} attribute for which to use as the instrument value.
* @param instrument - The {@link io.opentelemetry.metrics.Instrument}-producing {@link OtelHelper} method pointer:
* (e.g. new OtelHelper().&doubleValueRecorder)
*/
InstrumentHelper(MBeanHelper mBeanHelper, String instrumentName, String description, String unit, Map<String, Closure> labelFuncs, String attribute, Closure instrument) {
this.mBeanHelper = mBeanHelper
this.instrumentName = instrumentName
this.description = description
this.unit = unit
this.labelFuncs = labelFuncs
this.attribute = attribute
this.instrument = instrument
}

void update() {
def mbeans = mBeanHelper.getMBeans()
def values = mBeanHelper.getAttribute(attribute)
if (values.size() == 0) {
logger.warning("No valid value(s) for ${instrumentName} - ${mBeanHelper}.${attribute}")
return
}

[mbeans, values].transpose().each { mbean, value ->
Copy link
Member

Choose a reason for hiding this comment

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

transpose 😎

if (value instanceof CompositeData) {
value.getCompositeType().keySet().each { key ->
def val = value.get(key)
def updatedInstrumentName = "${instrumentName}.${key}"
def labels = getLabels(mbean, labelFuncs)
def inst = instrument(updatedInstrumentName, description, unit)
logger.fine("Recording ${updatedInstrumentName} - ${inst} w/ ${val} - ${labels}")
updateInstrumentWithValue(inst, val, labels)
}
} else {
def labels = getLabels(mbean, labelFuncs)
def inst = instrument(instrumentName, description, unit)
logger.fine("Recording ${instrumentName} - ${inst} w/ ${value} - ${labels}")
updateInstrumentWithValue(inst, value, labels)
}
}
}

private static Map<String, String> getLabels(GroovyMBean mbean, Map<String, Closure> labelFuncs) {
def labels = [:]
labelFuncs.each { label, labelFunc ->
labels[label] = labelFunc(mbean) as String
}
return labels
}

private static void updateInstrumentWithValue(inst, value, labels) {
def labelMap = GroovyMetricEnvironment.mapToLabels(labels)
if (inst instanceof DoubleCounter
|| inst instanceof DoubleUpDownCounter
|| inst instanceof LongCounter
|| inst instanceof LongUpDownCounter) {
inst.add(value, labelMap)
} else {
inst.record(value, labelMap)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed 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 io.opentelemetry.contrib.jmxmetrics

import groovy.transform.PackageScope
import javax.management.MBeanServerConnection
import javax.management.ObjectName
import java.util.logging.Logger

/**
* A helper for easy {@link GroovyMBean} querying, creation, and protected attribute access.
*
* Basic query functionality is provided by the static queryJmx methods, while MBeanHelper
* instances operate on an underlying GroovyMBean list field.
*
* Specifying if a single underlying GroovyMBean is expected or of interest is done via the
* isSingle argument, where resulting attribute values are only returned for the first
* match.
*
* Intended to be used via the script-bound `otel` instance:
*
* def singleMBean = otel.mbean("com.example:type=SingleType")
* def multipleMBeans = otel.mbeans("com.example:type=MultipleType,*")
* [singleMBean, multipleMBeans].each { it.fetch() }
*
*/
class MBeanHelper {
private static final Logger logger = Logger.getLogger(MBeanHelper.class.getName());

private final JmxClient jmxClient
private final boolean isSingle
private final String objectName

private List<GroovyMBean> mbeans

MBeanHelper(JmxClient jmxClient, String objectName, boolean isSingle) {
this.jmxClient = jmxClient
pmcollins marked this conversation as resolved.
Show resolved Hide resolved
this.objectName = objectName
this.isSingle = isSingle
}

@PackageScope static List<GroovyMBean> queryJmx(JmxClient jmxClient, String objNameStr) {
return queryJmx(jmxClient, new ObjectName(objNameStr))
}

@PackageScope static List<GroovyMBean> queryJmx(JmxClient jmxClient, ObjectName objName) {
List<ObjectName> names = jmxClient.query(objName)
MBeanServerConnection server = jmxClient.connection
return names.collect { new GroovyMBean(server, it) }
}

void fetch() {
mbeans = queryJmx(jmxClient, objectName)
if (mbeans.size() == 0) {
logger.warning("Failed to fetch MBean ${objectName}.")
} else {
logger.fine("Fetched ${mbeans.size()} MBeans - ${mbeans}")
}
}

@PackageScope List<GroovyMBean> getMBeans() {
if (mbeans == null) {
logger.warning("No active MBeans. Be sure to fetch() before updating any applicable instruments.")
return []
}
return mbeans
}

@PackageScope List<Object> getAttribute(String attribute) {
if (mbeans == null || mbeans.size() == 0) {
return []
}

def ofInterest = isSingle ? [mbeans[0]]: mbeans
return ofInterest.collect {
it.getProperty(attribute)
}
}
}
Loading