mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-19 16:27:37 +00:00
Make Relay history entitlement enforceable
This commit is contained in:
parent
f060f261cd
commit
e16f15b398
18 changed files with 229 additions and 40 deletions
|
|
@ -775,6 +775,11 @@ must flow through the canonical `/api/metrics-store/history` boundary and the
|
|||
disk `MetricsTarget.ResourceID` that monitoring projects for the resource,
|
||||
rather than reviving a browser-local collector or a lifecycle-only
|
||||
agent/device identity.
|
||||
That shared metrics-history boundary may enforce commercial history windows
|
||||
such as Relay 14-day and Pro 90-day retention for operator charts, but lifecycle
|
||||
surfaces must treat those windows as presentation entitlements only. Agent
|
||||
registration, heartbeat, installer status, and fleet freshness must not infer
|
||||
lifecycle truth from whether a longer chart range is enabled or denied.
|
||||
That same adjacent API boundary now also owns internal demo-fixture runtime
|
||||
gating. Lifecycle-adjacent install, reporting, and demo-facing flows may
|
||||
share mock-mode handlers in dev and test, but release builds must authorize
|
||||
|
|
|
|||
|
|
@ -801,6 +801,13 @@ the canonical cluster source key, node history on
|
|||
`k8s:<cluster>:pod:<uid-or-namespace/name>`, and deployment history on
|
||||
`<cluster>:deployment:<uid-or-namespace/name>`, so demo and live workload
|
||||
detail charts all resolve through one governed identity contract.
|
||||
That same metrics-history contract also owns commercial history-range
|
||||
enforcement. `frontend-modern/src/api/charts.ts` may expose `14d` as a
|
||||
first-class `HistoryTimeRange`, and `/api/metrics-store/history` must parse
|
||||
positive day ranges before querying the store so entitlement checks cannot be
|
||||
bypassed with duration syntax. Community instances must remain capped at seven
|
||||
days, Relay must allow 14 days and reject longer history, and Pro-tier
|
||||
entitlements must continue to allow 90-day history.
|
||||
The Pulse Account commercial shell now also owns a dedicated bootstrap
|
||||
contract in `internal/cloudcp/portal/page.go`, `internal/cloudcp/portal/handlers.go`,
|
||||
and `internal/cloudcp/portal/handlers_test.go`. `/api/portal/bootstrap` and
|
||||
|
|
|
|||
|
|
@ -1447,6 +1447,12 @@ ordinary self-hosted surfaces must stay informational rather than presenting
|
|||
trial-start or upgrade-link actions. Future history-chart work should extend
|
||||
those owners instead of pushing fetch, license, commercial trial actions, or
|
||||
canvas math back into the shared component shell.
|
||||
The shared history range catalog is also owned here. The canonical product
|
||||
range sequence is `24h`, `7d`, `14d`, `30d`, and `90d`, with `14d` preserved
|
||||
as the Relay entitlement surface rather than hidden behind the Pro-only
|
||||
long-range controls. Lock copy must derive its target days and tier label from
|
||||
the selected range instead of assuming every locked history selection is a
|
||||
30-day or 90-day Pro ask.
|
||||
The remaining header, overlay, and tooltip render surfaces now live in
|
||||
`frontend-modern/src/components/shared/HistoryChartHeader.tsx`,
|
||||
`frontend-modern/src/components/shared/HistoryChartOverlay.tsx`, and
|
||||
|
|
|
|||
|
|
@ -533,6 +533,12 @@ onto the shared backend `resourceType=k8s` transport, but it must preserve the
|
|||
canonical frontend target types for clusters, nodes, pods, and deployments so
|
||||
the workloads and drawer hot paths do not silently drop deployment history or
|
||||
split Kubernetes charts across incompatible cache keys.
|
||||
That same protected metrics-store boundary also owns tiered history range
|
||||
parsing as pre-query work. Day-based ranges such as `14d`, longer day ranges,
|
||||
and equivalent duration syntax must be normalized and checked against the
|
||||
licensed `max_history_days` before the store read is planned, so denied history
|
||||
windows fail without widening DB scans or letting duration strings bypass the
|
||||
Relay 14-day and Pro 90-day budgets.
|
||||
That same protected metrics-store boundary also owns write-path churn on local
|
||||
instances. `pkg/metrics/store.go` must prefer fewer, larger SQLite write
|
||||
transactions over tiny frequent commits: the default write buffer should stay
|
||||
|
|
|
|||
|
|
@ -825,6 +825,11 @@ resource's canonical history target. Storage must not keep a drawer-local live
|
|||
metrics collector, agent-id/device fallback stream, or separate real-time
|
||||
history store once monitoring and `/api/metrics-store/history` already own the
|
||||
disk timeline.
|
||||
Storage pool and disk detail range selectors must mirror the shared history
|
||||
chart entitlement sequence. They must expose `14d` between `7d` and `30d` and
|
||||
pass the selected range through to `HistoryChart` unchanged, rather than
|
||||
inventing storage-local range catalogs, paid-tier labels, or alternate
|
||||
metrics-history gating.
|
||||
Shared chart transport that storage and recovery coexist with must also stay
|
||||
on rendered-metric budgets. When `internal/api/router.go` batches workload
|
||||
history for adjacent overview or shared summary cards, it may parallelize the
|
||||
|
|
|
|||
|
|
@ -84,12 +84,12 @@ describe('ChartsAPI', () => {
|
|||
resourceType: 'agent',
|
||||
resourceId: 'agent-1',
|
||||
metric: 'cpu',
|
||||
range: '7d',
|
||||
range: '14d',
|
||||
maxPoints: 321.4,
|
||||
});
|
||||
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith(
|
||||
'/api/metrics-store/history?resourceType=agent&resourceId=agent-1&metric=cpu&range=7d&maxPoints=321',
|
||||
'/api/metrics-store/history?resourceType=agent&resourceId=agent-1&metric=cpu&range=14d&maxPoints=321',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,16 @@ export interface StorageSummaryTrendResponse {
|
|||
}
|
||||
|
||||
// Persistent metrics history types (SQLite-backed, longer retention)
|
||||
export type HistoryTimeRange = '30m' | '1h' | '6h' | '12h' | '24h' | '7d' | '30d' | '90d';
|
||||
export type HistoryTimeRange =
|
||||
| '30m'
|
||||
| '1h'
|
||||
| '6h'
|
||||
| '12h'
|
||||
| '24h'
|
||||
| '7d'
|
||||
| '14d'
|
||||
| '30d'
|
||||
| '90d';
|
||||
type MetricsHistoryAPIResourceType =
|
||||
| 'vm'
|
||||
| 'system-container'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render, screen } from '@solidjs/testing-library';
|
||||
import { fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { StoragePoolDetail } from '@/components/Storage/StoragePoolDetail';
|
||||
import type { StorageRecord } from '@/features/storageBackups/models';
|
||||
|
|
@ -7,11 +7,11 @@ import type { Resource } from '@/types/resource';
|
|||
const historyChartSpy = vi.fn();
|
||||
|
||||
vi.mock('@/components/shared/HistoryChart', () => ({
|
||||
HistoryChart: (props: { resourceType: string; resourceId: string; metric: string }) => {
|
||||
HistoryChart: (props: { resourceType: string; resourceId: string; metric: string; range?: string }) => {
|
||||
historyChartSpy(props);
|
||||
return (
|
||||
<div data-testid="history-chart">
|
||||
{props.resourceType}:{props.resourceId}:{props.metric}
|
||||
{props.resourceType}:{props.resourceId}:{props.metric}:{props.range}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
@ -60,6 +60,45 @@ describe('StoragePoolDetail', () => {
|
|||
resourceType: 'storage',
|
||||
resourceId: 'pool:tank',
|
||||
metric: 'usage',
|
||||
range: '7d',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the Relay 14-day range available in pool detail charts', () => {
|
||||
historyChartSpy.mockClear();
|
||||
|
||||
render(() => (
|
||||
<table>
|
||||
<tbody>
|
||||
<StoragePoolDetail
|
||||
record={makeRecord({
|
||||
metricsTarget: { resourceType: 'storage', resourceId: 'pool:tank' },
|
||||
})}
|
||||
physicalDisks={[]}
|
||||
summarySeriesId="pool:tank"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
));
|
||||
|
||||
const rangeSelector = screen.getByRole('combobox') as HTMLSelectElement;
|
||||
expect(Array.from(rangeSelector.options).map((option) => option.value)).toEqual([
|
||||
'24h',
|
||||
'7d',
|
||||
'14d',
|
||||
'30d',
|
||||
'90d',
|
||||
]);
|
||||
|
||||
fireEvent.change(rangeSelector, { target: { value: '14d' } });
|
||||
|
||||
expect(historyChartSpy).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
resourceType: 'storage',
|
||||
resourceId: 'pool:tank',
|
||||
metric: 'usage',
|
||||
range: '14d',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import historyChartModelSource from '@/components/shared/historyChartModel.ts?ra
|
|||
import historyChartStateSource from '@/components/shared/useHistoryChartState.ts?raw';
|
||||
import historyChartTooltipSource from '@/components/shared/HistoryChartTooltip.tsx?raw';
|
||||
import { HistoryChart } from '@/components/shared/HistoryChart';
|
||||
import { HISTORY_CHART_RANGES } from '@/components/shared/historyChartModel';
|
||||
|
||||
if (typeof globalThis.ResizeObserver === 'undefined') {
|
||||
globalThis.ResizeObserver = class ResizeObserver {
|
||||
|
|
@ -107,4 +108,8 @@ describe('HistoryChart', () => {
|
|||
|
||||
expect(screen.getByText('History')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('exposes the Relay history range as a first-class chart option', () => {
|
||||
expect(HISTORY_CHART_RANGES).toEqual(['24h', '7d', '14d', '30d', '90d']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export interface HistoryChartTooltipLayout {
|
|||
height: number;
|
||||
}
|
||||
|
||||
export const HISTORY_CHART_RANGES: HistoryTimeRange[] = ['24h', '7d', '30d', '90d'];
|
||||
export const HISTORY_CHART_RANGES: HistoryTimeRange[] = ['24h', '7d', '14d', '30d', '90d'];
|
||||
|
||||
export function formatHistoryChartTooltipValue(value: number, unit?: string): string {
|
||||
if (unit === '%') return `${value.toFixed(1)}%`;
|
||||
|
|
@ -48,6 +48,7 @@ export function formatHistoryChartTooltipValue(value: number, unit?: string): st
|
|||
export function getHistoryChartRefreshIntervalMs(range: HistoryTimeRange) {
|
||||
switch (range) {
|
||||
case '7d':
|
||||
case '14d':
|
||||
return 30000;
|
||||
case '30d':
|
||||
return 60000;
|
||||
|
|
@ -131,7 +132,7 @@ export function getHistoryChartYAxisLabels({
|
|||
|
||||
export function formatHistoryChartTimeLabel(timestamp: number, range: HistoryTimeRange) {
|
||||
const date = new Date(timestamp);
|
||||
if (range === '30d' || range === '90d' || range === '7d') {
|
||||
if (range === '30d' || range === '90d' || range === '14d' || range === '7d') {
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||
}
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
|
|
|
|||
|
|
@ -65,10 +65,22 @@ export function useHistoryChartState(props: HistoryChartProps, refs: HistoryChar
|
|||
};
|
||||
|
||||
const isLocked = createMemo(() => isRangeLocked(range()));
|
||||
const lockDays = createMemo(() => (range() === '30d' ? '30' : '90'));
|
||||
const lockDays = createMemo(() => {
|
||||
switch (range()) {
|
||||
case '14d':
|
||||
return '14';
|
||||
case '30d':
|
||||
return '30';
|
||||
case '90d':
|
||||
return '90';
|
||||
default:
|
||||
return '14';
|
||||
}
|
||||
});
|
||||
const lockTierLabel = createMemo(() => {
|
||||
const max = maxHistoryDays();
|
||||
const targetDays = range() === '30d' ? 30 : range() === '90d' ? 90 : 14;
|
||||
const targetDays =
|
||||
range() === '14d' ? 14 : range() === '30d' ? 30 : range() === '90d' ? 90 : 14;
|
||||
if (max <= 7 && targetDays <= 14) return 'Relay';
|
||||
return 'Pro';
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ describe('diskDetailPresentation', () => {
|
|||
'12h',
|
||||
'24h',
|
||||
'7d',
|
||||
'14d',
|
||||
'30d',
|
||||
'90d',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ describe('storagePoolDetailPresentation', () => {
|
|||
expect(STORAGE_POOL_DETAIL_HISTORY_RANGE_OPTIONS.map((option) => option.value)).toEqual([
|
||||
'24h',
|
||||
'7d',
|
||||
'14d',
|
||||
'30d',
|
||||
'90d',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export const DISK_DETAIL_HISTORY_RANGE_OPTIONS: readonly DiskDetailChartOption[]
|
|||
{ value: '12h', label: 'Last 12 hours' },
|
||||
{ value: '24h', label: 'Last 24 hours' },
|
||||
{ value: '7d', label: 'Last 7 days' },
|
||||
{ value: '14d', label: 'Last 14 days' },
|
||||
{ value: '30d', label: 'Last 30 days' },
|
||||
{ value: '90d', label: 'Last 90 days' },
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export const STORAGE_POOL_DETAIL_HISTORY_RANGE_OPTIONS: readonly {
|
|||
}[] = [
|
||||
{ value: '24h', label: '24h' },
|
||||
{ value: '7d', label: '7d' },
|
||||
{ value: '14d', label: '14d' },
|
||||
{ value: '30d', label: '30d' },
|
||||
{ value: '90d', label: '90d' },
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -5332,7 +5332,7 @@ func TestContract_SelfHostedCommunityEntitlementsJSONSnapshot(t *testing.T) {
|
|||
"limits":[],
|
||||
"subscription_state":"active",
|
||||
"upgrade_reasons":[
|
||||
{"key":"mobile_app","reason":"Get Relay so you can check Pulse from your phone when you are away from the dashboard.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=mobile_app"},
|
||||
{"key":"mobile_app","reason":"Get Relay so the mobile app can pair with this instance for secure handoff and notifications.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=mobile_app"},
|
||||
{"key":"push_notifications","reason":"Get Relay so important alerts reach you immediately on mobile instead of waiting for you to reopen Pulse.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=push_notifications"},
|
||||
{"key":"relay","reason":"Get Relay so Pulse stays reachable securely from anywhere instead of only on the local dashboard.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=relay"},
|
||||
{"key":"long_term_metrics","reason":"Get Relay for 14 days of history, or Pro for 90 days, so you can see what changed before and after an incident.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=long_term_metrics"},
|
||||
|
|
|
|||
|
|
@ -8113,13 +8113,42 @@ func (r *Router) handleMetricsStoreStats(w http.ResponseWriter, req *http.Reques
|
|||
}
|
||||
}
|
||||
|
||||
func parseMetricsHistoryDuration(timeRange string) time.Duration {
|
||||
normalizedRange := strings.ToLower(strings.TrimSpace(timeRange))
|
||||
switch normalizedRange {
|
||||
case "30m":
|
||||
return 30 * time.Minute
|
||||
case "1h":
|
||||
return time.Hour
|
||||
case "6h":
|
||||
return 6 * time.Hour
|
||||
case "12h":
|
||||
return 12 * time.Hour
|
||||
case "24h", "1d", "":
|
||||
return 24 * time.Hour
|
||||
}
|
||||
|
||||
if strings.HasSuffix(normalizedRange, "d") {
|
||||
days, err := strconv.Atoi(strings.TrimSuffix(normalizedRange, "d"))
|
||||
if err == nil && days > 0 {
|
||||
return time.Duration(days) * 24 * time.Hour
|
||||
}
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(normalizedRange)
|
||||
if err != nil || duration <= 0 {
|
||||
return 24 * time.Hour
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
// handleMetricsHistory returns historical metrics from the persistent SQLite store
|
||||
// Query params:
|
||||
// - resourceType: "node", "agent", "vm", "system-container", "oci-container", "app-container",
|
||||
// "docker-host", "k8s", "storage", or "disk" (required)
|
||||
// - resourceId: the resource identifier (required)
|
||||
// - metric: "cpu", "memory", "disk", etc. (optional, omit for all metrics)
|
||||
// - range: time range like "1h", "24h", "7d", "30d", "90d" (optional, default "24h")
|
||||
// - range: time range like "1h", "24h", "7d", "14d", "30d", "90d" (optional, default "24h")
|
||||
func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
|
|
@ -8151,36 +8180,9 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
|||
}
|
||||
resourceID = canonicalizeMetricsHistoryResourceID(runtimeResourceType, resourceID)
|
||||
|
||||
// Parse time range
|
||||
var duration time.Duration
|
||||
duration := parseMetricsHistoryDuration(timeRange)
|
||||
var stepSecs int64 = 0 // Default to no downsampling (use tier resolution)
|
||||
|
||||
switch timeRange {
|
||||
case "30m":
|
||||
duration = 30 * time.Minute
|
||||
case "1h":
|
||||
duration = time.Hour
|
||||
case "6h":
|
||||
duration = 6 * time.Hour
|
||||
case "12h":
|
||||
duration = 12 * time.Hour
|
||||
case "24h", "1d", "":
|
||||
duration = 24 * time.Hour
|
||||
case "7d":
|
||||
duration = 7 * 24 * time.Hour
|
||||
case "30d":
|
||||
duration = 30 * 24 * time.Hour
|
||||
case "90d":
|
||||
duration = 90 * 24 * time.Hour
|
||||
default:
|
||||
// Try parsing as duration
|
||||
var err error
|
||||
duration, err = time.ParseDuration(timeRange)
|
||||
if err != nil {
|
||||
duration = 24 * time.Hour // Default to 24 hours
|
||||
}
|
||||
}
|
||||
|
||||
// Optional downsampling based on requested max points.
|
||||
// When omitted, we return the native tier resolution.
|
||||
if maxPointsStr := query.Get("maxPoints"); maxPointsStr != "" {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
|
||||
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
|
||||
)
|
||||
|
||||
|
|
@ -133,6 +134,93 @@ func TestHandleMetricsHistory_LicenseRequired(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseMetricsHistoryDurationSupportsDayRanges(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want time.Duration
|
||||
}{
|
||||
{input: "", want: 24 * time.Hour},
|
||||
{input: "24h", want: 24 * time.Hour},
|
||||
{input: "7d", want: 7 * 24 * time.Hour},
|
||||
{input: "14d", want: 14 * 24 * time.Hour},
|
||||
{input: "15d", want: 15 * 24 * time.Hour},
|
||||
{input: "336h", want: 14 * 24 * time.Hour},
|
||||
{input: "nonsense", want: 24 * time.Hour},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
if got := parseMetricsHistoryDuration(tt.input); got != tt.want {
|
||||
t.Fatalf("parseMetricsHistoryDuration(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setMetricsHistoryLicenseTier(t *testing.T, handlers *LicenseHandlers, tier pkglicensing.Tier) {
|
||||
t.Helper()
|
||||
svc := handlers.Service(context.Background())
|
||||
svc.SetCurrentForTesting(&pkglicensing.License{
|
||||
Claims: pkglicensing.Claims{
|
||||
LicenseID: "metrics-history-tier-test",
|
||||
Email: "metrics-history@example.test",
|
||||
Tier: tier,
|
||||
IssuedAt: time.Now().Add(-time.Hour).Unix(),
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
|
||||
},
|
||||
ValidatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleMetricsHistory_TierAwareHistoryRanges(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tier pkglicensing.Tier
|
||||
range_ string
|
||||
want int
|
||||
}{
|
||||
{name: "community blocks relay history", range_: "14d", want: http.StatusPaymentRequired},
|
||||
{name: "relay allows 14 days", tier: pkglicensing.TierRelay, range_: "14d", want: http.StatusOK},
|
||||
{name: "relay blocks longer day range", tier: pkglicensing.TierRelay, range_: "15d", want: http.StatusPaymentRequired},
|
||||
{name: "pro allows 90 days", tier: pkglicensing.TierPro, range_: "90d", want: http.StatusOK},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
monitor, _, _ := newTestMonitor(t)
|
||||
store, err := metrics.NewStore(metrics.DefaultConfig(t.TempDir()))
|
||||
if err != nil {
|
||||
t.Fatalf("metrics.NewStore error: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
setUnexportedField(t, monitor, "metricsStore", store)
|
||||
|
||||
mtp := config.NewMultiTenantPersistence(t.TempDir())
|
||||
if _, err := mtp.GetPersistence("default"); err != nil {
|
||||
t.Fatalf("failed to init persistence: %v", err)
|
||||
}
|
||||
handlers := NewLicenseHandlers(mtp, false)
|
||||
if tt.tier != "" {
|
||||
setMetricsHistoryLicenseTier(t, handlers, tt.tier)
|
||||
}
|
||||
|
||||
router := &Router{
|
||||
monitor: monitor,
|
||||
licenseHandlers: handlers,
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/metrics-store/history?resourceType=vm&resourceId=vm-1&metric=cpu&range="+tt.range_, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.handleMetricsHistory(rec, req)
|
||||
|
||||
if rec.Code != tt.want {
|
||||
t.Fatalf("status = %d, want %d; body=%s", rec.Code, tt.want, rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMetricsHistory_UsesStore(t *testing.T) {
|
||||
monitor, _, _ := newTestMonitor(t)
|
||||
store, err := metrics.NewStore(metrics.DefaultConfig(t.TempDir()))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue