1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,275 @@
package jolokia2
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"time"
"github.com/influxdata/telegraf/plugins/common/tls"
)
type Client struct {
URL string
client *http.Client
config *ClientConfig
}
type ClientConfig struct {
ResponseTimeout time.Duration
Username string
Password string
Origin string
ProxyConfig *ProxyConfig
tls.ClientConfig
}
type ProxyConfig struct {
DefaultTargetUsername string
DefaultTargetPassword string
Targets []ProxyTargetConfig
}
type ProxyTargetConfig struct {
Username string
Password string
URL string
}
type ReadRequest struct {
Mbean string
Attributes []string
Path string
}
type ReadResponse struct {
Status int
Value interface{}
RequestMbean string
RequestAttributes []string
RequestPath string
RequestTarget string
}
// Jolokia JSON request object. Example: {
// "type": "read",
// "mbean: "java.lang:type="Runtime",
// "attribute": "Uptime",
// "target": {
// "url: "service:jmx:rmi:///jndi/rmi://target:9010/jmxrmi"
// }
// }
type jolokiaRequest struct {
Type string `json:"type"`
Mbean string `json:"mbean"`
Attribute interface{} `json:"attribute,omitempty"`
Path string `json:"path,omitempty"`
Target *jolokiaTarget `json:"target,omitempty"`
}
type jolokiaTarget struct {
URL string `json:"url"`
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
}
// Jolokia JSON response object. Example: {
// "request": {
// "type": "read"
// "mbean": "java.lang:type=Runtime",
// "attribute": "Uptime",
// "target": {
// "url": "service:jmx:rmi:///jndi/rmi://target:9010/jmxrmi"
// }
// },
// "value": 1214083,
// "timestamp": 1488059309,
// "status": 200
// }
type jolokiaResponse struct {
Request jolokiaRequest `json:"request"`
Value interface{} `json:"value"`
Status int `json:"status"`
}
func NewClient(address string, config *ClientConfig) (*Client, error) {
tlsConfig, err := config.ClientConfig.TLSConfig()
if err != nil {
return nil, err
}
transport := &http.Transport{
ResponseHeaderTimeout: config.ResponseTimeout,
TLSClientConfig: tlsConfig,
}
client := &http.Client{
Transport: transport,
Timeout: config.ResponseTimeout,
}
return &Client{
URL: address,
config: config,
client: client,
}, nil
}
func (c *Client) read(requests []ReadRequest) ([]ReadResponse, error) {
jRequests := makeJolokiaRequests(requests, c.config.ProxyConfig)
requestBody, err := json.Marshal(jRequests)
if err != nil {
return nil, err
}
requestURL, err := formatReadURL(c.URL, c.config.Username, c.config.Password)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(requestBody))
if err != nil {
// err is not contained in returned error - it may contain sensitive data (password) which should not be logged
return nil, fmt.Errorf("unable to create new request for: %q", c.URL)
}
req.Header.Add("Content-type", "application/json")
if c.config.Origin != "" {
req.Header.Add("Origin", c.config.Origin)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("response from url %q has status code %d (%s), expected %d (%s)",
c.URL, resp.StatusCode, http.StatusText(resp.StatusCode), http.StatusOK, http.StatusText(http.StatusOK))
}
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var jResponses []jolokiaResponse
if err = json.Unmarshal(responseBody, &jResponses); err != nil {
return nil, fmt.Errorf("decoding JSON response: %w: %s", err, responseBody)
}
return makeReadResponses(jResponses), nil
}
func makeJolokiaRequests(rrequests []ReadRequest, proxyConfig *ProxyConfig) []jolokiaRequest {
jrequests := make([]jolokiaRequest, 0)
if proxyConfig == nil {
for _, rr := range rrequests {
jrequests = append(jrequests, makeJolokiaRequest(rr, nil))
}
} else {
for _, t := range proxyConfig.Targets {
if t.Username == "" {
t.Username = proxyConfig.DefaultTargetUsername
}
if t.Password == "" {
t.Password = proxyConfig.DefaultTargetPassword
}
for _, rr := range rrequests {
jtarget := &jolokiaTarget{
URL: t.URL,
User: t.Username,
Password: t.Password,
}
jrequests = append(jrequests, makeJolokiaRequest(rr, jtarget))
}
}
}
return jrequests
}
func makeJolokiaRequest(rrequest ReadRequest, jtarget *jolokiaTarget) jolokiaRequest {
jrequest := jolokiaRequest{
Type: "read",
Mbean: rrequest.Mbean,
Path: rrequest.Path,
Target: jtarget,
}
if len(rrequest.Attributes) == 1 {
jrequest.Attribute = rrequest.Attributes[0]
}
if len(rrequest.Attributes) > 1 {
jrequest.Attribute = rrequest.Attributes
}
return jrequest
}
func makeReadResponses(jresponses []jolokiaResponse) []ReadResponse {
rresponses := make([]ReadResponse, 0)
for _, jr := range jresponses {
rrequest := ReadRequest{
Mbean: jr.Request.Mbean,
Path: jr.Request.Path,
Attributes: make([]string, 0),
}
attrValue := jr.Request.Attribute
if attrValue != nil {
attribute, ok := attrValue.(string)
if ok {
rrequest.Attributes = []string{attribute}
} else {
attributes, _ := attrValue.([]interface{})
rrequest.Attributes = make([]string, 0, len(attributes))
for _, attr := range attributes {
rrequest.Attributes = append(rrequest.Attributes, attr.(string))
}
}
}
rresponse := ReadResponse{
Value: jr.Value,
Status: jr.Status,
RequestMbean: rrequest.Mbean,
RequestAttributes: rrequest.Attributes,
RequestPath: rrequest.Path,
}
if jtarget := jr.Request.Target; jtarget != nil {
rresponse.RequestTarget = jtarget.URL
}
rresponses = append(rresponses, rresponse)
}
return rresponses
}
func formatReadURL(configURL, username, password string) (string, error) {
parsedURL, err := url.Parse(configURL)
if err != nil {
return "", err
}
readURL := url.URL{
Host: parsedURL.Host,
Scheme: parsedURL.Scheme,
}
if username != "" || password != "" {
readURL.User = url.UserPassword(username, password)
}
readURL.Path = path.Join(parsedURL.Path, "read")
readURL.Query().Add("ignoreErrors", "true")
return readURL.String(), nil
}

View file

@ -0,0 +1,266 @@
package jolokia2
import (
"fmt"
"sort"
"strings"
"github.com/influxdata/telegraf"
)
const defaultFieldName = "value"
type Gatherer struct {
metrics []Metric
requests []ReadRequest
}
func NewGatherer(metrics []Metric) *Gatherer {
return &Gatherer{
metrics: metrics,
requests: makeReadRequests(metrics),
}
}
// Gather adds points to an accumulator from responses returned
// by a Jolokia agent.
func (g *Gatherer) Gather(client *Client, acc telegraf.Accumulator) error {
var tags map[string]string
if client.config.ProxyConfig != nil {
tags = map[string]string{"jolokia_proxy_url": client.URL}
} else {
tags = map[string]string{"jolokia_agent_url": client.URL}
}
requests := makeReadRequests(g.metrics)
responses, err := client.read(requests)
if err != nil {
return err
}
g.gatherResponses(responses, tags, acc)
return nil
}
// gatherResponses adds points to an accumulator from the ReadResponse objects
// returned by a Jolokia agent.
func (g *Gatherer) gatherResponses(responses []ReadResponse, tags map[string]string, acc telegraf.Accumulator) {
series := make(map[string][]point)
for _, metric := range g.metrics {
points, ok := series[metric.Name]
if !ok {
points = make([]point, 0)
}
responsePoints, responseErrors := generatePoints(metric, responses)
points = append(points, responsePoints...)
for _, err := range responseErrors {
acc.AddError(err)
}
series[metric.Name] = points
}
for measurement, points := range series {
for _, point := range compactPoints(points) {
acc.AddFields(measurement,
point.Fields, mergeTags(point.Tags, tags))
}
}
}
// generatePoints creates points for the supplied metric from the ReadResponse objects returned by the Jolokia client.
func generatePoints(metric Metric, responses []ReadResponse) ([]point, []error) {
points := make([]point, 0)
errors := make([]error, 0)
for _, response := range responses {
switch response.Status {
case 200:
// Correct response status - do nothing.
case 404:
continue
default:
errors = append(errors, fmt.Errorf("unexpected status in response from target %s (%q): %d",
response.RequestTarget, response.RequestMbean, response.Status))
continue
}
if !metricMatchesResponse(metric, response) {
continue
}
pb := NewPointBuilder(metric, response.RequestAttributes, response.RequestPath)
ps, err := pb.Build(metric.Mbean, response.Value)
if err != nil {
errors = append(errors, err)
continue
}
for _, point := range ps {
if response.RequestTarget != "" {
point.Tags["jolokia_agent_url"] = response.RequestTarget
}
points = append(points, point)
}
}
return points, errors
}
// mergeTags combines two tag sets into a single tag set.
func mergeTags(metricTags, outerTags map[string]string) map[string]string {
tags := make(map[string]string)
for k, v := range outerTags {
tags[k] = strings.Trim(v, `'"`)
}
for k, v := range metricTags {
tags[k] = strings.Trim(v, `'"`)
}
return tags
}
// metricMatchesResponse returns true when the name, attributes, and path
// of a Metric match the corresponding elements in a ReadResponse object
// returned by a Jolokia agent.
func metricMatchesResponse(metric Metric, response ReadResponse) bool {
if !metric.MatchObjectName(response.RequestMbean) {
return false
}
if len(metric.Paths) == 0 {
return len(response.RequestAttributes) == 0
}
for _, attribute := range response.RequestAttributes {
if metric.MatchAttributeAndPath(attribute, response.RequestPath) {
return true
}
}
return false
}
// compactPoints attempts to remove points by compacting points
// with matching tag sets. When a match is found, the fields from
// one point are moved to another, and the empty point is removed.
func compactPoints(points []point) []point {
compactedPoints := make([]point, 0)
for _, sourcePoint := range points {
keepPoint := true
for _, compactPoint := range compactedPoints {
if !tagSetsMatch(sourcePoint.Tags, compactPoint.Tags) {
continue
}
keepPoint = false
for key, val := range sourcePoint.Fields {
compactPoint.Fields[key] = val
}
}
if keepPoint {
compactedPoints = append(compactedPoints, sourcePoint)
}
}
return compactedPoints
}
// tagSetsMatch returns true if two maps are equivalent.
func tagSetsMatch(a, b map[string]string) bool {
if len(a) != len(b) {
return false
}
for ak, av := range a {
bv, ok := b[ak]
if !ok {
return false
}
if av != bv {
return false
}
}
return true
}
// makeReadRequests creates ReadRequest objects from metrics definitions.
func makeReadRequests(metrics []Metric) []ReadRequest {
var requests []ReadRequest
for _, metric := range metrics {
if len(metric.Paths) == 0 {
requests = append(requests, ReadRequest{
Mbean: metric.Mbean,
Attributes: make([]string, 0),
})
} else {
attributes := make(map[string][]string)
for _, path := range metric.Paths {
segments := strings.Split(path, "/")
attribute := segments[0]
if _, ok := attributes[attribute]; !ok {
attributes[attribute] = make([]string, 0)
}
if len(segments) > 1 {
paths := attributes[attribute]
attributes[attribute] = append(paths, strings.Join(segments[1:], "/"))
}
}
rootAttributes := findRequestAttributesWithoutPaths(attributes)
if len(rootAttributes) > 0 {
requests = append(requests, ReadRequest{
Mbean: metric.Mbean,
Attributes: rootAttributes,
})
}
for _, deepAttribute := range findRequestAttributesWithPaths(attributes) {
for _, path := range attributes[deepAttribute] {
requests = append(requests, ReadRequest{
Mbean: metric.Mbean,
Attributes: []string{deepAttribute},
Path: path,
})
}
}
}
}
return requests
}
func findRequestAttributesWithoutPaths(attributes map[string][]string) []string {
results := make([]string, 0)
for attr, paths := range attributes {
if len(paths) == 0 {
results = append(results, attr)
}
}
sort.Strings(results)
return results
}
func findRequestAttributesWithPaths(attributes map[string][]string) []string {
results := make([]string, 0)
for attr, paths := range attributes {
if len(paths) != 0 {
results = append(results, attr)
}
}
sort.Strings(results)
return results
}

View file

@ -0,0 +1,104 @@
package jolokia2
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestJolokia2_makeReadRequests(t *testing.T) {
cases := []struct {
metric Metric
expected []ReadRequest
}{
{
metric: Metric{
Name: "object",
Mbean: "test:foo=bar",
},
expected: []ReadRequest{
{
Mbean: "test:foo=bar",
Attributes: make([]string, 0),
},
},
}, {
metric: Metric{
Name: "object_with_an_attribute",
Mbean: "test:foo=bar",
Paths: []string{"biz"},
},
expected: []ReadRequest{
{
Mbean: "test:foo=bar",
Attributes: []string{"biz"},
},
},
}, {
metric: Metric{
Name: "object_with_attributes",
Mbean: "test:foo=bar",
Paths: []string{"baz", "biz"},
},
expected: []ReadRequest{
{
Mbean: "test:foo=bar",
Attributes: []string{"baz", "biz"},
},
},
}, {
metric: Metric{
Name: "object_with_an_attribute_and_path",
Mbean: "test:foo=bar",
Paths: []string{"biz/baz"},
},
expected: []ReadRequest{
{
Mbean: "test:foo=bar",
Attributes: []string{"biz"},
Path: "baz",
},
},
}, {
metric: Metric{
Name: "object_with_an_attribute_and_a_deep_path",
Mbean: "test:foo=bar",
Paths: []string{"biz/baz/fiz/faz"},
},
expected: []ReadRequest{
{
Mbean: "test:foo=bar",
Attributes: []string{"biz"},
Path: "baz/fiz/faz",
},
},
}, {
metric: Metric{
Name: "object_with_attributes_and_paths",
Mbean: "test:foo=bar",
Paths: []string{"baz/biz", "faz/fiz"},
},
expected: []ReadRequest{
{
Mbean: "test:foo=bar",
Attributes: []string{"baz"},
Path: "biz",
},
{
Mbean: "test:foo=bar",
Attributes: []string{"faz"},
Path: "fiz",
},
},
},
}
for _, c := range cases {
payload := makeReadRequests([]Metric{c.metric})
require.Len(t, payload, len(c.expected), "Failing case: "+c.metric.Name)
for _, actual := range payload {
require.Contains(t, c.expected, actual, "Failing case: "+c.metric.Name)
}
}
}

View file

@ -0,0 +1,128 @@
package jolokia2
import "strings"
// A MetricConfig represents a TOML form of
// a Metric with some optional fields.
type MetricConfig struct {
Name string
Mbean string
Paths []string
FieldName *string
FieldPrefix *string
FieldSeparator *string
TagPrefix *string
TagKeys []string
}
// A Metric represents a specification for a
// Jolokia read request, and the transformations
// to apply to points generated from the responses.
type Metric struct {
Name string
Mbean string
Paths []string
FieldName string
FieldPrefix string
FieldSeparator string
TagPrefix string
TagKeys []string
mbeanDomain string
mbeanProperties []string
}
func NewMetric(config MetricConfig, defaultFieldPrefix, defaultFieldSeparator, defaultTagPrefix string) Metric {
metric := Metric{
Name: config.Name,
Mbean: config.Mbean,
Paths: config.Paths,
TagKeys: config.TagKeys,
}
if config.FieldName != nil {
metric.FieldName = *config.FieldName
}
if config.FieldPrefix == nil {
metric.FieldPrefix = defaultFieldPrefix
} else {
metric.FieldPrefix = *config.FieldPrefix
}
if config.FieldSeparator == nil {
metric.FieldSeparator = defaultFieldSeparator
} else {
metric.FieldSeparator = *config.FieldSeparator
}
if config.TagPrefix == nil {
metric.TagPrefix = defaultTagPrefix
} else {
metric.TagPrefix = *config.TagPrefix
}
mbeanDomain, mbeanProperties := parseMbeanObjectName(config.Mbean)
metric.mbeanDomain = mbeanDomain
metric.mbeanProperties = mbeanProperties
return metric
}
func (m Metric) MatchObjectName(name string) bool {
if name == m.Mbean {
return true
}
mbeanDomain, mbeanProperties := parseMbeanObjectName(name)
if mbeanDomain != m.mbeanDomain {
return false
}
if len(mbeanProperties) != len(m.mbeanProperties) {
return false
}
NEXT_PROPERTY:
for _, mbeanProperty := range m.mbeanProperties {
for i := range mbeanProperties {
if mbeanProperties[i] == mbeanProperty {
continue NEXT_PROPERTY
}
}
return false
}
return true
}
func (m Metric) MatchAttributeAndPath(attribute, innerPath string) bool {
path := attribute
if innerPath != "" {
path = path + "/" + innerPath
}
for i := range m.Paths {
if path == m.Paths[i] {
return true
}
}
return false
}
func parseMbeanObjectName(name string) (string, []string) {
index := strings.Index(name, ":")
if index == -1 {
return name, nil
}
domain := name[:index]
if index+1 > len(name) {
return domain, nil
}
return domain, strings.Split(name[index+1:], ",")
}

View file

@ -0,0 +1,274 @@
package jolokia2
import (
"fmt"
"strings"
)
type point struct {
Tags map[string]string
Fields map[string]interface{}
}
type pointBuilder struct {
metric Metric
objectAttributes []string
objectPath string
substitutions []string
}
func NewPointBuilder(metric Metric, attributes []string, path string) *pointBuilder {
return &pointBuilder{
metric: metric,
objectAttributes: attributes,
objectPath: path,
substitutions: makeSubstitutionList(metric.Mbean),
}
}
// Build generates a point for a given mbean name/pattern and value object.
func (pb *pointBuilder) Build(mbean string, value interface{}) ([]point, error) {
hasPattern := strings.Contains(mbean, "*")
if !hasPattern || value == nil {
value = map[string]interface{}{mbean: value}
}
valueMap, ok := value.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("the response of %s's value should be a map", mbean)
}
points := make([]point, 0)
for mbean, value := range valueMap {
points = append(points, point{
Tags: pb.extractTags(mbean),
Fields: pb.extractFields(mbean, value),
})
}
return compactPoints(points), nil
}
// extractTags generates the map of tags for a given mbean name/pattern.
func (pb *pointBuilder) extractTags(mbean string) map[string]string {
propertyMap := makePropertyMap(mbean)
tagMap := make(map[string]string)
for key, value := range propertyMap {
if pb.includeTag(key) {
tagName := pb.formatTagName(key)
tagMap[tagName] = value
}
}
return tagMap
}
func (pb *pointBuilder) includeTag(tagName string) bool {
for _, t := range pb.metric.TagKeys {
if tagName == t {
return true
}
}
return false
}
func (pb *pointBuilder) formatTagName(tagName string) string {
if tagName == "" {
return ""
}
if tagPrefix := pb.metric.TagPrefix; tagPrefix != "" {
return tagPrefix + tagName
}
return tagName
}
// extractFields generates the map of fields for a given mbean name
// and value object.
func (pb *pointBuilder) extractFields(mbean string, value interface{}) map[string]interface{} {
fieldMap := make(map[string]interface{})
valueMap, ok := value.(map[string]interface{})
if ok {
// complex value
if len(pb.objectAttributes) == 0 {
// if there were no attributes requested,
// then the keys are attributes
pb.FillFields("", valueMap, fieldMap)
} else if len(pb.objectAttributes) == 1 {
// if there was a single attribute requested,
// then the keys are the attribute's properties
fieldName := pb.formatFieldName(pb.objectAttributes[0], pb.objectPath)
pb.FillFields(fieldName, valueMap, fieldMap)
} else {
// if there were multiple attributes requested,
// then the keys are the attribute names
for _, attribute := range pb.objectAttributes {
fieldName := pb.formatFieldName(attribute, pb.objectPath)
pb.FillFields(fieldName, valueMap[attribute], fieldMap)
}
}
} else {
// scalar value
var fieldName string
if len(pb.objectAttributes) == 0 {
fieldName = pb.formatFieldName(defaultFieldName, pb.objectPath)
} else {
fieldName = pb.formatFieldName(pb.objectAttributes[0], pb.objectPath)
}
pb.FillFields(fieldName, value, fieldMap)
}
if len(pb.substitutions) > 1 {
pb.applySubstitutions(mbean, fieldMap)
}
return fieldMap
}
// formatFieldName generates a field name from the supplied attribute and
// path. The return value has the configured FieldPrefix and FieldSuffix
// instructions applied.
func (pb *pointBuilder) formatFieldName(attribute, path string) string {
fieldName := attribute
fieldPrefix := pb.metric.FieldPrefix
fieldSeparator := pb.metric.FieldSeparator
if fieldPrefix != "" {
fieldName = fieldPrefix + fieldName
}
if path != "" {
fieldName = fieldName + fieldSeparator + strings.ReplaceAll(path, "/", fieldSeparator)
}
return fieldName
}
// FillFields recurses into the supplied value object, generating a named field
// for every value it discovers.
func (pb *pointBuilder) FillFields(name string, value interface{}, fieldMap map[string]interface{}) {
if valueMap, ok := value.(map[string]interface{}); ok {
// keep going until we get to something that is not a map
for key, innerValue := range valueMap {
if _, ok := innerValue.([]interface{}); ok {
continue
}
var innerName string
if name == "" {
innerName = pb.metric.FieldPrefix + key
} else {
innerName = name + pb.metric.FieldSeparator + key
}
pb.FillFields(innerName, innerValue, fieldMap)
}
return
}
if _, ok := value.([]interface{}); ok {
return
}
if pb.metric.FieldName != "" {
name = pb.metric.FieldName
if prefix := pb.metric.FieldPrefix; prefix != "" {
name = prefix + name
}
}
if name == "" {
name = defaultFieldName
}
fieldMap[name] = value
}
// applySubstitutions updates all the keys in the supplied map
// of fields to account for $1-style substitution instructions.
func (pb *pointBuilder) applySubstitutions(mbean string, fieldMap map[string]interface{}) {
properties := makePropertyMap(mbean)
for i, subKey := range pb.substitutions[1:] {
symbol := fmt.Sprintf("$%d", i+1)
substitution := properties[subKey]
for fieldName, fieldValue := range fieldMap {
newFieldName := strings.ReplaceAll(fieldName, symbol, substitution)
if fieldName != newFieldName {
fieldMap[newFieldName] = fieldValue
delete(fieldMap, fieldName)
}
}
}
}
// makePropertyMap returns a the mbean property-key list as
// a dictionary. foo:x=y becomes map[string]string { "x": "y" }
func makePropertyMap(mbean string) map[string]string {
props := make(map[string]string)
object := strings.SplitN(mbean, ":", 2)
domain := object[0]
if domain != "" && len(object) == 2 {
list := object[1]
for _, keyProperty := range strings.Split(list, ",") {
pair := strings.SplitN(keyProperty, "=", 2)
if len(pair) != 2 {
continue
}
if key := pair[0]; key != "" {
props[key] = pair[1]
}
}
}
return props
}
// makeSubstitutionList returns an array of values to
// use as substitutions when renaming fields
// with the $1..$N syntax. The first item in the list
// is always the mbean domain.
func makeSubstitutionList(mbean string) []string {
subs := make([]string, 0)
object := strings.SplitN(mbean, ":", 2)
domain := object[0]
if domain != "" && len(object) == 2 {
subs = append(subs, domain)
list := object[1]
for _, keyProperty := range strings.Split(list, ",") {
pair := strings.SplitN(keyProperty, "=", 2)
if len(pair) != 2 {
continue
}
key := pair[0]
if key == "" {
continue
}
property := pair[1]
if !strings.Contains(property, "*") {
continue
}
subs = append(subs, key)
}
}
return subs
}