mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-07 19:11:39 +08:00
✨ feat(jvm): 新增持续监控与采样链路
- 后端新增监控会话管理,支持启动、停止和历史查询 - JMX、Endpoint、Agent Provider 补齐监控快照采集能力 - JMX helper 增加内存、GC、线程、类加载采样并更新内嵌运行时 - 生成 Wails 监控接口绑定并补充后端回归测试
This commit is contained in:
@@ -91,6 +91,20 @@ func (p *AgentProvider) GetValue(ctx context.Context, cfg connection.ConnectionC
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func (p *AgentProvider) GetMonitoringSnapshot(ctx context.Context, cfg connection.ConnectionConfig, previous *JVMMonitoringPoint) (JVMMonitoringSnapshot, error) {
|
||||
runtime, err := newAgentRuntime(cfg)
|
||||
if err != nil {
|
||||
return JVMMonitoringSnapshot{}, err
|
||||
}
|
||||
|
||||
var snapshot JVMMonitoringSnapshot
|
||||
if err := runtime.doJSON(ctx, http.MethodGet, "get monitoring snapshot", "metrics", nil, nil, &snapshot); err != nil {
|
||||
return JVMMonitoringSnapshot{}, err
|
||||
}
|
||||
finalizeMonitoringSnapshot(&snapshot, previous)
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func (p *AgentProvider) PreviewChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ChangePreview, error) {
|
||||
runtime, err := newAgentRuntime(cfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -59,6 +59,51 @@ func TestAgentProviderListResourcesBuildsRequestAndDecodesResponse(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProviderGetMonitoringSnapshotDecodesResponse(t *testing.T) {
|
||||
provider := &AgentProvider{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Fatalf("expected GET request, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/gonavi/agent/jvm/metrics" {
|
||||
t.Fatalf("expected path /gonavi/agent/jvm/metrics, got %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(JVMMonitoringSnapshot{
|
||||
Point: JVMMonitoringPoint{
|
||||
Timestamp: 1713945600000,
|
||||
ThreadCount: 27,
|
||||
HeapUsedBytes: 402653184,
|
||||
GCCollectionCount: 128,
|
||||
GCDeltaCount: 2,
|
||||
ProcessCpuLoad: 0.29,
|
||||
CommittedVirtualMemoryBytes: 2147483648,
|
||||
},
|
||||
RecentGCEvents: []RecentGCEvent{{
|
||||
Timestamp: 1713945600000,
|
||||
Name: "ConcurrentMarkSweep",
|
||||
DurationMs: 12,
|
||||
}},
|
||||
AvailableMetrics: []string{"thread.count", "heap.used", "gc.count", "cpu.process", "memory.virtual"},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
snapshot, err := provider.GetMonitoringSnapshot(context.Background(), newAgentProviderTestConfig(server.URL+"/gonavi/agent/jvm", 3), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMonitoringSnapshot returned error: %v", err)
|
||||
}
|
||||
if snapshot.Point.ThreadCount != 27 || snapshot.Point.GCDeltaCount != 2 || snapshot.Point.ProcessCpuLoad != 0.29 {
|
||||
t.Fatalf("unexpected monitoring snapshot: %#v", snapshot)
|
||||
}
|
||||
if len(snapshot.RecentGCEvents) != 1 || snapshot.RecentGCEvents[0].Name != "ConcurrentMarkSweep" {
|
||||
t.Fatalf("unexpected recent gc events: %#v", snapshot.RecentGCEvents)
|
||||
}
|
||||
if len(snapshot.AvailableMetrics) != 5 {
|
||||
t.Fatalf("unexpected available metrics: %#v", snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProviderRealAgentRoundTrip(t *testing.T) {
|
||||
if _, err := exec.LookPath("java"); err != nil {
|
||||
t.Skipf("java 不可用,跳过真实 Agent 集成测试: %v", err)
|
||||
@@ -214,7 +259,18 @@ func startAgentFixture(t *testing.T) agentFixtureProcess {
|
||||
t.Fatalf("write agent manifest failed: %v", err)
|
||||
}
|
||||
|
||||
agentJar := filepath.Join(t.TempDir(), "gonavi-test-agent.jar")
|
||||
agentJarFile, err := os.CreateTemp("", "gonavi-test-agent-*.jar")
|
||||
if err != nil {
|
||||
t.Fatalf("create agent jar temp file failed: %v", err)
|
||||
}
|
||||
agentJar := agentJarFile.Name()
|
||||
if closeErr := agentJarFile.Close(); closeErr != nil {
|
||||
t.Fatalf("close agent jar temp file failed: %v", closeErr)
|
||||
}
|
||||
_ = os.Remove(agentJar)
|
||||
t.Cleanup(func() {
|
||||
_ = os.Remove(agentJar)
|
||||
})
|
||||
jarCmd := exec.Command(jarBin, "cmf", manifestPath, agentJar, "-C", classesDir, "com")
|
||||
output, err = jarCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
|
||||
@@ -92,6 +92,20 @@ func (p *HTTPProvider) GetValue(ctx context.Context, cfg connection.ConnectionCo
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func (p *HTTPProvider) GetMonitoringSnapshot(ctx context.Context, cfg connection.ConnectionConfig, previous *JVMMonitoringPoint) (JVMMonitoringSnapshot, error) {
|
||||
runtime, err := newEndpointRuntime(cfg)
|
||||
if err != nil {
|
||||
return JVMMonitoringSnapshot{}, err
|
||||
}
|
||||
|
||||
var snapshot JVMMonitoringSnapshot
|
||||
if err := runtime.doJSON(ctx, http.MethodGet, "get monitoring snapshot", "metrics", nil, nil, &snapshot); err != nil {
|
||||
return JVMMonitoringSnapshot{}, err
|
||||
}
|
||||
finalizeMonitoringSnapshot(&snapshot, previous)
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func (p *HTTPProvider) PreviewChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ChangePreview, error) {
|
||||
runtime, err := newEndpointRuntime(cfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -93,6 +93,52 @@ func TestHTTPProviderGetValueDecodesResponse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPProviderGetMonitoringSnapshotDecodesResponse(t *testing.T) {
|
||||
provider := &HTTPProvider{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Fatalf("expected GET request, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/manage/jvm/metrics" {
|
||||
t.Fatalf("expected path /manage/jvm/metrics, got %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(JVMMonitoringSnapshot{
|
||||
Point: JVMMonitoringPoint{
|
||||
Timestamp: 1713945600000,
|
||||
ThreadCount: 18,
|
||||
HeapUsedBytes: 805306368,
|
||||
ProcessCpuLoad: 0.48,
|
||||
ProcessRssBytes: 1879048192,
|
||||
LoadedClassCount: 4096,
|
||||
},
|
||||
RecentGCEvents: []RecentGCEvent{{
|
||||
Timestamp: 1713945600000,
|
||||
Name: "G1 Old Generation",
|
||||
DurationMs: 41,
|
||||
}},
|
||||
AvailableMetrics: []string{"thread.count", "heap.used", "cpu.process", "memory.rss", "class.loading"},
|
||||
MissingMetrics: []string{"cpu.system"},
|
||||
ProviderWarnings: []string{"endpoint cpu metric unavailable"},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
snapshot, err := provider.GetMonitoringSnapshot(context.Background(), newHTTPProviderTestConfig(server.URL+"/manage/jvm", 3), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMonitoringSnapshot returned error: %v", err)
|
||||
}
|
||||
if snapshot.Point.ThreadCount != 18 || snapshot.Point.HeapUsedBytes != 805306368 || snapshot.Point.ProcessRssBytes != 1879048192 {
|
||||
t.Fatalf("unexpected monitoring snapshot: %#v", snapshot)
|
||||
}
|
||||
if len(snapshot.RecentGCEvents) != 1 || snapshot.RecentGCEvents[0].Name != "G1 Old Generation" {
|
||||
t.Fatalf("unexpected recent gc events: %#v", snapshot.RecentGCEvents)
|
||||
}
|
||||
if len(snapshot.MissingMetrics) != 1 || snapshot.MissingMetrics[0] != "cpu.system" {
|
||||
t.Fatalf("unexpected missing metrics: %#v", snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPProviderPreviewChangeAndApplySendJSONBody(t *testing.T) {
|
||||
provider := NewHTTPProvider()
|
||||
request := ChangeRequest{
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -32,6 +33,7 @@ const (
|
||||
jmxHelperCommandPing = "ping"
|
||||
jmxHelperCommandList = "list"
|
||||
jmxHelperCommandGet = "get"
|
||||
jmxHelperCommandMonitor = "monitor"
|
||||
jmxHelperCommandPreview = "preview"
|
||||
jmxHelperCommandApply = "apply"
|
||||
|
||||
@@ -44,6 +46,11 @@ var (
|
||||
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
|
||||
|
||||
@@ -94,13 +101,14 @@ type jmxHelperChangePlan struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
Preview *jmxHelperPreview `json:"preview,omitempty"`
|
||||
ApplyResult *jmxHelperApplyResponse `json:"applyResult,omitempty"`
|
||||
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 {
|
||||
@@ -127,6 +135,38 @@ type jmxHelperSnapshot struct {
|
||||
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"`
|
||||
@@ -366,6 +406,11 @@ func helperContextSummary(cfg connection.ConnectionConfig, target *jmxResourceTa
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -414,6 +459,7 @@ func runJMXHelper(
|
||||
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
|
||||
@@ -421,7 +467,7 @@ func runJMXHelper(
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
stderrText := strings.TrimSpace(stderr.String())
|
||||
stderrText := strings.TrimSpace(redactJMXHelperOutput(stderr.String()))
|
||||
if stderrText == "" {
|
||||
stderrText = "<empty>"
|
||||
}
|
||||
@@ -436,23 +482,24 @@ func runJMXHelper(
|
||||
|
||||
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,
|
||||
strings.TrimSpace(stdout.String()),
|
||||
stdoutText,
|
||||
)
|
||||
}
|
||||
if !response.OK {
|
||||
errText := strings.TrimSpace(response.Error)
|
||||
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=" + string(detailsJSON)
|
||||
errText += "; details=" + redactJMXHelperOutput(string(detailsJSON))
|
||||
}
|
||||
}
|
||||
return jmxHelperResponse{}, fmt.Errorf("jmx helper %s failed for %s: %s", command, helperContextSummary(cfg, target), errText)
|
||||
@@ -649,6 +696,42 @@ func previewFromHelper(target jmxResourceTarget, preview *jmxHelperPreview) (Cha
|
||||
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))
|
||||
|
||||
8
internal/jvm/jmx_helper_process_other.go
Normal file
8
internal/jvm/jmx_helper_process_other.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package jvm
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func configureJMXHelperCommand(_ *exec.Cmd) {
|
||||
}
|
||||
15
internal/jvm/jmx_helper_process_windows.go
Normal file
15
internal/jvm/jmx_helper_process_windows.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build windows
|
||||
|
||||
package jvm
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func configureJMXHelperCommand(cmd *exec.Cmd) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -82,3 +83,18 @@ func TestEnsureJMXHelperRuntimeUsesOverrideClasspath(t *testing.T) {
|
||||
t.Fatalf("expected override mode to skip cache writes, stat err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactJMXHelperOutputMasksSensitiveFields(t *testing.T) {
|
||||
output := `{"password":"secret-pass","apiKey":"agent-token","details":"token=abc123 password: raw-secret"}`
|
||||
|
||||
redacted := redactJMXHelperOutput(output)
|
||||
|
||||
for _, secret := range []string{"secret-pass", "agent-token", "abc123", "raw-secret"} {
|
||||
if strings.Contains(redacted, secret) {
|
||||
t.Fatalf("expected %q to be redacted from %q", secret, redacted)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(redacted, "<redacted>") {
|
||||
t.Fatalf("expected redaction marker, got %q", redacted)
|
||||
}
|
||||
}
|
||||
|
||||
18
internal/jvm/jmx_helper_windows_test.go
Normal file
18
internal/jvm/jmx_helper_windows_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build windows
|
||||
|
||||
package jvm
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigureJMXHelperCommandHidesWindowOnWindows(t *testing.T) {
|
||||
cmd := exec.Command("java")
|
||||
|
||||
configureJMXHelperCommand(cmd)
|
||||
|
||||
if cmd.SysProcAttr == nil || !cmd.SysProcAttr.HideWindow {
|
||||
t.Fatalf("expected JMX helper command to hide Windows console window, got %#v", cmd.SysProcAttr)
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,19 @@ func (p *JMXProvider) GetValue(ctx context.Context, cfg connection.ConnectionCon
|
||||
return valueSnapshotFromHelper(target, resp.Snapshot)
|
||||
}
|
||||
|
||||
func (p *JMXProvider) GetMonitoringSnapshot(ctx context.Context, cfg connection.ConnectionConfig, previous *JVMMonitoringPoint) (JVMMonitoringSnapshot, error) {
|
||||
resp, err := jmxHelperRunner(ctx, cfg, jmxHelperCommandMonitor, nil, nil)
|
||||
if err != nil {
|
||||
return JVMMonitoringSnapshot{}, fmt.Errorf("jmx get monitoring snapshot failed: %w", err)
|
||||
}
|
||||
snapshot, err := monitoringSnapshotFromHelper(resp.MonitoringSnapshot)
|
||||
if err != nil {
|
||||
return JVMMonitoringSnapshot{}, err
|
||||
}
|
||||
finalizeMonitoringSnapshot(&snapshot, previous)
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func (p *JMXProvider) PreviewChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ChangePreview, error) {
|
||||
target, err := parseRequiredResourcePath(req.ResourceID)
|
||||
if err != nil {
|
||||
|
||||
@@ -135,6 +135,51 @@ func TestJMXProviderGetValueUsesHelperSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestJMXProviderGetMonitoringSnapshotUsesHelperMonitorCommand(t *testing.T) {
|
||||
helper := &stubJMXHelper{
|
||||
response: jmxHelperResponse{
|
||||
MonitoringSnapshot: &jmxHelperMonitoringSnapshot{
|
||||
Point: jmxHelperMonitoringPoint{
|
||||
Timestamp: 1713945600000,
|
||||
ThreadCount: 33,
|
||||
HeapUsedBytes: 536870912,
|
||||
ProcessCpuLoad: 0.37,
|
||||
LoadedClassCount: 2048,
|
||||
ProcessRssBytes: 1610612736,
|
||||
CommittedVirtualMemoryBytes: 2147483648,
|
||||
},
|
||||
RecentGCEvents: []RecentGCEvent{{
|
||||
Timestamp: 1713945600000,
|
||||
Name: "G1 Young Generation",
|
||||
Cause: "G1 Evacuation Pause",
|
||||
DurationMs: 18,
|
||||
}},
|
||||
AvailableMetrics: []string{"thread.count", "heap.used", "class.loading", "memory.rss"},
|
||||
MissingMetrics: []string{"cpu.system"},
|
||||
},
|
||||
},
|
||||
}
|
||||
withStubJMXHelper(t, helper.run)
|
||||
provider := &JMXProvider{}
|
||||
|
||||
snapshot, err := provider.GetMonitoringSnapshot(context.Background(), newJMXProviderTestConfig(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMonitoringSnapshot returned error: %v", err)
|
||||
}
|
||||
if helper.lastRequest.Command != jmxHelperCommandMonitor {
|
||||
t.Fatalf("expected helper command %q, got %#v", jmxHelperCommandMonitor, helper.lastRequest)
|
||||
}
|
||||
if snapshot.Point.ThreadCount != 33 || snapshot.Point.HeapUsedBytes != 536870912 || snapshot.Point.LoadedClassCount != 2048 {
|
||||
t.Fatalf("unexpected monitoring snapshot: %#v", snapshot)
|
||||
}
|
||||
if len(snapshot.RecentGCEvents) != 1 || snapshot.RecentGCEvents[0].DurationMs != 18 {
|
||||
t.Fatalf("unexpected recent gc events: %#v", snapshot.RecentGCEvents)
|
||||
}
|
||||
if len(snapshot.MissingMetrics) != 1 || snapshot.MissingMetrics[0] != "cpu.system" {
|
||||
t.Fatalf("unexpected missing metrics: %#v", snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJMXProviderPreviewAndApplyUseHelperPayload(t *testing.T) {
|
||||
request := ChangeRequest{
|
||||
ProviderMode: ModeJMX,
|
||||
|
||||
Binary file not shown.
351
internal/jvm/monitoring_manager.go
Normal file
351
internal/jvm/monitoring_manager.go
Normal file
@@ -0,0 +1,351 @@
|
||||
package jvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMonitoringPointLimit = 180
|
||||
defaultMonitoringEventLimit = 20
|
||||
defaultMonitoringInterval = 2 * time.Second
|
||||
maxMonitoringSampleFailures = 3
|
||||
)
|
||||
|
||||
var monitoringProviderFactory = NewProvider
|
||||
|
||||
type monitoringManager struct {
|
||||
mu sync.Mutex
|
||||
limit int
|
||||
interval time.Duration
|
||||
sessions map[string]*monitoringSession
|
||||
}
|
||||
|
||||
type monitoringSession struct {
|
||||
mu sync.Mutex
|
||||
connectionID string
|
||||
providerMode string
|
||||
limit int
|
||||
running bool
|
||||
points []JVMMonitoringPoint
|
||||
recentGCEvents []RecentGCEvent
|
||||
availableMetrics []string
|
||||
missingMetrics []string
|
||||
providerWarnings []string
|
||||
cancel context.CancelFunc
|
||||
generation int64
|
||||
}
|
||||
|
||||
func newMonitoringManagerForTest(limit int) *monitoringManager {
|
||||
return newMonitoringManager(limit, 0)
|
||||
}
|
||||
|
||||
func NewMonitoringManager() *monitoringManager {
|
||||
return newMonitoringManager(defaultMonitoringPointLimit, defaultMonitoringInterval)
|
||||
}
|
||||
|
||||
func newMonitoringManager(limit int, interval time.Duration) *monitoringManager {
|
||||
if limit <= 0 {
|
||||
limit = defaultMonitoringPointLimit
|
||||
}
|
||||
return &monitoringManager{
|
||||
limit: limit,
|
||||
interval: interval,
|
||||
sessions: make(map[string]*monitoringSession),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *monitoringManager) ensureSession(connectionID string, providerMode string) *monitoringSession {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := connectionID + ":" + providerMode
|
||||
if session, ok := m.sessions[key]; ok {
|
||||
return session
|
||||
}
|
||||
|
||||
session := &monitoringSession{
|
||||
connectionID: connectionID,
|
||||
providerMode: providerMode,
|
||||
limit: m.limit,
|
||||
}
|
||||
m.sessions[key] = session
|
||||
return session
|
||||
}
|
||||
|
||||
func (m *monitoringManager) Start(ctx context.Context, raw connection.ConnectionConfig, requestedMode string) (MonitoringSessionSnapshot, error) {
|
||||
cfg, providerMode, err := ResolveProviderMode(raw, requestedMode)
|
||||
if err != nil {
|
||||
return MonitoringSessionSnapshot{}, err
|
||||
}
|
||||
|
||||
connectionID := resolveMonitoringConnectionID(cfg)
|
||||
session := m.ensureSession(connectionID, providerMode)
|
||||
|
||||
provider, err := monitoringProviderFactory(providerMode)
|
||||
if err != nil {
|
||||
return MonitoringSessionSnapshot{}, err
|
||||
}
|
||||
|
||||
monitoringProvider, ok := provider.(MonitoringCapableProvider)
|
||||
if !ok {
|
||||
return MonitoringSessionSnapshot{}, fmt.Errorf("%s provider does not implement monitoring snapshot yet", ModeDisplayLabel(providerMode))
|
||||
}
|
||||
|
||||
generation := session.reset(connectionID, providerMode)
|
||||
if err := m.sampleOnce(ctx, monitoringProvider, cfg, session, generation); err != nil {
|
||||
session.markStopped(generation)
|
||||
return MonitoringSessionSnapshot{}, err
|
||||
}
|
||||
|
||||
session.markRunning(generation)
|
||||
if m.interval > 0 {
|
||||
loopCtx, cancel := context.WithCancel(context.Background())
|
||||
session.setCancel(cancel)
|
||||
go m.runSampler(loopCtx, monitoringProvider, cfg, session, generation)
|
||||
}
|
||||
|
||||
return session.snapshot(), nil
|
||||
}
|
||||
|
||||
func (m *monitoringManager) Stop(connectionID string, providerMode string) error {
|
||||
m.mu.Lock()
|
||||
session, ok := m.sessions[m.sessionKey(connectionID, providerMode)]
|
||||
m.mu.Unlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("monitoring session not found for %s %s", connectionID, providerMode)
|
||||
}
|
||||
|
||||
session.stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *monitoringManager) GetHistory(connectionID string, providerMode string) (MonitoringSessionSnapshot, error) {
|
||||
m.mu.Lock()
|
||||
session, ok := m.sessions[m.sessionKey(connectionID, providerMode)]
|
||||
m.mu.Unlock()
|
||||
if !ok {
|
||||
return MonitoringSessionSnapshot{}, fmt.Errorf("monitoring session not found for %s %s", connectionID, providerMode)
|
||||
}
|
||||
return session.snapshot(), nil
|
||||
}
|
||||
|
||||
func (s *monitoringSession) appendPoint(point JVMMonitoringPoint) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.points = append(s.points, point)
|
||||
if len(s.points) > s.limit {
|
||||
s.points = append([]JVMMonitoringPoint(nil), s.points[len(s.points)-s.limit:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *monitoringManager) sessionKey(connectionID string, providerMode string) string {
|
||||
return strings.TrimSpace(connectionID) + ":" + strings.TrimSpace(providerMode)
|
||||
}
|
||||
|
||||
func (m *monitoringManager) runSampler(ctx context.Context, provider MonitoringCapableProvider, cfg connection.ConnectionConfig, session *monitoringSession, generation int64) {
|
||||
ticker := time.NewTicker(m.interval)
|
||||
defer ticker.Stop()
|
||||
consecutiveFailures := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
session.markStopped(generation)
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := m.sampleOnce(ctx, provider, cfg, session, generation); err != nil {
|
||||
consecutiveFailures++
|
||||
session.appendWarning(err.Error())
|
||||
if consecutiveFailures >= maxMonitoringSampleFailures {
|
||||
session.appendWarning(fmt.Sprintf("监控采样连续失败 %d 次,已自动停止本次监控会话", consecutiveFailures))
|
||||
session.markStopped(generation)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
consecutiveFailures = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *monitoringManager) sampleOnce(ctx context.Context, provider MonitoringCapableProvider, cfg connection.ConnectionConfig, session *monitoringSession, generation int64) error {
|
||||
previous, ok := session.previousPoint(generation)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
snapshot, err := provider.GetMonitoringSnapshot(ctx, cfg, previous)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.applySnapshot(snapshot, generation)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *monitoringSession) snapshot() MonitoringSessionSnapshot {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return MonitoringSessionSnapshot{
|
||||
ConnectionID: s.connectionID,
|
||||
ProviderMode: s.providerMode,
|
||||
Running: s.running,
|
||||
Points: append([]JVMMonitoringPoint(nil), s.points...),
|
||||
RecentGCEvents: append([]RecentGCEvent(nil), s.recentGCEvents...),
|
||||
AvailableMetrics: append([]string(nil), s.availableMetrics...),
|
||||
MissingMetrics: append([]string(nil), s.missingMetrics...),
|
||||
ProviderWarnings: append([]string(nil), s.providerWarnings...),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *monitoringSession) previousPoint(generation int64) (*JVMMonitoringPoint, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if generation != s.generation {
|
||||
return nil, false
|
||||
}
|
||||
if len(s.points) == 0 {
|
||||
return nil, true
|
||||
}
|
||||
point := s.points[len(s.points)-1]
|
||||
if point.ThreadStateCounts != nil {
|
||||
point.ThreadStateCounts = cloneStringIntMap(point.ThreadStateCounts)
|
||||
}
|
||||
return &point, true
|
||||
}
|
||||
|
||||
func (s *monitoringSession) applySnapshot(snapshot JVMMonitoringSnapshot, generation int64) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if generation != s.generation {
|
||||
return false
|
||||
}
|
||||
s.points = append(s.points, cloneMonitoringPoint(snapshot.Point))
|
||||
if len(s.points) > s.limit {
|
||||
s.points = append([]JVMMonitoringPoint(nil), s.points[len(s.points)-s.limit:]...)
|
||||
}
|
||||
s.recentGCEvents = append([]RecentGCEvent(nil), snapshot.RecentGCEvents...)
|
||||
if len(s.recentGCEvents) > defaultMonitoringEventLimit {
|
||||
s.recentGCEvents = append([]RecentGCEvent(nil), s.recentGCEvents[len(s.recentGCEvents)-defaultMonitoringEventLimit:]...)
|
||||
}
|
||||
s.availableMetrics = append([]string(nil), snapshot.AvailableMetrics...)
|
||||
s.missingMetrics = append([]string(nil), snapshot.MissingMetrics...)
|
||||
s.providerWarnings = append([]string(nil), snapshot.ProviderWarnings...)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *monitoringSession) appendWarning(warning string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
trimmed := strings.TrimSpace(warning)
|
||||
if trimmed == "" {
|
||||
return
|
||||
}
|
||||
for _, existing := range s.providerWarnings {
|
||||
if existing == trimmed {
|
||||
return
|
||||
}
|
||||
}
|
||||
s.providerWarnings = append(s.providerWarnings, trimmed)
|
||||
if len(s.providerWarnings) > defaultMonitoringEventLimit {
|
||||
s.providerWarnings = append([]string(nil), s.providerWarnings[len(s.providerWarnings)-defaultMonitoringEventLimit:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *monitoringSession) reset(connectionID string, providerMode string) int64 {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
s.cancel = nil
|
||||
}
|
||||
s.generation++
|
||||
s.connectionID = connectionID
|
||||
s.providerMode = providerMode
|
||||
s.running = false
|
||||
s.points = nil
|
||||
s.recentGCEvents = nil
|
||||
s.availableMetrics = nil
|
||||
s.missingMetrics = nil
|
||||
s.providerWarnings = nil
|
||||
return s.generation
|
||||
}
|
||||
|
||||
func (s *monitoringSession) setCancel(cancel context.CancelFunc) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.cancel = cancel
|
||||
}
|
||||
|
||||
func (s *monitoringSession) markRunning(generation int64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if generation != s.generation {
|
||||
return
|
||||
}
|
||||
s.running = true
|
||||
}
|
||||
|
||||
func (s *monitoringSession) markStopped(generation int64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if generation != s.generation {
|
||||
return
|
||||
}
|
||||
s.running = false
|
||||
s.cancel = nil
|
||||
}
|
||||
|
||||
func (s *monitoringSession) stop() {
|
||||
s.mu.Lock()
|
||||
cancel := s.cancel
|
||||
s.cancel = nil
|
||||
s.generation++
|
||||
s.running = false
|
||||
s.mu.Unlock()
|
||||
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func resolveMonitoringConnectionID(cfg connection.ConnectionConfig) string {
|
||||
if trimmed := strings.TrimSpace(cfg.ID); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
host := strings.TrimSpace(cfg.Host)
|
||||
if host == "" {
|
||||
host = "unknown"
|
||||
}
|
||||
if cfg.Port > 0 {
|
||||
return fmt.Sprintf("%s:%d", host, cfg.Port)
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func cloneMonitoringPoint(point JVMMonitoringPoint) JVMMonitoringPoint {
|
||||
cloned := point
|
||||
cloned.ThreadStateCounts = cloneStringIntMap(point.ThreadStateCounts)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneStringIntMap(input map[string]int) map[string]int {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string]int, len(input))
|
||||
for key, value := range input {
|
||||
cloned[key] = value
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
353
internal/jvm/monitoring_manager_test.go
Normal file
353
internal/jvm/monitoring_manager_test.go
Normal file
@@ -0,0 +1,353 @@
|
||||
package jvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
type fakeMonitoringProvider struct {
|
||||
snapshot JVMMonitoringSnapshot
|
||||
snapshotErr error
|
||||
}
|
||||
|
||||
type blockingMonitoringProvider struct {
|
||||
fakeMonitoringProvider
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (f fakeMonitoringProvider) Mode() string { return ModeJMX }
|
||||
func (f fakeMonitoringProvider) TestConnection(context.Context, connection.ConnectionConfig) error {
|
||||
return nil
|
||||
}
|
||||
func (f fakeMonitoringProvider) ProbeCapabilities(context.Context, connection.ConnectionConfig) ([]Capability, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f fakeMonitoringProvider) ListResources(context.Context, connection.ConnectionConfig, string) ([]ResourceSummary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f fakeMonitoringProvider) GetValue(context.Context, connection.ConnectionConfig, string) (ValueSnapshot, error) {
|
||||
return ValueSnapshot{}, nil
|
||||
}
|
||||
func (f fakeMonitoringProvider) PreviewChange(context.Context, connection.ConnectionConfig, ChangeRequest) (ChangePreview, error) {
|
||||
return ChangePreview{}, nil
|
||||
}
|
||||
func (f fakeMonitoringProvider) ApplyChange(context.Context, connection.ConnectionConfig, ChangeRequest) (ApplyResult, error) {
|
||||
return ApplyResult{}, nil
|
||||
}
|
||||
func (f fakeMonitoringProvider) GetMonitoringSnapshot(context.Context, connection.ConnectionConfig, *JVMMonitoringPoint) (JVMMonitoringSnapshot, error) {
|
||||
return f.snapshot, f.snapshotErr
|
||||
}
|
||||
|
||||
func (p *blockingMonitoringProvider) GetMonitoringSnapshot(context.Context, connection.ConnectionConfig, *JVMMonitoringPoint) (JVMMonitoringSnapshot, error) {
|
||||
p.once.Do(func() {
|
||||
close(p.started)
|
||||
})
|
||||
<-p.release
|
||||
return p.snapshot, p.snapshotErr
|
||||
}
|
||||
|
||||
func swapMonitoringProviderFactory(factory func(mode string) (Provider, error)) func() {
|
||||
prev := monitoringProviderFactory
|
||||
monitoringProviderFactory = factory
|
||||
return func() { monitoringProviderFactory = prev }
|
||||
}
|
||||
|
||||
func TestMonitoringRingBufferKeepsLatestPoints(t *testing.T) {
|
||||
manager := newMonitoringManagerForTest(3)
|
||||
session := manager.ensureSession("conn-1", ModeJMX)
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
session.appendPoint(JVMMonitoringPoint{Timestamp: int64(i)})
|
||||
}
|
||||
|
||||
snapshot := session.snapshot()
|
||||
if len(snapshot.Points) != 3 {
|
||||
t.Fatalf("expected 3 points, got %d", len(snapshot.Points))
|
||||
}
|
||||
if snapshot.Points[0].Timestamp != 3 || snapshot.Points[2].Timestamp != 5 {
|
||||
t.Fatalf("unexpected points order: %#v", snapshot.Points)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoringSessionSnapshotCarriesProviderWarningsAndGCEvents(t *testing.T) {
|
||||
manager := newMonitoringManagerForTest(5)
|
||||
session := manager.ensureSession("conn-2", ModeEndpoint)
|
||||
session.running = true
|
||||
session.availableMetrics = []string{"heap.used", "thread.count", "memory.rss"}
|
||||
session.missingMetrics = []string{"cpu.process", "gc.events"}
|
||||
session.providerWarnings = []string{"endpoint metrics degraded"}
|
||||
session.recentGCEvents = []RecentGCEvent{
|
||||
{
|
||||
Timestamp: 1713945600000,
|
||||
Name: "G1 Young Generation",
|
||||
Cause: "G1 Evacuation Pause",
|
||||
Action: "end of minor GC",
|
||||
DurationMs: 21,
|
||||
BeforeUsedBytes: 734003200,
|
||||
AfterUsedBytes: 503316480,
|
||||
},
|
||||
}
|
||||
session.appendPoint(JVMMonitoringPoint{
|
||||
Timestamp: 1713945600000,
|
||||
ThreadCount: 18,
|
||||
HeapUsedBytes: 503316480,
|
||||
ProcessRssBytes: 1073741824,
|
||||
})
|
||||
|
||||
snapshot := session.snapshot()
|
||||
if !snapshot.Running {
|
||||
t.Fatalf("expected session to be running")
|
||||
}
|
||||
if snapshot.ProviderMode != ModeEndpoint {
|
||||
t.Fatalf("expected provider mode %q, got %q", ModeEndpoint, snapshot.ProviderMode)
|
||||
}
|
||||
if len(snapshot.AvailableMetrics) != 3 {
|
||||
t.Fatalf("expected available metrics, got %#v", snapshot.AvailableMetrics)
|
||||
}
|
||||
if len(snapshot.MissingMetrics) != 2 || snapshot.MissingMetrics[0] != "cpu.process" {
|
||||
t.Fatalf("unexpected missing metrics: %#v", snapshot.MissingMetrics)
|
||||
}
|
||||
if len(snapshot.ProviderWarnings) != 1 {
|
||||
t.Fatalf("expected provider warning, got %#v", snapshot.ProviderWarnings)
|
||||
}
|
||||
if len(snapshot.RecentGCEvents) != 1 {
|
||||
t.Fatalf("expected recent gc event, got %#v", snapshot.RecentGCEvents)
|
||||
}
|
||||
if len(snapshot.Points) != 1 || snapshot.Points[0].ThreadCount != 18 || snapshot.Points[0].HeapUsedBytes != 503316480 {
|
||||
t.Fatalf("unexpected points snapshot: %#v", snapshot.Points)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoringManagerStartSamplesImmediatelyAndReturnsHistory(t *testing.T) {
|
||||
manager := newMonitoringManagerForTest(5)
|
||||
restore := swapMonitoringProviderFactory(func(mode string) (Provider, error) {
|
||||
return fakeMonitoringProvider{
|
||||
snapshot: JVMMonitoringSnapshot{
|
||||
Point: JVMMonitoringPoint{
|
||||
Timestamp: 1713945600000,
|
||||
ThreadCount: 12,
|
||||
HeapUsedBytes: 268435456,
|
||||
ProcessCpuLoad: 0.42,
|
||||
},
|
||||
AvailableMetrics: []string{"thread.count", "heap.used"},
|
||||
MissingMetrics: []string{"cpu.process"},
|
||||
ProviderWarnings: []string{"jmx cpu metric unavailable"},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
readOnly := true
|
||||
cfg := connection.ConnectionConfig{
|
||||
ID: "conn-monitor",
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
ReadOnly: &readOnly,
|
||||
PreferredMode: ModeJMX,
|
||||
AllowedModes: []string{ModeJMX},
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := manager.Start(context.Background(), cfg, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Start returned error: %v", err)
|
||||
}
|
||||
if !snapshot.Running {
|
||||
t.Fatalf("expected started session to be running")
|
||||
}
|
||||
if len(snapshot.Points) != 1 || snapshot.Points[0].ThreadCount != 12 || snapshot.Points[0].HeapUsedBytes != 268435456 {
|
||||
t.Fatalf("unexpected initial points: %#v", snapshot.Points)
|
||||
}
|
||||
|
||||
history, err := manager.GetHistory("conn-monitor", ModeJMX)
|
||||
if err != nil {
|
||||
t.Fatalf("GetHistory returned error: %v", err)
|
||||
}
|
||||
if len(history.MissingMetrics) != 1 || history.MissingMetrics[0] != "cpu.process" {
|
||||
t.Fatalf("unexpected history missing metrics: %#v", history.MissingMetrics)
|
||||
}
|
||||
if len(history.ProviderWarnings) != 1 {
|
||||
t.Fatalf("unexpected provider warnings: %#v", history.ProviderWarnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoringManagerStopMarksSessionStopped(t *testing.T) {
|
||||
manager := newMonitoringManagerForTest(5)
|
||||
restore := swapMonitoringProviderFactory(func(mode string) (Provider, error) {
|
||||
return fakeMonitoringProvider{
|
||||
snapshot: JVMMonitoringSnapshot{
|
||||
Point: JVMMonitoringPoint{Timestamp: 1713945600000, ThreadCount: 7},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
cfg := connection.ConnectionConfig{
|
||||
ID: "conn-stop",
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: ModeEndpoint,
|
||||
AllowedModes: []string{ModeEndpoint},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := manager.Start(context.Background(), cfg, ModeEndpoint); err != nil {
|
||||
t.Fatalf("Start returned error: %v", err)
|
||||
}
|
||||
if err := manager.Stop("conn-stop", ModeEndpoint); err != nil {
|
||||
t.Fatalf("Stop returned error: %v", err)
|
||||
}
|
||||
|
||||
history, err := manager.GetHistory("conn-stop", ModeEndpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("GetHistory returned error: %v", err)
|
||||
}
|
||||
if history.Running {
|
||||
t.Fatalf("expected session to stop running, got %#v", history)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoringSessionIgnoresStaleStopFromPreviousSampler(t *testing.T) {
|
||||
session := &monitoringSession{}
|
||||
|
||||
firstGeneration := session.reset("conn-race", ModeJMX)
|
||||
session.markRunning(firstGeneration)
|
||||
secondGeneration := session.reset("conn-race", ModeJMX)
|
||||
session.markRunning(secondGeneration)
|
||||
|
||||
session.markStopped(firstGeneration)
|
||||
if snapshot := session.snapshot(); !snapshot.Running {
|
||||
t.Fatalf("expected stale sampler stop to be ignored, got %#v", snapshot)
|
||||
}
|
||||
|
||||
session.markStopped(secondGeneration)
|
||||
if snapshot := session.snapshot(); snapshot.Running {
|
||||
t.Fatalf("expected active generation stop to mark stopped, got %#v", snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoringSessionIgnoresStalePointFromPreviousSampler(t *testing.T) {
|
||||
manager := newMonitoringManager(5, time.Millisecond)
|
||||
session := &monitoringSession{limit: 5}
|
||||
provider := &blockingMonitoringProvider{
|
||||
fakeMonitoringProvider: fakeMonitoringProvider{
|
||||
snapshot: JVMMonitoringSnapshot{
|
||||
Point: JVMMonitoringPoint{
|
||||
Timestamp: 1713945600000,
|
||||
ThreadCount: 8,
|
||||
},
|
||||
AvailableMetrics: []string{"thread.count"},
|
||||
},
|
||||
},
|
||||
started: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
}
|
||||
|
||||
firstGeneration := session.reset("conn-race", ModeJMX)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
manager.runSampler(ctx, provider, connection.ConnectionConfig{}, session, firstGeneration)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-provider.started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("sampler did not start within 1s")
|
||||
}
|
||||
|
||||
secondGeneration := session.reset("conn-race", ModeJMX)
|
||||
session.markRunning(secondGeneration)
|
||||
close(provider.release)
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("sampler did not stop within 1s")
|
||||
}
|
||||
|
||||
snapshot := session.snapshot()
|
||||
if !snapshot.Running {
|
||||
t.Fatalf("expected new generation to remain running, got %#v", snapshot)
|
||||
}
|
||||
if len(snapshot.Points) != 0 {
|
||||
t.Fatalf("expected stale sampler point to be ignored, got %#v", snapshot.Points)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalizeMonitoringSnapshotPreservesProviderDeltaWhenClassTotalMissing(t *testing.T) {
|
||||
snapshot := JVMMonitoringSnapshot{
|
||||
Point: JVMMonitoringPoint{
|
||||
Timestamp: 1713945602000,
|
||||
ClassLoadDelta: 3,
|
||||
},
|
||||
AvailableMetrics: []string{"class.delta"},
|
||||
}
|
||||
|
||||
finalizeMonitoringSnapshot(&snapshot, &JVMMonitoringPoint{
|
||||
Timestamp: 1713945600000,
|
||||
LoadedClassCount: 200,
|
||||
})
|
||||
|
||||
if snapshot.Point.ClassLoadDelta != 3 {
|
||||
t.Fatalf("expected provider class delta to be preserved, got %#v", snapshot.Point)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoringSamplerStopsAfterConsecutiveFailures(t *testing.T) {
|
||||
manager := newMonitoringManager(5, time.Millisecond)
|
||||
session := &monitoringSession{limit: 5}
|
||||
generation := session.reset("conn-fail", ModeJMX)
|
||||
session.markRunning(generation)
|
||||
provider := fakeMonitoringProvider{snapshotErr: errors.New("collector unavailable")}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
manager.runSampler(ctx, provider, connection.ConnectionConfig{}, session, generation)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
deadline := time.After(time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
snapshot := session.snapshot()
|
||||
if snapshot.Running {
|
||||
t.Fatalf("expected session to stop after consecutive failures, got %#v", snapshot)
|
||||
}
|
||||
if len(snapshot.ProviderWarnings) == 0 {
|
||||
t.Fatalf("expected provider warnings to explain sampling failure")
|
||||
}
|
||||
return
|
||||
case <-deadline:
|
||||
t.Fatal("sampler did not stop after consecutive failures")
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoringSessionDeduplicatesProviderWarnings(t *testing.T) {
|
||||
session := &monitoringSession{}
|
||||
|
||||
session.appendWarning("collector unavailable")
|
||||
session.appendWarning("collector unavailable")
|
||||
session.appendWarning(" collector unavailable ")
|
||||
|
||||
snapshot := session.snapshot()
|
||||
if len(snapshot.ProviderWarnings) != 1 {
|
||||
t.Fatalf("expected duplicate provider warnings to be collapsed, got %#v", snapshot.ProviderWarnings)
|
||||
}
|
||||
}
|
||||
90
internal/jvm/monitoring_types.go
Normal file
90
internal/jvm/monitoring_types.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package jvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
type JVMMonitoringPoint 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 RecentGCEvent struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Cause string `json:"cause,omitempty"`
|
||||
Action string `json:"action,omitempty"`
|
||||
DurationMs int64 `json:"durationMs,omitempty"`
|
||||
BeforeUsedBytes int64 `json:"beforeUsedBytes,omitempty"`
|
||||
AfterUsedBytes int64 `json:"afterUsedBytes,omitempty"`
|
||||
}
|
||||
|
||||
type MonitoringSessionSnapshot struct {
|
||||
ConnectionID string `json:"connectionId"`
|
||||
ProviderMode string `json:"providerMode"`
|
||||
Running bool `json:"running"`
|
||||
Points []JVMMonitoringPoint `json:"points,omitempty"`
|
||||
RecentGCEvents []RecentGCEvent `json:"recentGcEvents,omitempty"`
|
||||
AvailableMetrics []string `json:"availableMetrics,omitempty"`
|
||||
MissingMetrics []string `json:"missingMetrics,omitempty"`
|
||||
ProviderWarnings []string `json:"providerWarnings,omitempty"`
|
||||
}
|
||||
|
||||
type JVMMonitoringSnapshot struct {
|
||||
Point JVMMonitoringPoint `json:"point"`
|
||||
RecentGCEvents []RecentGCEvent `json:"recentGcEvents,omitempty"`
|
||||
AvailableMetrics []string `json:"availableMetrics,omitempty"`
|
||||
MissingMetrics []string `json:"missingMetrics,omitempty"`
|
||||
ProviderWarnings []string `json:"providerWarnings,omitempty"`
|
||||
}
|
||||
|
||||
type MonitoringCapableProvider interface {
|
||||
Provider
|
||||
GetMonitoringSnapshot(ctx context.Context, cfg connection.ConnectionConfig, previous *JVMMonitoringPoint) (JVMMonitoringSnapshot, error)
|
||||
}
|
||||
|
||||
func finalizeMonitoringSnapshot(snapshot *JVMMonitoringSnapshot, previous *JVMMonitoringPoint) {
|
||||
if snapshot == nil || previous == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if hasMonitoringMetric(snapshot.AvailableMetrics, "gc.count") && snapshot.Point.GCCollectionCount >= previous.GCCollectionCount {
|
||||
snapshot.Point.GCDeltaCount = snapshot.Point.GCCollectionCount - previous.GCCollectionCount
|
||||
}
|
||||
if hasMonitoringMetric(snapshot.AvailableMetrics, "gc.time") && snapshot.Point.GCCollectionTimeMs >= previous.GCCollectionTimeMs {
|
||||
snapshot.Point.GCDeltaTimeMs = snapshot.Point.GCCollectionTimeMs - previous.GCCollectionTimeMs
|
||||
}
|
||||
if hasMonitoringMetric(snapshot.AvailableMetrics, "class.loading") {
|
||||
snapshot.Point.ClassLoadDelta = int64(snapshot.Point.LoadedClassCount) - int64(previous.LoadedClassCount)
|
||||
}
|
||||
}
|
||||
|
||||
func hasMonitoringMetric(metrics []string, expected string) bool {
|
||||
for _, metric := range metrics {
|
||||
if metric == expected {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user