metricbeat/module/jolokia/jmx/config.go (273 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. 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 jmx
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"sort"
"strings"
"github.com/elastic/elastic-agent-libs/mapstr"
)
type JMXMapping struct {
MBean string
Attributes []Attribute
Target Target
}
type Attribute struct {
Attr string
Field string
Event string
}
// Target inputs the value you want to set for jolokia target block
type Target struct {
URL string
User string
Password string
}
// RequestBlock is used to build the request blocks of the following format:
//
// [
//
// {
// "type":"read",
// "mbean":"java.lang:type=Runtime",
// "attribute":[
// "Uptime"
// ]
// },
// {
// "type":"read",
// "mbean":"java.lang:type=GarbageCollector,name=ConcurrentMarkSweep",
// "attribute":[
// "CollectionTime",
// "CollectionCount"
// ],
// "target":{
// "url":"service:jmx:rmi:///jndi/rmi://targethost:9999/jmxrmi",
// "user":"jolokia",
// "password":"s!cr!t"
// }
// }
//
// ]
type RequestBlock struct {
Type string `json:"type"`
MBean string `json:"mbean"`
Attribute []string `json:"attribute"`
Config map[string]interface{} `json:"config"`
Target *TargetBlock `json:"target,omitempty"`
}
// TargetBlock is used to build the target blocks of the following format into RequestBlock.
//
// "target":{
// "url":"service:jmx:rmi:///jndi/rmi://targethost:9999/jmxrmi",
// "user":"jolokia",
// "password":"s!cr!t"
// }
type TargetBlock struct {
URL string `json:"url"`
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
}
type attributeMappingKey struct {
mbean, attr string
}
// AttributeMapping contains the mapping information between attributes in Jolokia
// responses and fields in metricbeat events
type AttributeMapping map[attributeMappingKey]Attribute
// Get the mapping options for the attribute of an mbean
func (m AttributeMapping) Get(mbean, attr string) (Attribute, bool) {
a, found := m[attributeMappingKey{mbean, attr}]
return a, found
}
// MBeanName is an internal struct used to store
// the information by the parsed `mbean` (bean name) configuration
// field in `jmx.mappings`.
type MBeanName struct {
Domain string
Properties map[string]string
}
// Parse strings with properties with the format key=value, being:
// - Key a nonempty string of characters which may not contain any of the characters,
// comma (,), equals (=), colon, asterisk, or question mark.
// - Value a string that can be quoted or unquoted, if unquoted it cannot be empty and
// cannot contain any of the characters comma, equals, colon, or quote.
// If quoted, it can contain any character, including newlines, but quote needs to be
// escaped with a backslash.
var mbeanRegexp = regexp.MustCompile(`([^,=:*?]+)=([^,=:"]+|"([^\\"]|\\.)*?")`)
// This replacer is responsible for adding a "!" before special characters in GET request URIs
// For more information refer: https://jolokia.org/reference/html/protocol.html
var mbeanGetEscapeReplacer = strings.NewReplacer("\"", "!\"", ".", "!.", "!", "!!", "/", "!/")
// Canonicalize Returns the canonical form of the name; that is, a string representation where the
// properties are sorted in lexical order.
// The canonical form of the name is a String consisting of the domain part,
// a colon (:), the canonical key property list, and a pattern indication.
//
// For more information refer to Java 8 [getCanonicalName()](https://docs.oracle.com/javase/8/docs/api/javax/management/ObjectName.html#getCanonicalName--)
// method.
//
// Set "escape" parameter to true if you want to use the canonicalized name for a Jolokia HTTP GET request, false otherwise.
func (m *MBeanName) Canonicalize(escape bool) string {
var keySlice []string
for key := range m.Properties {
keySlice = append(keySlice, key)
}
sort.Strings(keySlice)
var propertySlice = make([]string, len(keySlice))
for i, key := range keySlice {
value := m.Properties[key]
tmpVal := value
if escape {
tmpVal = mbeanGetEscapeReplacer.Replace(value)
}
propertySlice[i] = key + "=" + tmpVal
}
return m.Domain + ":" + strings.Join(propertySlice, ",")
}
// ParseMBeanName is a factory function which parses a Managed Bean name string
// identified by mBeanName and returns a new MBean object which
// contains all the information, i.e. domain and properties of the MBean.
//
// The Mbean string has to abide by the rules which are imposed by Java.
// For more info: https://docs.oracle.com/javase/8/docs/api/javax/management/ObjectName.html#getCanonicalName--
func ParseMBeanName(mBeanName string) (*MBeanName, error) {
// Split mbean string in two parts: the bean domain and the properties
parts := strings.SplitN(mBeanName, ":", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return nil, fmt.Errorf("domain and properties needed in mbean name: %s", mBeanName)
}
// Create a new MBean object
mybean := &MBeanName{
Domain: parts[0],
}
// Using this regexp we will split the properties in a 2 dimensional array
// instead of just splitting by commas because values can be quoted
// and contain commas, what complicates the parsing.
// For example this MBean property string:
//
// name=HttpRequest1,type=RequestProcessor,worker="http-nio-8080"
//
// will become:
//
// [][]string{
// []string{"name=HttpRequest1", "name", "HttpRequest1"},
// []string{"type=RequestProcessor", "type", "RequestProcessor"},
// []string{"worker=\"http-nio-8080\"", "worker", "\"http-nio-8080\""}
// }
properties := mbeanRegexp.FindAllStringSubmatch(parts[1], -1)
if properties == nil {
return nil, fmt.Errorf("mbean properties must be in the form key=value: %s", mBeanName)
}
// Initialise properties map
mybean.Properties = make(map[string]string)
// Get the parsed string to check that everything has been parsed
var parsed []string
for _, prop := range properties {
// If every row does not have 3 columns, then
// parsing must have failed.
if (prop == nil) || (len(prop) < 3) {
// Some property didn't match
return nil, fmt.Errorf("mbean properties must be in the form key=value: %s", mBeanName)
}
parsed = append(parsed, prop[0])
mybean.Properties[prop[1]] = prop[2]
}
// Not all the properties definition has been parsed
if parsed := strings.Join(parsed, ","); len(parts[1]) != len(parsed) {
return nil, fmt.Errorf("not all properties could be parsed: %s, parsed: %s", mBeanName, parsed)
}
return mybean, nil
}
// JolokiaHTTPRequest is a small struct which contains all request information
// needed to construct a reqest helper.HTTP object which will be sent to Jolokia.
// It is just an intermediary structure which can be easily tested as helper.HTTP
// fields are all private.
type JolokiaHTTPRequest struct {
// HttpMethod can be either "GET" or "POST"
HTTPMethod string
// URI which will be used to query Jolokia
URI string
// Request body which is only filled if the http method is "POST"
Body []byte
}
// JolokiaHTTPRequestFetcher is an interface which describes
// the behaviour of the builder which generates,fetches the HTTP request,
// which is sent to Jolokia and then parses and maps the response to
// Metricbeat events.
type JolokiaHTTPRequestFetcher interface {
// BuildRequestsAndMappings builds the request information and mappings needed to fetch information from Jolokia server
BuildRequestsAndMappings(configMappings []JMXMapping) ([]*JolokiaHTTPRequest, AttributeMapping, error)
// Fetches the information from Jolokia server regarding MBeans
Fetch(m *MetricSet) ([]mapstr.M, error)
EventMapping(content []byte, mapping AttributeMapping) ([]mapstr.M, error)
}
// JolokiaHTTPGetFetcher constructs and executes an HTTP GET request
// which will read MBean information from Jolokia
type JolokiaHTTPGetFetcher struct {
}
// BuildRequestsAndMappings generates HTTP GET request
// such as URI,Body.
func (pc *JolokiaHTTPGetFetcher) BuildRequestsAndMappings(configMappings []JMXMapping) ([]*JolokiaHTTPRequest, AttributeMapping, error) {
// Create Jolokia URLs
uris, responseMapping, err := pc.buildGetRequestURIs(configMappings)
if err != nil {
return nil, nil, err
}
// Create one or more HTTP GET requests
var httpRequests []*JolokiaHTTPRequest
for _, i := range uris {
http := &JolokiaHTTPRequest{
HTTPMethod: "GET",
URI: i,
}
httpRequests = append(httpRequests, http)
}
return httpRequests, responseMapping, err
}
// Builds a GET URI which will have the following format:
//
// /read/<mbean>/<attribute>/[path]?ignoreErrors=true&canonicalNaming=false
func (pc *JolokiaHTTPGetFetcher) buildJolokiaGETUri(mbean string, attr []Attribute) string {
initialURI := "/read/%s?ignoreErrors=true&canonicalNaming=false"
var attrList []string
for _, attribute := range attr {
attrList = append(attrList, attribute.Attr)
}
tmpURL := mbean + "/" + strings.Join(attrList, ",")
tmpURL = fmt.Sprintf(initialURI, tmpURL)
return tmpURL
}
func (pc *JolokiaHTTPGetFetcher) mBeanAttributeHasField(attr *Attribute) bool {
if attr.Field != "" && (strings.Trim(attr.Field, " ") != "") {
return true
}
return false
}
func (pc *JolokiaHTTPGetFetcher) buildGetRequestURIs(mappings []JMXMapping) ([]string, AttributeMapping, error) {
responseMapping := make(AttributeMapping)
var urls []string
// At least Jolokia 1.5 responses with canonicalized MBean names when using
// wildcards, even when canonicalNaming is set to false, this makes mappings to fail.
// So use canonicalized names everywhere.
// If Jolokia returns non-canonicalized MBean names, then we'll need to canonicalize
// them or change our approach to mappings.
for _, mapping := range mappings {
mbean, err := ParseMBeanName(mapping.MBean)
if err != nil {
return urls, nil, err
}
if len(mapping.Target.URL) != 0 {
err := errors.New("Proxy requests are only valid when using POST method")
return urls, nil, err
}
// For every attribute we will build a response mapping
for _, attribute := range mapping.Attributes {
responseMapping[attributeMappingKey{mbean.Canonicalize(true), attribute.Attr}] = attribute
}
// Build a new URI for all attributes
urls = append(urls, pc.buildJolokiaGETUri(mbean.Canonicalize(true), mapping.Attributes))
}
return urls, responseMapping, nil
}
// Fetch perfrorms one or more GET requests to Jolokia server and gets information about MBeans.
func (pc *JolokiaHTTPGetFetcher) Fetch(m *MetricSet) ([]mapstr.M, error) {
var allEvents []mapstr.M
// Prepare Http request objects and attribute mappings according to selected Http method
httpReqs, mapping, err := pc.BuildRequestsAndMappings(m.mapping)
if err != nil {
return nil, err
}
// Log request information
for _, r := range httpReqs {
m.log.Debugw("Jolokia request URI and body",
"httpMethod", r.HTTPMethod, "URI", r.URI, "body", string(r.Body), "type", "request")
}
for _, r := range httpReqs {
m.http.SetMethod(r.HTTPMethod)
m.http.SetURI(m.BaseMetricSet.HostData().SanitizedURI + r.URI)
resBody, err := m.http.FetchContent()
if err != nil {
return nil, err
}
m.log.Debugw("Jolokia response body",
"host", m.HostData().Host, "uri", m.http.GetURI(), "body", string(resBody), "type", "response")
// Map response to Metricbeat events
events, err := pc.EventMapping(resBody, mapping)
if err != nil {
return nil, err
}
allEvents = append(allEvents, events...)
}
return allEvents, nil
}
// EventMapping maps a Jolokia response from a GET request is to one or more Metricbeat events
func (pc *JolokiaHTTPGetFetcher) EventMapping(content []byte, mapping AttributeMapping) ([]mapstr.M, error) {
var singleEntry Entry
// When we use GET, the response is a single Entry
if err := json.Unmarshal(content, &singleEntry); err != nil {
return nil, fmt.Errorf("failed to unmarshal jolokia JSON response '%v': %w", string(content), err)
}
return eventMapping([]Entry{singleEntry}, mapping)
}
// JolokiaHTTPPostFetcher constructs and executes an HTTP GET request
// which will read MBean information from Jolokia
type JolokiaHTTPPostFetcher struct {
}
// BuildRequestsAndMappings generates HTTP POST request
// such as URI,Body.
func (pc *JolokiaHTTPPostFetcher) BuildRequestsAndMappings(configMappings []JMXMapping) ([]*JolokiaHTTPRequest, AttributeMapping, error) {
body, mapping, err := pc.buildRequestBodyAndMapping(configMappings)
if err != nil {
return nil, nil, err
}
http := &JolokiaHTTPRequest{
HTTPMethod: "POST",
Body: body,
}
// Create an array with only one HTTP POST request
httpRequests := []*JolokiaHTTPRequest{http}
return httpRequests, mapping, nil
}
func (pc *JolokiaHTTPPostFetcher) buildRequestBodyAndMapping(mappings []JMXMapping) ([]byte, AttributeMapping, error) {
responseMapping := make(AttributeMapping)
var blocks []RequestBlock
// At least Jolokia 1.5 responses with canonicalized MBean names when using
// wildcards, even when canonicalNaming is set to false, this makes mappings to fail.
// So use canonicalized names everywhere.
// If Jolokia returns non-canonicalized MBean names, then we'll need to canonicalize
// them or change our approach to mappings.
config := map[string]interface{}{
"ignoreErrors": true,
"canonicalNaming": true,
}
for _, mapping := range mappings {
mbeanObj, err := ParseMBeanName(mapping.MBean)
if err != nil {
return nil, nil, err
}
mbean := mbeanObj.Canonicalize(false)
rb := RequestBlock{
Type: "read",
MBean: mbean,
Config: config,
}
if len(mapping.Target.URL) != 0 {
rb.Target = new(TargetBlock)
rb.Target.URL = mapping.Target.URL
rb.Target.User = mapping.Target.User
rb.Target.Password = mapping.Target.Password
}
for _, attribute := range mapping.Attributes {
rb.Attribute = append(rb.Attribute, attribute.Attr)
responseMapping[attributeMappingKey{mbean, attribute.Attr}] = attribute
}
blocks = append(blocks, rb)
}
content, err := json.Marshal(blocks)
return content, responseMapping, err
}
// Fetch perfrorms a POST request to Jolokia server and gets information about MBeans.
func (pc *JolokiaHTTPPostFetcher) Fetch(m *MetricSet) ([]mapstr.M, error) {
// Prepare Http POST request object and attribute mappings according to selected Http method
httpReqs, mapping, err := pc.BuildRequestsAndMappings(m.mapping)
if err != nil {
return nil, err
}
// Log request information
for _, r := range httpReqs {
m.log.Debugw("Jolokia request URI and body",
"httpMethod", r.HTTPMethod, "URI", m.http.GetURI(), "body", string(r.Body), "type", "request")
}
m.http.SetMethod(httpReqs[0].HTTPMethod)
m.http.SetBody(httpReqs[0].Body)
resBody, err := m.http.FetchContent()
if err != nil {
return nil, err
}
m.log.Debugw("Jolokia response body",
"host", m.HostData().Host, "uri", m.http.GetURI(), "body", string(resBody), "type", "response")
// Map response to Metricbeat events
events, err := pc.EventMapping(resBody, mapping)
if err != nil {
return nil, err
}
return events, nil
}
// EventMapping maps a Jolokia response from a POST request is to one or more Metricbeat events
func (pc *JolokiaHTTPPostFetcher) EventMapping(content []byte, mapping AttributeMapping) ([]mapstr.M, error) {
var entries []Entry
// When we use POST, the response is an array of Entry objects
if err := json.Unmarshal(content, &entries); err != nil {
return nil, fmt.Errorf("failed to unmarshal jolokia JSON response '%v': %w", string(content), err)
}
return eventMapping(entries, mapping)
}
// NewJolokiaHTTPRequestFetcher is a factory method which creates and returns an implementation
// class of JolokiaHTTPRequestFetcher interface. HTTP GET and POST are currently supported.
func NewJolokiaHTTPRequestFetcher(httpMethod string) JolokiaHTTPRequestFetcher {
if httpMethod == "GET" {
return &JolokiaHTTPGetFetcher{}
}
return &JolokiaHTTPPostFetcher{}
}