Files
MyGoNavi/internal/jvm/jmx_helper.go
Syngnat 9d08b185d0 feat(jvm): 新增持续监控与采样链路
- 后端新增监控会话管理,支持启动、停止和历史查询

- JMX、Endpoint、Agent Provider 补齐监控快照采集能力

- JMX helper 增加内存、GC、线程、类加载采样并更新内嵌运行时

- 生成 Wails 监控接口绑定并补充后端回归测试
2026-04-26 14:33:41 +08:00

840 lines
27 KiB
Go

package jvm
import (
"bytes"
"context"
"crypto/sha256"
_ "embed"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"GoNavi-Wails/internal/connection"
)
const (
jmxResourceScheme = "jmx"
jmxResourceKindRoot = "root"
jmxResourceKindDomain = "domain"
jmxResourceKindMBean = "mbean"
jmxResourceKindAttribute = "attribute"
jmxResourceKindOperation = "operation"
jmxHelperCommandPing = "ping"
jmxHelperCommandList = "list"
jmxHelperCommandGet = "get"
jmxHelperCommandMonitor = "monitor"
jmxHelperCommandPreview = "preview"
jmxHelperCommandApply = "apply"
jmxHelperMainClass = "com.gonavi.jmxhelper.JmxHelperMain"
)
var (
jmxHelperCompileMu sync.Mutex
jmxHelperCommandContext = exec.CommandContext
jmxHelperLookPath = exec.LookPath
)
var (
jmxHelperSensitiveJSONFieldPattern = regexp.MustCompile(`(?i)("(?:password|apiKey|token|secret)"\s*:\s*")([^"]*)(")`)
jmxHelperSensitivePairPattern = regexp.MustCompile(`(?i)\b(password|api[_-]?key|token|secret)(\s*[:=]\s*)([^&\s;,"'}]+)`)
)
//go:embed jmxhelper_assets/jmx-helper-runtime.jar
var embeddedJMXHelperJar []byte
type jmxResourceTarget struct {
Kind string
Domain string
ObjectName string
Attribute string
Operation string
Signature []string
}
type jmxHelperRuntime struct {
javaBinary string
classpath string
}
type jmxHelperRequest struct {
Command string `json:"command"`
Connection jmxHelperConnection `json:"connection"`
Target *jmxHelperTarget `json:"target,omitempty"`
Change *jmxHelperChangePlan `json:"change,omitempty"`
}
type jmxHelperConnection struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
DomainAllowlist []string `json:"domainAllowlist,omitempty"`
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
}
type jmxHelperTarget struct {
Kind string `json:"kind"`
Domain string `json:"domain,omitempty"`
ObjectName string `json:"objectName,omitempty"`
Attribute string `json:"attribute,omitempty"`
Operation string `json:"operation,omitempty"`
Signature []string `json:"signature,omitempty"`
}
type jmxHelperChangePlan struct {
Action string `json:"action,omitempty"`
Reason string `json:"reason,omitempty"`
ExpectedVersion string `json:"expectedVersion,omitempty"`
Payload map[string]any `json:"payload,omitempty"`
}
type jmxHelperResponse struct {
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Details map[string]any `json:"details,omitempty"`
Resources []jmxHelperResource `json:"resources,omitempty"`
Snapshot *jmxHelperSnapshot `json:"snapshot,omitempty"`
MonitoringSnapshot *jmxHelperMonitoringSnapshot `json:"monitoringSnapshot,omitempty"`
Preview *jmxHelperPreview `json:"preview,omitempty"`
ApplyResult *jmxHelperApplyResponse `json:"applyResult,omitempty"`
}
type jmxHelperResource struct {
Kind string `json:"kind"`
Domain string `json:"domain,omitempty"`
ObjectName string `json:"objectName,omitempty"`
Attribute string `json:"attribute,omitempty"`
Operation string `json:"operation,omitempty"`
Signature []string `json:"signature,omitempty"`
Name string `json:"name"`
CanRead bool `json:"canRead"`
CanWrite bool `json:"canWrite"`
HasChildren bool `json:"hasChildren"`
Sensitive bool `json:"sensitive,omitempty"`
}
type jmxHelperSnapshot struct {
Kind string `json:"kind"`
Format string `json:"format"`
Value any `json:"value"`
Description string `json:"description,omitempty"`
Sensitive bool `json:"sensitive,omitempty"`
SupportedActions []ActionDefinition `json:"supportedActions,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
type jmxHelperMonitoringPoint struct {
Timestamp int64 `json:"timestamp"`
HeapUsedBytes int64 `json:"heapUsedBytes,omitempty"`
HeapCommittedBytes int64 `json:"heapCommittedBytes,omitempty"`
HeapMaxBytes int64 `json:"heapMaxBytes,omitempty"`
NonHeapUsedBytes int64 `json:"nonHeapUsedBytes,omitempty"`
NonHeapCommittedBytes int64 `json:"nonHeapCommittedBytes,omitempty"`
GCCollectionCount int64 `json:"gcCollectionCount,omitempty"`
GCCollectionTimeMs int64 `json:"gcCollectionTimeMs,omitempty"`
GCDeltaCount int64 `json:"gcDeltaCount,omitempty"`
GCDeltaTimeMs int64 `json:"gcDeltaTimeMs,omitempty"`
ThreadCount int `json:"threadCount,omitempty"`
DaemonThreadCount int `json:"daemonThreadCount,omitempty"`
PeakThreadCount int `json:"peakThreadCount,omitempty"`
ThreadStateCounts map[string]int `json:"threadStateCounts,omitempty"`
LoadedClassCount int `json:"loadedClassCount,omitempty"`
UnloadedClassCount int64 `json:"unloadedClassCount,omitempty"`
ClassLoadDelta int64 `json:"classLoadDelta,omitempty"`
ProcessCpuLoad float64 `json:"processCpuLoad,omitempty"`
SystemCpuLoad float64 `json:"systemCpuLoad,omitempty"`
ProcessRssBytes int64 `json:"processRssBytes,omitempty"`
CommittedVirtualMemoryBytes int64 `json:"committedVirtualMemoryBytes,omitempty"`
}
type jmxHelperMonitoringSnapshot struct {
Point jmxHelperMonitoringPoint `json:"point"`
RecentGCEvents []RecentGCEvent `json:"recentGcEvents,omitempty"`
AvailableMetrics []string `json:"availableMetrics,omitempty"`
MissingMetrics []string `json:"missingMetrics,omitempty"`
ProviderWarnings []string `json:"providerWarnings,omitempty"`
}
type jmxHelperPreview struct {
Allowed bool `json:"allowed"`
RequiresConfirmation bool `json:"requiresConfirmation,omitempty"`
Summary string `json:"summary"`
RiskLevel string `json:"riskLevel"`
BlockingReason string `json:"blockingReason,omitempty"`
Before *jmxHelperSnapshot `json:"before,omitempty"`
After *jmxHelperSnapshot `json:"after,omitempty"`
}
type jmxHelperApplyResponse struct {
Status string `json:"status"`
Message string `json:"message,omitempty"`
UpdatedValue *jmxHelperSnapshot `json:"updatedValue,omitempty"`
}
func resolveJMXHost(cfg connection.ConnectionConfig) string {
host := strings.TrimSpace(cfg.JVM.JMX.Host)
if host == "" {
host = strings.TrimSpace(cfg.Host)
}
return host
}
func resolveJMXPort(cfg connection.ConnectionConfig) int {
if cfg.JVM.JMX.Port != 0 {
return cfg.JVM.JMX.Port
}
if cfg.Port > 0 {
return cfg.Port
}
return defaultJMXPort
}
func resolveJMXTimeout(cfg connection.ConnectionConfig) time.Duration {
timeout := time.Duration(cfg.Timeout) * time.Second
if timeout <= 0 {
timeout = 5 * time.Second
}
return timeout
}
func normalizeJMXAllowlist(values []string) []string {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, item := range values {
trimmed := strings.TrimSpace(item)
if trimmed == "" {
continue
}
if _, exists := seen[trimmed]; exists {
continue
}
seen[trimmed] = struct{}{}
result = append(result, trimmed)
}
sort.Strings(result)
return result
}
func validateJMXConnection(cfg connection.ConnectionConfig) error {
host := resolveJMXHost(cfg)
if host == "" {
return fmt.Errorf("jmx host is required")
}
port := resolveJMXPort(cfg)
if port <= 0 {
return fmt.Errorf("jmx port is invalid: %d", port)
}
return nil
}
func buildJMXResourcePath(target jmxResourceTarget) string {
query := url.Values{}
var path string
switch target.Kind {
case jmxResourceKindDomain:
path = "/domain/" + url.PathEscape(target.Domain)
case jmxResourceKindMBean:
path = "/mbean/" + url.PathEscape(target.ObjectName)
case jmxResourceKindAttribute:
path = "/attribute/" + url.PathEscape(target.ObjectName) + "/" + url.PathEscape(target.Attribute)
case jmxResourceKindOperation:
path = "/operation/" + url.PathEscape(target.ObjectName) + "/" + url.PathEscape(target.Operation)
if len(target.Signature) > 0 {
query.Set("signature", strings.Join(target.Signature, ","))
}
default:
return ""
}
if len(query) == 0 {
return jmxResourceScheme + ":" + path
}
return jmxResourceScheme + ":" + path + "?" + query.Encode()
}
func parseJMXResourcePath(raw string) (jmxResourceTarget, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return jmxResourceTarget{}, fmt.Errorf("resource path is empty")
}
parsed, err := url.Parse(trimmed)
if err != nil {
return jmxResourceTarget{}, fmt.Errorf("resource path parse failed: %w", err)
}
if !strings.EqualFold(parsed.Scheme, jmxResourceScheme) {
return jmxResourceTarget{}, fmt.Errorf("resource path scheme must be %q", jmxResourceScheme)
}
segments := strings.Split(strings.TrimPrefix(parsed.EscapedPath(), "/"), "/")
if len(segments) == 0 || segments[0] == "" {
return jmxResourceTarget{}, fmt.Errorf("resource path kind is missing")
}
unescape := func(value string) (string, error) {
decoded, decodeErr := url.PathUnescape(value)
if decodeErr != nil {
return "", fmt.Errorf("resource path decode failed: %w", decodeErr)
}
return decoded, nil
}
target := jmxResourceTarget{Kind: segments[0]}
switch target.Kind {
case jmxResourceKindDomain:
if len(segments) != 2 {
return jmxResourceTarget{}, fmt.Errorf("domain resource path must contain exactly 2 segments")
}
target.Domain, err = unescape(segments[1])
case jmxResourceKindMBean:
if len(segments) != 2 {
return jmxResourceTarget{}, fmt.Errorf("mbean resource path must contain exactly 2 segments")
}
target.ObjectName, err = unescape(segments[1])
case jmxResourceKindAttribute:
if len(segments) != 3 {
return jmxResourceTarget{}, fmt.Errorf("attribute resource path must contain exactly 3 segments")
}
target.ObjectName, err = unescape(segments[1])
if err == nil {
target.Attribute, err = unescape(segments[2])
}
case jmxResourceKindOperation:
if len(segments) != 3 {
return jmxResourceTarget{}, fmt.Errorf("operation resource path must contain exactly 3 segments")
}
target.ObjectName, err = unescape(segments[1])
if err == nil {
target.Operation, err = unescape(segments[2])
}
if signatureValue := strings.TrimSpace(parsed.Query().Get("signature")); signatureValue != "" {
target.Signature = splitSignature(signatureValue)
}
default:
return jmxResourceTarget{}, fmt.Errorf("resource path kind %q is unsupported", target.Kind)
}
if err != nil {
return jmxResourceTarget{}, err
}
return target, nil
}
func splitSignature(raw string) []string {
parts := strings.Split(raw, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
continue
}
result = append(result, trimmed)
}
return result
}
func helperTargetFromResource(target jmxResourceTarget) *jmxHelperTarget {
return &jmxHelperTarget{
Kind: target.Kind,
Domain: target.Domain,
ObjectName: target.ObjectName,
Attribute: target.Attribute,
Operation: target.Operation,
Signature: append([]string(nil), target.Signature...),
}
}
func resourceTargetFromHelper(item jmxHelperResource) jmxResourceTarget {
return jmxResourceTarget{
Kind: item.Kind,
Domain: item.Domain,
ObjectName: item.ObjectName,
Attribute: item.Attribute,
Operation: item.Operation,
Signature: append([]string(nil), item.Signature...),
}
}
func parentResourcePath(target jmxResourceTarget) string {
switch target.Kind {
case jmxResourceKindDomain:
return ""
case jmxResourceKindMBean:
return buildJMXResourcePath(jmxResourceTarget{Kind: jmxResourceKindDomain, Domain: domainFromObjectName(target.ObjectName)})
case jmxResourceKindAttribute, jmxResourceKindOperation:
return buildJMXResourcePath(jmxResourceTarget{Kind: jmxResourceKindMBean, ObjectName: target.ObjectName})
default:
return ""
}
}
func domainFromObjectName(objectName string) string {
if idx := strings.Index(strings.TrimSpace(objectName), ":"); idx > 0 {
return objectName[:idx]
}
return ""
}
func helperContextSummary(cfg connection.ConnectionConfig, target *jmxResourceTarget) string {
base := fmt.Sprintf("%s:%d", resolveJMXHost(cfg), resolveJMXPort(cfg))
if target == nil {
return base
}
switch target.Kind {
case jmxResourceKindDomain:
return fmt.Sprintf("%s domain=%s", base, target.Domain)
case jmxResourceKindMBean:
return fmt.Sprintf("%s mbean=%s", base, target.ObjectName)
case jmxResourceKindAttribute:
return fmt.Sprintf("%s attribute=%s::%s", base, target.ObjectName, target.Attribute)
case jmxResourceKindOperation:
return fmt.Sprintf("%s operation=%s::%s(%s)", base, target.ObjectName, target.Operation, strings.Join(target.Signature, ","))
default:
return base
}
}
func redactJMXHelperOutput(text string) string {
redacted := jmxHelperSensitiveJSONFieldPattern.ReplaceAllString(text, `${1}<redacted>${3}`)
return jmxHelperSensitivePairPattern.ReplaceAllString(redacted, `${1}${2}<redacted>`)
}
func runJMXHelper(
ctx context.Context,
cfg connection.ConnectionConfig,
command string,
target *jmxResourceTarget,
change *ChangeRequest,
) (jmxHelperResponse, error) {
if err := validateJMXConnection(cfg); err != nil {
return jmxHelperResponse{}, err
}
runtimeInfo, err := ensureJMXHelperRuntime(ctx)
if err != nil {
return jmxHelperResponse{}, err
}
requestPayload := jmxHelperRequest{
Command: command,
Connection: jmxHelperConnection{
Host: resolveJMXHost(cfg),
Port: resolveJMXPort(cfg),
Username: strings.TrimSpace(cfg.JVM.JMX.Username),
Password: cfg.JVM.JMX.Password,
DomainAllowlist: normalizeJMXAllowlist(cfg.JVM.JMX.DomainAllowlist),
TimeoutSeconds: int(resolveJMXTimeout(cfg).Seconds()),
},
}
if target != nil {
requestPayload.Target = helperTargetFromResource(*target)
}
if change != nil {
requestPayload.Change = &jmxHelperChangePlan{
Action: strings.TrimSpace(change.Action),
Reason: strings.TrimSpace(change.Reason),
ExpectedVersion: strings.TrimSpace(change.ExpectedVersion),
Payload: change.Payload,
}
}
input, err := json.Marshal(requestPayload)
if err != nil {
return jmxHelperResponse{}, fmt.Errorf("encode JMX helper request failed: %w", err)
}
execCtx, cancel := withJMXTimeout(ctx, cfg)
defer cancel()
cmd := jmxHelperCommandContext(execCtx, runtimeInfo.javaBinary, "-cp", runtimeInfo.classpath, jmxHelperMainClass)
configureJMXHelperCommand(cmd)
cmd.Stdin = bytes.NewReader(input)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
stderrText := strings.TrimSpace(redactJMXHelperOutput(stderr.String()))
if stderrText == "" {
stderrText = "<empty>"
}
return jmxHelperResponse{}, fmt.Errorf(
"jmx helper %s failed for %s: %w; stderr: %s",
command,
helperContextSummary(cfg, target),
err,
stderrText,
)
}
var response jmxHelperResponse
if err := json.Unmarshal(stdout.Bytes(), &response); err != nil {
stdoutText := strings.TrimSpace(redactJMXHelperOutput(stdout.String()))
return jmxHelperResponse{}, fmt.Errorf(
"decode JMX helper %s response failed for %s: %w; stdout: %s",
command,
helperContextSummary(cfg, target),
err,
stdoutText,
)
}
if !response.OK {
errText := strings.TrimSpace(redactJMXHelperOutput(response.Error))
if errText == "" {
errText = "unknown helper failure"
}
if len(response.Details) > 0 {
detailsJSON, marshalErr := json.Marshal(response.Details)
if marshalErr == nil {
errText += "; details=" + redactJMXHelperOutput(string(detailsJSON))
}
}
return jmxHelperResponse{}, fmt.Errorf("jmx helper %s failed for %s: %s", command, helperContextSummary(cfg, target), errText)
}
return response, nil
}
func withJMXTimeout(ctx context.Context, cfg connection.ConnectionConfig) (context.Context, context.CancelFunc) {
if _, hasDeadline := ctx.Deadline(); hasDeadline {
return ctx, func() {}
}
return context.WithTimeout(ctx, resolveJMXTimeout(cfg))
}
func ensureJMXHelperRuntime(ctx context.Context) (jmxHelperRuntime, error) {
if err := ctx.Err(); err != nil {
return jmxHelperRuntime{}, err
}
javaBinary, err := resolveJMXBinary("GONAVI_JMX_JAVA_BIN", "java")
if err != nil {
return jmxHelperRuntime{}, err
}
if overridden := strings.TrimSpace(os.Getenv("GONAVI_JMX_HELPER_CLASSPATH")); overridden != "" {
return jmxHelperRuntime{javaBinary: javaBinary, classpath: overridden}, nil
}
jarBytes, fingerprint, err := resolveEmbeddedJMXHelperJar()
if err != nil {
return jmxHelperRuntime{}, err
}
cacheRoot, err := resolveJMXHelperCacheRoot()
if err != nil {
return jmxHelperRuntime{}, err
}
jarPath := filepath.Join(cacheRoot, fingerprint, "jmx-helper-runtime.jar")
if _, statErr := os.Stat(jarPath); statErr == nil {
return jmxHelperRuntime{javaBinary: javaBinary, classpath: jarPath}, nil
}
jmxHelperCompileMu.Lock()
defer jmxHelperCompileMu.Unlock()
if err := ctx.Err(); err != nil {
return jmxHelperRuntime{}, err
}
if _, statErr := os.Stat(jarPath); statErr == nil {
return jmxHelperRuntime{javaBinary: javaBinary, classpath: jarPath}, nil
}
if err := os.MkdirAll(filepath.Dir(jarPath), 0o755); err != nil {
return jmxHelperRuntime{}, fmt.Errorf("create JMX helper cache parent failed: %w", err)
}
tmpPath := jarPath + ".tmp"
_ = os.Remove(tmpPath)
defer func() {
_ = os.Remove(tmpPath)
}()
if err := os.WriteFile(tmpPath, jarBytes, 0o644); err != nil {
return jmxHelperRuntime{}, fmt.Errorf("write embedded JMX helper jar failed: %w", err)
}
_ = os.Remove(jarPath)
if err := os.Rename(tmpPath, jarPath); err != nil {
return jmxHelperRuntime{}, fmt.Errorf("publish embedded JMX helper jar failed: %w", err)
}
return jmxHelperRuntime{javaBinary: javaBinary, classpath: jarPath}, nil
}
func resolveJMXBinary(envKey string, defaultName string) (string, error) {
if overridden := strings.TrimSpace(os.Getenv(envKey)); overridden != "" {
return overridden, nil
}
bin, err := jmxHelperLookPath(defaultName)
if err != nil {
return "", fmt.Errorf("required JMX helper dependency %q not found: %w", defaultName, err)
}
return bin, nil
}
func resolveEmbeddedJMXHelperJar() ([]byte, string, error) {
if len(embeddedJMXHelperJar) == 0 {
return nil, "", fmt.Errorf("embedded JMX helper jar is empty")
}
sum := sha256.Sum256(embeddedJMXHelperJar)
return embeddedJMXHelperJar, hex.EncodeToString(sum[:]), nil
}
func resolveJMXHelperCacheRoot() (string, error) {
if overridden := strings.TrimSpace(os.Getenv("GONAVI_JMX_HELPER_CACHE_DIR")); overridden != "" {
return overridden, nil
}
cacheDir, err := os.UserCacheDir()
if err != nil {
return "", fmt.Errorf("resolve JMX helper cache dir failed: %w", err)
}
return filepath.Join(cacheDir, "gonavi", "jmx-helper"), nil
}
func inferSnapshotFormat(value any) string {
switch value.(type) {
case nil:
return "null"
case string:
return "string"
case bool:
return "boolean"
case float64, float32, int, int64, int32, int16, int8, uint, uint64, uint32, uint16, uint8, json.Number:
return "number"
case []any:
return "array"
default:
return "json"
}
}
func computeSnapshotVersion(snapshot ValueSnapshot) string {
payload := map[string]any{
"kind": strings.TrimSpace(snapshot.Kind),
"format": strings.TrimSpace(snapshot.Format),
"value": snapshot.Value,
"metadata": snapshot.Metadata,
}
encoded, err := json.Marshal(payload)
if err != nil {
encoded = []byte(fmt.Sprintf("%#v", payload))
}
sum := sha256.Sum256(encoded)
return hex.EncodeToString(sum[:])
}
func valueSnapshotFromHelper(target jmxResourceTarget, snapshot *jmxHelperSnapshot) (ValueSnapshot, error) {
if snapshot == nil {
return ValueSnapshot{}, fmt.Errorf("helper did not return snapshot for %s", buildJMXResourcePath(target))
}
normalized := ValueSnapshot{
ResourceID: buildJMXResourcePath(target),
Kind: strings.TrimSpace(snapshot.Kind),
Format: strings.TrimSpace(snapshot.Format),
Value: snapshot.Value,
Description: strings.TrimSpace(snapshot.Description),
Sensitive: snapshot.Sensitive,
SupportedActions: cloneActionDefinitions(snapshot.SupportedActions),
Metadata: cloneStringAnyMap(snapshot.Metadata),
}
if normalized.Kind == "" {
normalized.Kind = target.Kind
}
if normalized.Format == "" {
normalized.Format = inferSnapshotFormat(normalized.Value)
}
normalized.Version = computeSnapshotVersion(normalized)
return normalized, nil
}
func previewFromHelper(target jmxResourceTarget, preview *jmxHelperPreview) (ChangePreview, error) {
if preview == nil {
return ChangePreview{}, fmt.Errorf("helper did not return preview for %s", buildJMXResourcePath(target))
}
result := ChangePreview{
Allowed: preview.Allowed,
RequiresConfirmation: preview.RequiresConfirmation,
Summary: strings.TrimSpace(preview.Summary),
RiskLevel: strings.TrimSpace(preview.RiskLevel),
BlockingReason: strings.TrimSpace(preview.BlockingReason),
}
if result.Summary == "" {
result.Summary = buildJMXResourcePath(target)
}
if result.RiskLevel == "" {
result.RiskLevel = "medium"
}
if preview.Before != nil {
before, err := valueSnapshotFromHelper(target, preview.Before)
if err != nil {
return ChangePreview{}, err
}
result.Before = before
}
if preview.After != nil {
after, err := valueSnapshotFromHelper(target, preview.After)
if err != nil {
return ChangePreview{}, err
}
result.After = after
}
return result, nil
}
func monitoringSnapshotFromHelper(snapshot *jmxHelperMonitoringSnapshot) (JVMMonitoringSnapshot, error) {
if snapshot == nil {
return JVMMonitoringSnapshot{}, fmt.Errorf("helper did not return monitoring snapshot")
}
return JVMMonitoringSnapshot{
Point: JVMMonitoringPoint{
Timestamp: snapshot.Point.Timestamp,
HeapUsedBytes: snapshot.Point.HeapUsedBytes,
HeapCommittedBytes: snapshot.Point.HeapCommittedBytes,
HeapMaxBytes: snapshot.Point.HeapMaxBytes,
NonHeapUsedBytes: snapshot.Point.NonHeapUsedBytes,
NonHeapCommittedBytes: snapshot.Point.NonHeapCommittedBytes,
GCCollectionCount: snapshot.Point.GCCollectionCount,
GCCollectionTimeMs: snapshot.Point.GCCollectionTimeMs,
GCDeltaCount: snapshot.Point.GCDeltaCount,
GCDeltaTimeMs: snapshot.Point.GCDeltaTimeMs,
ThreadCount: snapshot.Point.ThreadCount,
DaemonThreadCount: snapshot.Point.DaemonThreadCount,
PeakThreadCount: snapshot.Point.PeakThreadCount,
ThreadStateCounts: cloneStringIntMap(snapshot.Point.ThreadStateCounts),
LoadedClassCount: snapshot.Point.LoadedClassCount,
UnloadedClassCount: snapshot.Point.UnloadedClassCount,
ClassLoadDelta: snapshot.Point.ClassLoadDelta,
ProcessCpuLoad: snapshot.Point.ProcessCpuLoad,
SystemCpuLoad: snapshot.Point.SystemCpuLoad,
ProcessRssBytes: snapshot.Point.ProcessRssBytes,
CommittedVirtualMemoryBytes: snapshot.Point.CommittedVirtualMemoryBytes,
},
RecentGCEvents: append([]RecentGCEvent(nil), snapshot.RecentGCEvents...),
AvailableMetrics: append([]string(nil), snapshot.AvailableMetrics...),
MissingMetrics: append([]string(nil), snapshot.MissingMetrics...),
ProviderWarnings: append([]string(nil), snapshot.ProviderWarnings...),
}, nil
}
func applyResultFromHelper(target jmxResourceTarget, result *jmxHelperApplyResponse) (ApplyResult, error) {
if result == nil {
return ApplyResult{}, fmt.Errorf("helper did not return apply result for %s", buildJMXResourcePath(target))
}
updatedValue, err := valueSnapshotFromHelper(target, result.UpdatedValue)
if err != nil {
return ApplyResult{}, err
}
return ApplyResult{
Status: strings.TrimSpace(result.Status),
Message: strings.TrimSpace(result.Message),
UpdatedValue: updatedValue,
}, nil
}
func cloneStringAnyMap(input map[string]any) map[string]any {
if len(input) == 0 {
return nil
}
result := make(map[string]any, len(input))
for key, value := range input {
result[key] = value
}
return result
}
func cloneActionDefinitions(input []ActionDefinition) []ActionDefinition {
if len(input) == 0 {
return nil
}
result := make([]ActionDefinition, 0, len(input))
for _, item := range input {
copied := item
if len(item.PayloadFields) > 0 {
copied.PayloadFields = append([]ActionPayloadField(nil), item.PayloadFields...)
}
if item.PayloadExample != nil {
copied.PayloadExample = cloneStringAnyMap(item.PayloadExample)
}
result = append(result, copied)
}
return result
}
func resourceSummaryFromHelper(item jmxHelperResource) ResourceSummary {
target := resourceTargetFromHelper(item)
path := buildJMXResourcePath(target)
return ResourceSummary{
ID: path,
ParentID: parentResourcePath(target),
Kind: item.Kind,
Name: item.Name,
Path: path,
ProviderMode: ModeJMX,
CanRead: item.CanRead,
CanWrite: item.CanWrite,
HasChildren: item.HasChildren,
Sensitive: item.Sensitive,
}
}
func staleVersionError(resourcePath string, expected string, actual string) error {
return fmt.Errorf(
"jmx apply change rejected for %s: version mismatch, expected %s, got %s",
resourcePath,
strings.TrimSpace(expected),
strings.TrimSpace(actual),
)
}
func parseParentResourcePath(raw string) (*jmxResourceTarget, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return nil, nil
}
target, err := parseJMXResourcePath(trimmed)
if err != nil {
return nil, fmt.Errorf("invalid JMX parent resource path %q: %w", raw, err)
}
if target.Kind != jmxResourceKindDomain && target.Kind != jmxResourceKindMBean {
return nil, fmt.Errorf("JMX parent resource path %q cannot be listed", raw)
}
return &target, nil
}
func parseRequiredResourcePath(raw string) (jmxResourceTarget, error) {
target, err := parseJMXResourcePath(raw)
if err != nil {
return jmxResourceTarget{}, fmt.Errorf("invalid JMX resource path %q: %w", raw, err)
}
return target, nil
}
func normalizeHelperPort(value any) int {
switch typed := value.(type) {
case float64:
return int(typed)
case string:
parsed, err := strconv.Atoi(strings.TrimSpace(typed))
if err == nil {
return parsed
}
}
return 0
}