mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Complete mock drawer history coverage
Contract-Neutral: restores existing metricsTarget identity and complete mock history fallback contracts without changing their public shape
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "4d457159fc7684e7e63c9fd68b85db313300f2ed",
|
||||
"verified_at": "2026-08-26T11:03:57Z",
|
||||
"base_sha": "18b74c0adb07749cdd00caf9fc01ec09849029cd",
|
||||
"verified_at": "2026-08-26T11:10:47Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/utils/alertWebhookPresentation.ts"
|
||||
"frontend-modern/src/features/docker/dockerHostDrawerModel.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/utils/alertWebhookPresentation.ts": "aaf1bc2327b6b232470569f9110159a88222e7c8cd7b0bf4e473ff0c34eb5147"
|
||||
"frontend-modern/src/features/docker/dockerHostDrawerModel.ts": "af67dd2e6ae26a4c220ba77342842a5782a52dae77d664b5b56d37e996abdb79"
|
||||
},
|
||||
"routes": ["/alerts/notifications"],
|
||||
"routes": ["/docker/overview", "/proxmox/overview"],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
"width": 1230,
|
||||
"height": 1234
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
@@ -21,14 +21,17 @@
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"new Generic webhook editor with its default custom-payload placeholder visible",
|
||||
"translation-ready variable list showing Event, MessageKey, ResourceType and NodeDisplayName",
|
||||
"narrow webhook editor with no page-level horizontal overflow"
|
||||
"Docker host History at 24 hours and 7 days with utilization, network I/O, disk I/O, and thermals populated",
|
||||
"Docker app-container History at 24 hours and 7 days with utilization, network I/O, and disk I/O populated",
|
||||
"Proxmox guest History with utilization, network I/O, and disk I/O populated",
|
||||
"Proxmox node History at 24 hours and 7 days with utilization, network I/O, disk I/O, and thermals populated",
|
||||
"narrow Docker host History stacked without document-level horizontal overflow"
|
||||
],
|
||||
"interactions": [
|
||||
"signed into the current local mock build and opened Alerts, Notifications, then Add Webhook",
|
||||
"confirmed the Generic payload placeholder includes event, message_key and resource_type at desktop width",
|
||||
"confirmed the available-variable help exposes MessageKey, ResourceType and NodeDisplayName",
|
||||
"rechecked the open editor at 390 by 844 and measured document scroll width equal to the 390-pixel viewport"
|
||||
"opened Edge Apps 01 from the Docker hosts table and selected History; confirmed four chart groups and zero Collecting history placeholders",
|
||||
"opened nextcloud from the Docker containers table and selected History; confirmed three chart groups and zero Collecting history placeholders",
|
||||
"switched Docker host, Docker container, and Proxmox node History selectors to 7 days; every expected group remained populated",
|
||||
"opened checkout-web-265 and Analytics A on the Proxmox overview; guest and node History drawers rendered their full chart catalogs",
|
||||
"rechecked Edge Apps 01 at 390 by 844; confirmed four stacked groups, no horizontal overflow, and row focus return after collapse"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type HostOverrides = {
|
||||
agent?: { agentId?: string } | undefined;
|
||||
id?: string;
|
||||
name?: string;
|
||||
metricsTarget?: Resource['metricsTarget'];
|
||||
temperature?: number;
|
||||
docker?: { temperature?: number } | undefined;
|
||||
};
|
||||
@@ -25,12 +26,25 @@ const makeHost = (over: HostOverrides = {}): Resource =>
|
||||
name: over.name ?? '',
|
||||
type: 'docker-host',
|
||||
agent: over.agent,
|
||||
metricsTarget: over.metricsTarget,
|
||||
temperature: over.temperature,
|
||||
docker: over.docker,
|
||||
}) as unknown as Resource;
|
||||
|
||||
describe('dockerHostDrawerModel', () => {
|
||||
describe('getDockerHostDrawerHistoryTarget', () => {
|
||||
it('prefers the canonical backend metrics target over display and agent identities', () => {
|
||||
expect(
|
||||
getDockerHostDrawerHistoryTarget(
|
||||
makeHost({
|
||||
metricsTarget: { resourceType: 'docker-host', resourceId: ' runtime-host-9 ' },
|
||||
agent: { agentId: 'agent-9' },
|
||||
id: 'display-row-9',
|
||||
}),
|
||||
),
|
||||
).toEqual({ resourceType: 'docker-host', resourceId: 'runtime-host-9' });
|
||||
});
|
||||
|
||||
it('builds an agent-scoped target from the agent agentId', () => {
|
||||
expect(getDockerHostDrawerHistoryTarget(makeHost({ agent: { agentId: 'agent-9' } }))).toEqual(
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
} from '@/components/Workloads/guestDrawerModel';
|
||||
|
||||
export interface DockerHostDrawerHistoryTarget extends GuestDrawerHistoryTarget {
|
||||
resourceType: Extract<HistoryResourceType, 'agent'>;
|
||||
resourceType: Extract<HistoryResourceType, 'agent' | 'docker-host'>;
|
||||
}
|
||||
|
||||
const stripAgentPrefix = (value: string): string =>
|
||||
@@ -22,6 +22,12 @@ export const DOCKER_HOST_DRAWER_HISTORY_GROUPS: GuestDrawerHistoryGroupConfig[]
|
||||
export const getDockerHostDrawerHistoryTarget = (
|
||||
host: Resource,
|
||||
): DockerHostDrawerHistoryTarget | null => {
|
||||
const explicitType = host.metricsTarget?.resourceType;
|
||||
const explicitId = host.metricsTarget?.resourceId.trim();
|
||||
if ((explicitType === 'agent' || explicitType === 'docker-host') && explicitId) {
|
||||
return { resourceType: explicitType, resourceId: explicitId };
|
||||
}
|
||||
|
||||
const candidate = host.agent?.agentId || host.id || host.name || '';
|
||||
const resourceId = stripAgentPrefix(candidate.trim());
|
||||
if (!resourceId) return null;
|
||||
|
||||
@@ -201,6 +201,124 @@ func TestMetricsHistoryFallbackSynthesizesUnseededMockWorkloadHistory(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsHistoryFallbackSynthesizesMockHostAndNodeDrawerSeries(t *testing.T) {
|
||||
setMockModeForTest(t, true)
|
||||
|
||||
monitor := &monitoring.Monitor{}
|
||||
setUnexportedField(t, monitor, "metricsHistory", monitoring.NewMetricsHistory(10, time.Hour))
|
||||
router := &Router{monitor: monitor}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
resourceType string
|
||||
resourceID string
|
||||
metrics []string
|
||||
}{
|
||||
{
|
||||
name: "docker host",
|
||||
resourceType: "docker-host",
|
||||
resourceID: "runtime-host-9",
|
||||
metrics: []string{"cpu", "netin", "diskread", "temperature"},
|
||||
},
|
||||
{
|
||||
name: "Proxmox node",
|
||||
resourceType: "node",
|
||||
resourceID: "cluster-1-node-9",
|
||||
metrics: []string{"cpu", "netin", "temperature"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
"/api/metrics-store/history?resourceType="+tc.resourceType+"&resourceId="+tc.resourceID+"&range=24h",
|
||||
nil,
|
||||
)
|
||||
rec := httptest.NewRecorder()
|
||||
router.handleMetricsHistory(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Source string `json:"source"`
|
||||
Metrics map[string][]json.RawMessage `json:"metrics"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
if resp.Source != "mock_synthetic" {
|
||||
t.Fatalf("expected source mock_synthetic, got %q", resp.Source)
|
||||
}
|
||||
for _, metric := range tc.metrics {
|
||||
if len(resp.Metrics[metric]) < 2 {
|
||||
t.Fatalf("expected drawable %s history, got %d points", metric, len(resp.Metrics[metric]))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsHistoryFallbackSupplementsPartialMockStoreSeries(t *testing.T) {
|
||||
setMockModeForTest(t, true)
|
||||
|
||||
store, err := metrics.NewStore(metrics.DefaultConfig(t.TempDir()))
|
||||
if err != nil {
|
||||
t.Fatalf("metrics.NewStore() error = %v", err)
|
||||
}
|
||||
defer func() { _ = store.Close() }()
|
||||
|
||||
resourceID := "container-partial-1"
|
||||
now := time.Now().UTC()
|
||||
for _, sample := range []struct {
|
||||
offset time.Duration
|
||||
value float64
|
||||
}{
|
||||
{-50 * time.Minute, 15},
|
||||
{-25 * time.Minute, 25},
|
||||
{-time.Minute, 35},
|
||||
} {
|
||||
store.Write("dockerContainer", resourceID, "cpu", sample.value, now.Add(sample.offset))
|
||||
}
|
||||
store.Flush()
|
||||
|
||||
monitor := &monitoring.Monitor{}
|
||||
setUnexportedField(t, monitor, "metricsHistory", monitoring.NewMetricsHistory(10, time.Hour))
|
||||
setUnexportedField(t, monitor, "metricsStore", store)
|
||||
router := &Router{monitor: monitor}
|
||||
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
"/api/metrics-store/history?resourceType=app-container&resourceId="+resourceID+"&range=1h",
|
||||
nil,
|
||||
)
|
||||
rec := httptest.NewRecorder()
|
||||
router.handleMetricsHistory(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Source string `json:"source"`
|
||||
Metrics map[string][]json.RawMessage `json:"metrics"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
if resp.Source != "mock_synthetic" {
|
||||
t.Fatalf("expected supplemented source mock_synthetic, got %q", resp.Source)
|
||||
}
|
||||
if len(resp.Metrics["cpu"]) != 3 {
|
||||
t.Fatalf("expected the three stored CPU points to be preserved, got %d", len(resp.Metrics["cpu"]))
|
||||
}
|
||||
for _, metricType := range []string{"netin", "netout", "diskread", "diskwrite"} {
|
||||
if len(resp.Metrics[metricType]) < 2 {
|
||||
t.Fatalf("expected missing %s store history to be supplemented, got %d points", metricType, len(resp.Metrics[metricType]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsHistoryFallbackMockDiskSynthesizesSeries(t *testing.T) {
|
||||
setMockModeForTest(t, true)
|
||||
|
||||
|
||||
+40
-6
@@ -6827,7 +6827,7 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
return buildHistoryPoints(points, stepSecs), guestChartSource(), true
|
||||
case "node":
|
||||
points := monitor.GetNodeMetrics(resourceID, metricType, duration)
|
||||
points := monitor.GetNodeMetricsForChart(resourceID, metricType, duration)
|
||||
if len(points) == 0 {
|
||||
livePoints := liveMetricPoints(runtimeResourceType, resourceID)
|
||||
if live, ok := livePoints[metricType]; ok {
|
||||
@@ -6835,7 +6835,11 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
return nil, "", false
|
||||
}
|
||||
return buildHistoryPoints(points, stepSecs), historySourceMemory, true
|
||||
source := historySourceMemory
|
||||
if mock.IsMockEnabled() {
|
||||
source = historySourceMock
|
||||
}
|
||||
return buildHistoryPoints(points, stepSecs), source, true
|
||||
case "storage":
|
||||
metrics := monitor.GetStorageMetrics(resourceID, duration)
|
||||
points := metrics[queryMetric]
|
||||
@@ -6873,6 +6877,7 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
||||
|
||||
var metrics map[string][]monitoring.MetricPoint
|
||||
guestHistory := false
|
||||
source := historySourceMemory
|
||||
switch runtimeResourceType {
|
||||
case "vm", "system-container", "oci-container", "k8s", "docker-host":
|
||||
metrics, guestHistory = guestChartMetrics()
|
||||
@@ -6899,9 +6904,15 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
||||
default:
|
||||
if runtimeResourceType == "node" {
|
||||
metrics = map[string][]monitoring.MetricPoint{
|
||||
"cpu": monitor.GetNodeMetrics(resourceID, "cpu", duration),
|
||||
"memory": monitor.GetNodeMetrics(resourceID, "memory", duration),
|
||||
"disk": monitor.GetNodeMetrics(resourceID, "disk", duration),
|
||||
"cpu": monitor.GetNodeMetricsForChart(resourceID, "cpu", duration),
|
||||
"memory": monitor.GetNodeMetricsForChart(resourceID, "memory", duration),
|
||||
"disk": monitor.GetNodeMetricsForChart(resourceID, "disk", duration),
|
||||
"netin": monitor.GetNodeMetricsForChart(resourceID, "netin", duration),
|
||||
"netout": monitor.GetNodeMetricsForChart(resourceID, "netout", duration),
|
||||
"temperature": monitor.GetNodeMetricsForChart(resourceID, "temperature", duration),
|
||||
}
|
||||
if mock.IsMockEnabled() {
|
||||
source = historySourceMock
|
||||
}
|
||||
} else {
|
||||
return nil, "", false
|
||||
@@ -6909,7 +6920,6 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
apiData := make(map[string][]map[string]interface{})
|
||||
source := historySourceMemory
|
||||
if guestHistory {
|
||||
source = guestChartSource()
|
||||
}
|
||||
@@ -7165,6 +7175,30 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
||||
apiData[metric] = apiPoints
|
||||
}
|
||||
|
||||
// QueryAll can return a non-empty but incomplete mock metric map. Fill
|
||||
// only the absent series from the deterministic chart fallback so one
|
||||
// populated metric cannot suppress entire drawer groups.
|
||||
if mock.IsMockEnabled() {
|
||||
if fallbackData, fallbackSource, ok := fallbackAll(); ok {
|
||||
supplemented := false
|
||||
for metric, points := range fallbackData {
|
||||
if len(apiData[metric]) > 0 || len(points) == 0 {
|
||||
continue
|
||||
}
|
||||
apiData[metric] = points
|
||||
supplemented = true
|
||||
}
|
||||
if supplemented {
|
||||
source = fallbackSource
|
||||
log.Info().
|
||||
Str("resourceType", runtimeResourceType).
|
||||
Str("resourceId", resourceID).
|
||||
Str("source", source).
|
||||
Msg("Metrics store incomplete; supplementing missing mock history series")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = map[string]interface{}{
|
||||
"resourceType": responseResourceType,
|
||||
"resourceId": resourceID,
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
var (
|
||||
mockGuestChartMetricTypes = []string{"cpu", "memory", "disk", "diskread", "diskwrite", "netin", "netout"}
|
||||
mockNodeChartMetricTypes = []string{"cpu", "memory", "disk", "netin", "netout"}
|
||||
mockHostChartMetricTypes = []string{"cpu", "memory", "disk", "diskread", "diskwrite", "netin", "netout", "temperature"}
|
||||
mockNodeChartMetricTypes = []string{"cpu", "memory", "disk", "netin", "netout", "temperature"}
|
||||
mockDiskChartMetricTypes = []string{"disk", "diskread", "diskwrite", "smart_temp"}
|
||||
)
|
||||
|
||||
@@ -77,8 +78,12 @@ func mockGuestMetricsForChart(resourceType, resourceID string, duration time.Dur
|
||||
}
|
||||
|
||||
timestamps := mockChartTimestamps(duration)
|
||||
result := make(map[string][]MetricPoint, len(mockGuestChartMetricTypes)+1)
|
||||
for _, metricType := range mockGuestChartMetricTypes {
|
||||
metricTypes := mockGuestChartMetricTypes
|
||||
if resourceType == "agent" || resourceType == "dockerHost" {
|
||||
metricTypes = mockHostChartMetricTypes
|
||||
}
|
||||
result := make(map[string][]MetricPoint, len(metricTypes)+1)
|
||||
for _, metricType := range metricTypes {
|
||||
result[metricType] = mockCanonicalMetricSeries(resourceType, resourceID, metricType, timestamps)
|
||||
}
|
||||
if series := mockGuestMemoryUsedSeries(resourceType, resourceID, result["memory"]); len(series) > 0 {
|
||||
|
||||
@@ -700,11 +700,17 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, graph mock.Fi
|
||||
cpuSeries := canonicalMetricSeriesWithSampler(sampler, "node", node.ID, "cpu", seedTimestamps)
|
||||
memSeries := canonicalMetricSeriesWithSampler(sampler, "node", node.ID, "memory", seedTimestamps)
|
||||
diskSeries := canonicalMetricSeriesWithSampler(sampler, "node", node.ID, "disk", seedTimestamps)
|
||||
netInSeries := canonicalMetricSeriesWithSampler(sampler, "node", node.ID, "netin", seedTimestamps)
|
||||
netOutSeries := canonicalMetricSeriesWithSampler(sampler, "node", node.ID, "netout", seedTimestamps)
|
||||
temperatureSeries := canonicalMetricSeriesWithSampler(sampler, "node", node.ID, "temperature", seedTimestamps)
|
||||
|
||||
mh.addNodeMetricSeries(node.ID, "cpu", cpuSeries, seedTimestamps)
|
||||
mh.addNodeMetricSeries(node.ID, "memory", memSeries, seedTimestamps)
|
||||
mh.addNodeMetricSeries(node.ID, "disk", diskSeries, seedTimestamps)
|
||||
seedStoreSeries("node", node.ID, "cpu", "memory", "disk")
|
||||
mh.addNodeMetricSeries(node.ID, "netin", netInSeries, seedTimestamps)
|
||||
mh.addNodeMetricSeries(node.ID, "netout", netOutSeries, seedTimestamps)
|
||||
mh.addNodeMetricSeries(node.ID, "temperature", temperatureSeries, seedTimestamps)
|
||||
seedStoreSeries("node", node.ID, "cpu", "memory", "disk", "netin", "netout", "temperature")
|
||||
}
|
||||
|
||||
recordGuest := func(
|
||||
@@ -777,6 +783,24 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, graph mock.Fi
|
||||
}
|
||||
seedStoreSeries(storeType, storeID, storeMetrics...)
|
||||
}
|
||||
recordGuestTemperature := func(metricIDs []string, storeType, storeID string) {
|
||||
if len(metricIDs) == 0 || strings.TrimSpace(storeID) == "" {
|
||||
return
|
||||
}
|
||||
temperatureSeries := canonicalMetricSeriesWithSampler(
|
||||
sampler,
|
||||
storeType,
|
||||
storeID,
|
||||
"temperature",
|
||||
seedTimestamps,
|
||||
)
|
||||
for _, metricID := range metricIDs {
|
||||
if id := strings.TrimSpace(metricID); id != "" {
|
||||
mh.addGuestMetricSeries(id, "temperature", temperatureSeries, seedTimestamps)
|
||||
}
|
||||
}
|
||||
seedStoreSeries(storeType, storeID, "temperature")
|
||||
}
|
||||
recordGuestMemoryUsed := func(metricIDs []string, storeType, storeID string, memoryTotal float64) {
|
||||
if len(metricIDs) == 0 || strings.TrimSpace(storeID) == "" || memoryTotal <= 0 {
|
||||
return
|
||||
@@ -947,6 +971,7 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, graph mock.Fi
|
||||
true,
|
||||
true,
|
||||
)
|
||||
recordGuestTemperature([]string{"dockerHost:" + host.ID}, "dockerHost", host.ID)
|
||||
|
||||
for _, container := range host.Containers {
|
||||
if container.ID == "" {
|
||||
@@ -957,8 +982,8 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, graph mock.Fi
|
||||
"dockerContainer",
|
||||
container.ID,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -977,6 +1002,7 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, graph mock.Fi
|
||||
true,
|
||||
true,
|
||||
)
|
||||
recordGuestTemperature([]string{"agent:" + host.ID}, "agent", host.ID)
|
||||
}
|
||||
|
||||
platformFixtures := graph.PlatformFixtures
|
||||
@@ -996,6 +1022,7 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, graph mock.Fi
|
||||
true,
|
||||
true,
|
||||
)
|
||||
recordGuestTemperature(systemMetricIDs, "agent", trueNASFixtures.System.Hostname)
|
||||
|
||||
for _, pool := range trueNASFixtures.Pools {
|
||||
poolKey := mock.TrueNASPoolMetricID(trueNASFixtures.System.Hostname, pool.Name)
|
||||
@@ -1465,9 +1492,13 @@ func recordMockStateToMetricsHistory(mh *MetricsHistory, ms *metrics.Store, grap
|
||||
cpu := sampler.SampleMetric("node", node.ID, "cpu", ts)
|
||||
memory := sampler.SampleMetric("node", node.ID, "memory", ts)
|
||||
disk := sampler.SampleMetric("node", node.ID, "disk", ts)
|
||||
netIn := sampler.SampleMetric("node", node.ID, "netin", ts)
|
||||
netOut := sampler.SampleMetric("node", node.ID, "netout", ts)
|
||||
mh.AddNodeMetric(node.ID, "cpu", cpu, ts)
|
||||
mh.AddNodeMetric(node.ID, "memory", memory, ts)
|
||||
mh.AddNodeMetric(node.ID, "disk", disk, ts)
|
||||
mh.AddNodeMetric(node.ID, "netin", netIn, ts)
|
||||
mh.AddNodeMetric(node.ID, "netout", netOut, ts)
|
||||
if temperature := nodePrimaryTemperatureCelsius(node.Temperature); temperature != nil {
|
||||
mh.AddNodeMetric(node.ID, "temperature", *temperature, ts)
|
||||
}
|
||||
@@ -1476,6 +1507,8 @@ func recordMockStateToMetricsHistory(mh *MetricsHistory, ms *metrics.Store, grap
|
||||
ms.Write("node", node.ID, "cpu", cpu, ts)
|
||||
ms.Write("node", node.ID, "memory", memory, ts)
|
||||
ms.Write("node", node.ID, "disk", disk, ts)
|
||||
ms.Write("node", node.ID, "netin", netIn, ts)
|
||||
ms.Write("node", node.ID, "netout", netOut, ts)
|
||||
if temperature := nodePrimaryTemperatureCelsius(node.Temperature); temperature != nil {
|
||||
ms.Write("node", node.ID, "temperature", *temperature, ts)
|
||||
}
|
||||
@@ -1622,14 +1655,29 @@ func recordMockStateToMetricsHistory(mh *MetricsHistory, ms *metrics.Store, grap
|
||||
cpu := sampler.SampleMetric("dockerHost", host.ID, "cpu", ts)
|
||||
memory := sampler.SampleMetric("dockerHost", host.ID, "memory", ts)
|
||||
disk := sampler.SampleMetric("dockerHost", host.ID, "disk", ts)
|
||||
diskRead := sampler.SampleMetric("dockerHost", host.ID, "diskread", ts)
|
||||
diskWrite := sampler.SampleMetric("dockerHost", host.ID, "diskwrite", ts)
|
||||
netIn := sampler.SampleMetric("dockerHost", host.ID, "netin", ts)
|
||||
netOut := sampler.SampleMetric("dockerHost", host.ID, "netout", ts)
|
||||
temperature := sampler.SampleMetric("dockerHost", host.ID, "temperature", ts)
|
||||
mh.AddGuestMetric(hostKey, "cpu", cpu, ts)
|
||||
mh.AddGuestMetric(hostKey, "memory", memory, ts)
|
||||
mh.AddGuestMetric(hostKey, "disk", disk, ts)
|
||||
mh.AddGuestMetric(hostKey, "diskread", diskRead, ts)
|
||||
mh.AddGuestMetric(hostKey, "diskwrite", diskWrite, ts)
|
||||
mh.AddGuestMetric(hostKey, "netin", netIn, ts)
|
||||
mh.AddGuestMetric(hostKey, "netout", netOut, ts)
|
||||
mh.AddGuestMetric(hostKey, "temperature", temperature, ts)
|
||||
|
||||
if ms != nil {
|
||||
ms.Write("dockerHost", host.ID, "cpu", cpu, ts)
|
||||
ms.Write("dockerHost", host.ID, "memory", memory, ts)
|
||||
ms.Write("dockerHost", host.ID, "disk", disk, ts)
|
||||
ms.Write("dockerHost", host.ID, "diskread", diskRead, ts)
|
||||
ms.Write("dockerHost", host.ID, "diskwrite", diskWrite, ts)
|
||||
ms.Write("dockerHost", host.ID, "netin", netIn, ts)
|
||||
ms.Write("dockerHost", host.ID, "netout", netOut, ts)
|
||||
ms.Write("dockerHost", host.ID, "temperature", temperature, ts)
|
||||
}
|
||||
|
||||
for _, container := range host.Containers {
|
||||
@@ -1641,14 +1689,26 @@ func recordMockStateToMetricsHistory(mh *MetricsHistory, ms *metrics.Store, grap
|
||||
cpu := sampler.SampleMetric("dockerContainer", container.ID, "cpu", ts)
|
||||
memory := sampler.SampleMetric("dockerContainer", container.ID, "memory", ts)
|
||||
disk := sampler.SampleMetric("dockerContainer", container.ID, "disk", ts)
|
||||
diskRead := sampler.SampleMetric("dockerContainer", container.ID, "diskread", ts)
|
||||
diskWrite := sampler.SampleMetric("dockerContainer", container.ID, "diskwrite", ts)
|
||||
netIn := sampler.SampleMetric("dockerContainer", container.ID, "netin", ts)
|
||||
netOut := sampler.SampleMetric("dockerContainer", container.ID, "netout", ts)
|
||||
mh.AddGuestMetric(metricKey, "cpu", cpu, ts)
|
||||
mh.AddGuestMetric(metricKey, "memory", memory, ts)
|
||||
mh.AddGuestMetric(metricKey, "disk", disk, ts)
|
||||
mh.AddGuestMetric(metricKey, "diskread", diskRead, ts)
|
||||
mh.AddGuestMetric(metricKey, "diskwrite", diskWrite, ts)
|
||||
mh.AddGuestMetric(metricKey, "netin", netIn, ts)
|
||||
mh.AddGuestMetric(metricKey, "netout", netOut, ts)
|
||||
|
||||
if ms != nil {
|
||||
ms.Write("dockerContainer", container.ID, "cpu", cpu, ts)
|
||||
ms.Write("dockerContainer", container.ID, "memory", memory, ts)
|
||||
ms.Write("dockerContainer", container.ID, "disk", disk, ts)
|
||||
ms.Write("dockerContainer", container.ID, "diskread", diskRead, ts)
|
||||
ms.Write("dockerContainer", container.ID, "diskwrite", diskWrite, ts)
|
||||
ms.Write("dockerContainer", container.ID, "netin", netIn, ts)
|
||||
ms.Write("dockerContainer", container.ID, "netout", netOut, ts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1898,6 +1898,9 @@ func TestRecordMockStateToMetricsHistory_UsesCanonicalMetricModelForStateBackedR
|
||||
if got, want := mh.GetNodeMetrics("node-live", "cpu", lookback)[0].Value, mock.SampleMetric("node", "node-live", "cpu", ts); math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("expected node cpu live tick to use canonical metric model: got=%f want=%f", got, want)
|
||||
}
|
||||
if got, want := mh.GetNodeMetrics("node-live", "netin", lookback)[0].Value, mock.SampleMetric("node", "node-live", "netin", ts); math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("expected node netin live tick to use canonical metric model: got=%f want=%f", got, want)
|
||||
}
|
||||
if got, want := mh.GetGuestMetrics("vm-live", "diskread", lookback)[0].Value, mock.SampleMetric("vm", "vm-live", "diskread", ts); math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("expected vm diskread live tick to use canonical metric model: got=%f want=%f", got, want)
|
||||
}
|
||||
@@ -1921,9 +1924,15 @@ func TestRecordMockStateToMetricsHistory_UsesCanonicalMetricModelForStateBackedR
|
||||
if got, want := mh.GetGuestMetrics("dockerHost:docker-host-live", "disk", lookback)[0].Value, mock.SampleMetric("dockerHost", "docker-host-live", "disk", ts); math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("expected docker host disk live tick to use canonical metric model: got=%f want=%f", got, want)
|
||||
}
|
||||
if got, want := mh.GetGuestMetrics("dockerHost:docker-host-live", "temperature", lookback)[0].Value, mock.SampleMetric("dockerHost", "docker-host-live", "temperature", ts); math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("expected docker host temperature live tick to use canonical metric model: got=%f want=%f", got, want)
|
||||
}
|
||||
if got, want := mh.GetGuestMetrics("docker:docker-cont-live", "cpu", lookback)[0].Value, mock.SampleMetric("dockerContainer", "docker-cont-live", "cpu", ts); math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("expected docker container cpu live tick to use canonical metric model: got=%f want=%f", got, want)
|
||||
}
|
||||
if got, want := mh.GetGuestMetrics("docker:docker-cont-live", "netin", lookback)[0].Value, mock.SampleMetric("dockerContainer", "docker-cont-live", "netin", ts); math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("expected docker container netin live tick to use canonical metric model: got=%f want=%f", got, want)
|
||||
}
|
||||
if got, want := mh.GetGuestMetrics("agent:agent-live", "netin", lookback)[0].Value, mock.SampleMetric("agent", "agent-live", "netin", ts); math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("expected agent netin live tick to use canonical metric model: got=%f want=%f", got, want)
|
||||
}
|
||||
@@ -2058,10 +2067,56 @@ func TestMockDockerHostSeedCoversTheSameSeriesAsTheSyntheticGenerator(t *testing
|
||||
seedMockMetricsHistory(history, nil, graph, now, 2*time.Hour, time.Minute)
|
||||
|
||||
hostID := graph.State.DockerHosts[0].ID
|
||||
for _, metricType := range mockGuestChartMetricTypes {
|
||||
for _, metricType := range mockHostChartMetricTypes {
|
||||
points := history.GetGuestMetrics("dockerHost:"+hostID, metricType, 2*time.Hour)
|
||||
if len(points) == 0 {
|
||||
t.Fatalf("seeded docker host %s is missing the %s series", hostID, metricType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockDockerContainerSeedCoversDrawerHistorySeries(t *testing.T) {
|
||||
previous := mock.IsMockEnabled()
|
||||
mustSetMockEnabled(t, true)
|
||||
defer mustSetMockEnabled(t, previous)
|
||||
|
||||
history := NewMetricsHistory(4096, 24*time.Hour)
|
||||
graph := mock.CurrentFixtureGraph()
|
||||
if len(graph.State.DockerHosts) == 0 || len(graph.State.DockerHosts[0].Containers) == 0 {
|
||||
t.Fatal("mock fixture graph has no Docker containers")
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
seedMockMetricsHistory(history, nil, graph, now, 2*time.Hour, time.Minute)
|
||||
|
||||
containerID := graph.State.DockerHosts[0].Containers[0].ID
|
||||
for _, metricType := range mockGuestChartMetricTypes {
|
||||
points := history.GetGuestMetrics("docker:"+containerID, metricType, 2*time.Hour)
|
||||
if len(points) == 0 {
|
||||
t.Fatalf("seeded Docker container %s is missing the %s series", containerID, metricType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockNodeSeedCoversDrawerHistorySeries(t *testing.T) {
|
||||
previous := mock.IsMockEnabled()
|
||||
mustSetMockEnabled(t, true)
|
||||
defer mustSetMockEnabled(t, previous)
|
||||
|
||||
history := NewMetricsHistory(4096, 24*time.Hour)
|
||||
graph := mock.CurrentFixtureGraph()
|
||||
if len(graph.State.Nodes) == 0 {
|
||||
t.Fatal("mock fixture graph has no Proxmox nodes")
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
seedMockMetricsHistory(history, nil, graph, now, 2*time.Hour, time.Minute)
|
||||
|
||||
nodeID := graph.State.Nodes[0].ID
|
||||
for _, metricType := range mockNodeChartMetricTypes {
|
||||
points := history.GetNodeMetrics(nodeID, metricType, 2*time.Hour)
|
||||
if len(points) == 0 {
|
||||
t.Fatalf("seeded node %s is missing the %s series", nodeID, metricType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1319,17 +1319,28 @@ func (m *Monitor) mockGuestMetricsForChart(
|
||||
return cached
|
||||
}
|
||||
|
||||
var computed map[string][]MetricPoint
|
||||
switch {
|
||||
case hasSufficientChartMapCoverage(inMemoryResult, duration):
|
||||
computed = downsampleMetricMapForMockChart(inMemoryResult, duration)
|
||||
case len(sqlResourceID) > 0:
|
||||
if synthetic := mockGuestMetricsForChart(sqlResourceType, sqlResourceID, duration); len(synthetic) > 0 {
|
||||
computed = downsampleMetricMapForMockChart(synthetic, duration)
|
||||
// Resolve mock coverage per series, not per resource. A resource can have a
|
||||
// full window of CPU/memory/disk while I/O or thermal series are absent; a
|
||||
// resource-level coverage check would then return the partial map unchanged
|
||||
// and leave whole drawer groups stuck in "Collecting history".
|
||||
synthetic := mockGuestMetricsForChart(sqlResourceType, sqlResourceID, duration)
|
||||
computed := make(map[string][]MetricPoint, len(inMemoryResult)+len(synthetic))
|
||||
for metricType, points := range inMemoryResult {
|
||||
if hasSufficientChartSeriesCoverage(points, duration) {
|
||||
computed[metricType] = downsampleMetricSeriesForMockChart(points, duration)
|
||||
continue
|
||||
}
|
||||
if fallback := synthetic[metricType]; len(fallback) > 0 {
|
||||
computed[metricType] = downsampleMetricSeriesForMockChart(fallback, duration)
|
||||
continue
|
||||
}
|
||||
computed[metricType] = downsampleMetricSeriesForMockChart(points, duration)
|
||||
}
|
||||
if computed == nil {
|
||||
computed = downsampleMetricMapForMockChart(inMemoryResult, duration)
|
||||
for metricType, points := range synthetic {
|
||||
if len(computed[metricType]) > 0 {
|
||||
continue
|
||||
}
|
||||
computed[metricType] = downsampleMetricSeriesForMockChart(points, duration)
|
||||
}
|
||||
return m.writeMockChartMetricMapCache(key, computed)
|
||||
}
|
||||
|
||||
@@ -548,6 +548,66 @@ func TestGetGuestMetricsForChart_UsesCanonicalMockSamplerInMockMode(t *testing.T
|
||||
assertFollowsCanonicalMockSeries(t, result["memory"], "vm", "vm-1", "memory")
|
||||
}
|
||||
|
||||
func TestGetGuestMetricsForChart_SynthesizesMockHostTemperature(t *testing.T) {
|
||||
previous := mock.IsMockEnabled()
|
||||
mustSetMockEnabled(t, true)
|
||||
defer mustSetMockEnabled(t, previous)
|
||||
|
||||
monitor := newChartFallbackTestMonitor(t)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
inMemoryKey string
|
||||
sqlResourceType string
|
||||
resourceID string
|
||||
}{
|
||||
{name: "agent", inMemoryKey: "agent:host-1", sqlResourceType: "agent", resourceID: "host-1"},
|
||||
{name: "docker host", inMemoryKey: "dockerHost:runtime-1", sqlResourceType: "dockerHost", resourceID: "runtime-1"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := monitor.GetGuestMetricsForChart(tc.inMemoryKey, tc.sqlResourceType, tc.resourceID, time.Hour)
|
||||
if len(result["temperature"]) < 2 {
|
||||
t.Fatalf("expected drawable mock temperature history, got %+v", result["temperature"])
|
||||
}
|
||||
assertFollowsCanonicalMockSeries(t, result["temperature"], tc.sqlResourceType, tc.resourceID, "temperature")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGuestMetricsForChart_SupplementsPartialMockHistoryPerSeries(t *testing.T) {
|
||||
previous := mock.IsMockEnabled()
|
||||
mustSetMockEnabled(t, true)
|
||||
defer mustSetMockEnabled(t, previous)
|
||||
|
||||
monitor := newChartFallbackTestMonitor(t)
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
duration := time.Hour
|
||||
for _, sample := range []struct {
|
||||
offset time.Duration
|
||||
value float64
|
||||
}{
|
||||
{-58 * time.Minute, 12.5},
|
||||
{-30 * time.Minute, 37.25},
|
||||
{-1 * time.Minute, 81.75},
|
||||
} {
|
||||
monitor.metricsHistory.AddGuestMetric("docker:container-1", "cpu", sample.value, now.Add(sample.offset))
|
||||
}
|
||||
|
||||
result := monitor.GetGuestMetricsForChart(
|
||||
"docker:container-1",
|
||||
"dockerContainer",
|
||||
"container-1",
|
||||
duration,
|
||||
)
|
||||
if len(result["cpu"]) != 3 || result["cpu"][0].Value != 12.5 || result["cpu"][2].Value != 81.75 {
|
||||
t.Fatalf("expected seeded CPU history to win, got %+v", result["cpu"])
|
||||
}
|
||||
for _, metricType := range []string{"netin", "netout", "diskread", "diskwrite"} {
|
||||
if len(result[metricType]) < 2 {
|
||||
t.Fatalf("expected missing %s history to be synthesized, got %+v", metricType, result[metricType])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGuestMetricsForChart_PrefersSeededMockHistoryInMockMode(t *testing.T) {
|
||||
previous := mock.IsMockEnabled()
|
||||
mustSetMockEnabled(t, true)
|
||||
|
||||
@@ -322,7 +322,7 @@ func TestBranchcov0723Am_MockNodeMetricsForChart(t *testing.T) {
|
||||
// Comprehensive ordering check across the default set + a custom type,
|
||||
// exercising every codepath that materialises MetricPoint slices.
|
||||
types := append([]string(nil), mockNodeChartMetricTypes...)
|
||||
types = append(types, "temperature")
|
||||
types = append(types, "custom_ordered_metric")
|
||||
got := mockNodeMetricsForChart(nodeID, types, 2*time.Hour)
|
||||
expectKeysExact(t, got, types)
|
||||
for k, pts := range got {
|
||||
|
||||
Reference in New Issue
Block a user