From e55c1b941d841cbeba72b62fa0fbd6f670ec34c4 Mon Sep 17 00:00:00 2001 From: sheya Date: Tue, 8 Sep 2026 18:00:07 +0100 Subject: [PATCH 1/2] collect Dedicated Inference (vLLM) metrics, gated by gateway capability flag --- cmd/agent/main.go | 43 ++++++++++++-- internal/client/hub.go | 17 ++++++ internal/client/hub_test.go | 40 +++++++++++++ internal/collectors/manager.go | 2 + internal/collectors/vllm.go | 71 +++++++++++++++++++++++ internal/collectors/vllm_test.go | 74 ++++++++++++++++++++++++ internal/config/config.go | 17 +++++- internal/config/config_test.go | 18 ++++++ internal/metrics/filter.go | 2 + internal/probes/vllm.go | 94 ++++++++++++++++++++++++++++++ internal/probes/vllm_test.go | 98 ++++++++++++++++++++++++++++++++ 11 files changed, 470 insertions(+), 6 deletions(-) create mode 100644 internal/collectors/vllm.go create mode 100644 internal/collectors/vllm_test.go create mode 100644 internal/probes/vllm.go create mode 100644 internal/probes/vllm_test.go diff --git a/cmd/agent/main.go b/cmd/agent/main.go index c8e3be8..d2e5f0a 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -160,10 +160,25 @@ func main() { slog.Info("gpu collector skipped", "reason", "disabled") } + // Dedicated Inference is always scheduled: whether this VM actually runs + // vLLM is a fact the gateway knows (from the cloud provider API), not + // something the agent can detect locally, so the collector starts gated + // off and is flipped on by runMetricsEnabledSyncLoop once confirmed. + vllmCollector := &collectors.VLLMCollector{ + Hub: hub, + Probe: probes.VLLMProbe{Endpoint: cfg.DedicatedInference.Endpoint}, + } + scheduled = append(scheduled, collectors.ScheduledCollector{ + Collector: vllmCollector, + Interval: cfg.DedicatedInference.ScrapeInterval, + }) + slog.Info("dedicated inference collector scheduled", "endpoint", cfg.DedicatedInference.Endpoint, "interval", cfg.DedicatedInference.ScrapeInterval) + enabledCollectors := map[string]bool{ - "agent": true, - "gpu": gpuCollectorEnabled, - "node": cfg.Node.Enable, + "agent": true, + "gpu": gpuCollectorEnabled, + "node": cfg.Node.Enable, + "dedicated_inference": true, } scheduled = append(scheduled, collectors.ScheduledCollector{ Collector: &collectors.AgentCollector{ @@ -194,7 +209,7 @@ func main() { // an explicit false from the gateway preserves that contract. Starting // disabled is risky: any first-sync failure (network blip, missing field, // gateway rollout) would leave collectors permanently off. - go runMetricsEnabledSyncLoop(ctx, hub, mgr, meta.UUID, len(scheduled), metricsConfigSyncInterval) + go runMetricsEnabledSyncLoop(ctx, hub, mgr, vllmCollector, meta.UUID, len(scheduled), metricsConfigSyncInterval) } managerErrCh := make(chan error, 1) @@ -281,6 +296,7 @@ func runMetricsEnabledSyncLoop( ctx context.Context, hub *client.HubClient, manager *collectors.Manager, + vllmCollector *collectors.VLLMCollector, uuid string, collectorCount int, interval time.Duration, @@ -291,6 +307,8 @@ func runMetricsEnabledSyncLoop( lastEnabled := true hasLastEnabled := false + lastDIEnabled := false + hasLastDIEnabled := false sync := func() { syncCtx, cancel := context.WithTimeout(ctx, metricsConfigSyncTimeout) metadata, err := hub.GetMetadata(syncCtx, uuid) @@ -323,6 +341,23 @@ func runMetricsEnabledSyncLoop( lastEnabled = enabled hasLastEnabled = true } + + // DedicatedInference is a fact about the VM, not a toggle: a nil + // Capabilities (no enrichment data) means "not a Dedicated Inference + // VM", the opposite default direction from MetricsEnabled above. + isDI := metadata.Capabilities != nil && metadata.Capabilities.DedicatedInference + if vllmCollector != nil { + vllmCollector.SetEnabled(isDI) + } + if !hasLastDIEnabled || lastDIEnabled != isDI { + if isDI { + slog.Info("dedicated inference confirmed; collector active", "uuid", uuid) + } else { + slog.Info("dedicated inference not present; collector idle", "uuid", uuid) + } + lastDIEnabled = isDI + hasLastDIEnabled = true + } } sync() diff --git a/internal/client/hub.go b/internal/client/hub.go index 2e50343..7103f00 100644 --- a/internal/client/hub.go +++ b/internal/client/hub.go @@ -718,6 +718,23 @@ func closeResponseBody(resp *http.Response, operation string) { // dropped field silently disable all collectors. type VMMetadata struct { MetricsEnabled *bool `json:"metrics_enabled"` + + // Capabilities is only present when the gateway was able to enrich this + // VM from Hyperstack (vm.enhanced_metrics). A nil Capabilities means no + // enrichment data is available; callers should treat every field within + // it as its zero value in that case, not as "default enabled". + Capabilities *VMMetadataCapabilities `json:"capabilities"` +} + +// VMMetadataCapabilities holds enrichment-derived facts about what a VM is +// provisioned for. +type VMMetadataCapabilities struct { + // DedicatedInference reports whether this VM was provisioned as a + // Hyperstack Dedicated Inference (vLLM) instance. Unlike MetricsEnabled, + // this is a fact about the VM rather than a toggle: when Capabilities is + // nil, or this is false, callers should treat it as "not a Dedicated + // Inference VM". + DedicatedInference bool `json:"dedicated_inference"` } func (h *HubClient) GetMetadata(ctx context.Context, uuid string) (*VMMetadata, error) { diff --git a/internal/client/hub_test.go b/internal/client/hub_test.go index 8f92b14..f2d4120 100644 --- a/internal/client/hub_test.go +++ b/internal/client/hub_test.go @@ -1004,6 +1004,46 @@ func TestGetMetadata_MissingFieldIsNil(t *testing.T) { } } +// TestGetMetadata_CapabilitiesDedicatedInferenceTrue verifies that a nested +// capabilities.dedicated_inference:true is decoded correctly. +func TestGetMetadata_CapabilitiesDedicatedInferenceTrue(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintln(w, `{"capabilities":{"dedicated_inference":true}}`) + })) + defer server.Close() + + h := NewHubClient(server.URL) + meta, err := h.GetMetadata(context.Background(), "test-uuid") + if err != nil { + t.Fatalf("GetMetadata() error = %v", err) + } + if meta.Capabilities == nil || !meta.Capabilities.DedicatedInference { + t.Fatalf("Capabilities = %v, want DedicatedInference = true", meta.Capabilities) + } +} + +// TestGetMetadata_CapabilitiesMissingFieldIsNil verifies that a response +// without a capabilities object leaves the field nil, distinguishable from an +// enriched-but-false response. Unlike MetricsEnabled, nil must be treated as +// "not a Dedicated Inference VM" by callers, not as default-enabled. +func TestGetMetadata_CapabilitiesMissingFieldIsNil(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintln(w, `{}`) + })) + defer server.Close() + + h := NewHubClient(server.URL) + meta, err := h.GetMetadata(context.Background(), "test-uuid") + if err != nil { + t.Fatalf("GetMetadata() error = %v", err) + } + if meta.Capabilities != nil { + t.Fatalf("Capabilities = %v, want nil for absent field", meta.Capabilities) + } +} + // TestGetMetadata_RefreshesHyperstackKeyOnUnauthorized verifies that GetMetadata // retries the request after a 401 triggers a successful key refresh // (mirrors the same test pattern used for SubmitBatch and Submit). diff --git a/internal/collectors/manager.go b/internal/collectors/manager.go index a08a4cb..5b7f7e6 100644 --- a/internal/collectors/manager.go +++ b/internal/collectors/manager.go @@ -54,6 +54,8 @@ func (m *Manager) Run(ctx context.Context) error { collectorName = "gpu" case *AgentCollector: collectorName = "agent" + case *VLLMCollector: + collectorName = "dedicated_inference" } go func() { slog.Debug("collector starting", "collector", collectorName, "interval", interval.String()) diff --git a/internal/collectors/vllm.go b/internal/collectors/vllm.go new file mode 100644 index 0000000..823f560 --- /dev/null +++ b/internal/collectors/vllm.go @@ -0,0 +1,71 @@ +package collectors + +import ( + "context" + "errors" + "log/slog" + "sync/atomic" + "time" + + "github.com/NexGenCloud/hyperstack-agent/internal/client" + "github.com/NexGenCloud/hyperstack-agent/internal/metrics" + "github.com/NexGenCloud/hyperstack-agent/internal/probes" + "github.com/NexGenCloud/hyperstack-agent/internal/system" +) + +// VLLMCollector scrapes a local vLLM (Hyperstack Dedicated Inference) server. +// Unlike GPU/Node, whether this VM should scrape at all is decided by the +// gateway at runtime (not local hardware detection), so it carries its own +// gate defaulting to disabled until the gateway confirms this is a +// Dedicated Inference VM. +type VLLMCollector struct { + Hub *client.HubClient + Probe probes.Probe + + gate atomic.Bool +} + +func (v *VLLMCollector) SetEnabled(enabled bool) { v.gate.Store(enabled) } + +func (v *VLLMCollector) IsEnabled() bool { return v.gate.Load() } + +func (v *VLLMCollector) Run(ctx context.Context) error { + if v.Probe == nil { + return errors.New("vllm collector missing probe implementation") + } + + if !v.IsEnabled() { + slog.Debug("dedicated inference collector skipped", "reason", "gate disabled") + return nil + } + + start := time.Now() + slog.Debug("dedicated inference probe: begin") + samples, err := v.Probe.Collect(ctx) + recordDuration := time.Since(start) + system.SetLastScrapeMs(recordDuration.Milliseconds()) + if err != nil { + system.RecordProbeFailure(v.Probe.Name(), err) + slog.Warn("dedicated inference probe failed", "error", err) + return err + } + filtered := metrics.FilterSamples(samples) + if len(filtered) == 0 { + system.RecordProbeFailure(v.Probe.Name(), errors.New("no dedicated inference samples collected")) + return nil + } + system.RecordProbeSuccess(v.Probe.Name()) + slog.Debug("dedicated inference probe: samples", "count", len(filtered), "duration_ms", recordDuration.Milliseconds()) + + if v.Hub != nil { + now := time.Now().UTC() + measures := metrics.SamplesToMeasures(filtered, now, v.Probe.Name()) + v.Hub.EnqueueMetrics(measures) + slog.Info( + "metrics enqueued", + "collector", v.Probe.Name(), + "samples", len(filtered), + ) + } + return nil +} diff --git a/internal/collectors/vllm_test.go b/internal/collectors/vllm_test.go new file mode 100644 index 0000000..c5683d8 --- /dev/null +++ b/internal/collectors/vllm_test.go @@ -0,0 +1,74 @@ +package collectors + +import ( + "context" + "errors" + "sync/atomic" + "testing" + + "github.com/NexGenCloud/hyperstack-agent/internal/metrics" +) + +type stubProbe struct { + name string + calls atomic.Int64 + samples []metrics.Sample + err error +} + +func (p *stubProbe) Name() string { return p.name } + +func (p *stubProbe) Collect(context.Context) ([]metrics.Sample, error) { + p.calls.Add(1) + if p.err != nil { + return nil, p.err + } + return p.samples, nil +} + +func TestVLLMCollectorSkipsProbeWhenGateDisabled(t *testing.T) { + probe := &stubProbe{name: "dedicated_inference"} + c := &VLLMCollector{Probe: probe} + + if err := c.Run(context.Background()); err != nil { + t.Fatalf("Run() error = %v", err) + } + if got := probe.calls.Load(); got != 0 { + t.Fatalf("probe calls = %d, want 0 when gate disabled", got) + } +} + +func TestVLLMCollectorRunsProbeWhenGateEnabled(t *testing.T) { + probe := &stubProbe{ + name: "dedicated_inference", + samples: []metrics.Sample{ + {Name: "hyperstack_dedicated_inference_num_requests_running", Value: "1"}, + }, + } + c := &VLLMCollector{Probe: probe} + c.SetEnabled(true) + + if err := c.Run(context.Background()); err != nil { + t.Fatalf("Run() error = %v", err) + } + if got := probe.calls.Load(); got != 1 { + t.Fatalf("probe calls = %d, want 1 when gate enabled", got) + } +} + +func TestVLLMCollectorPropagatesProbeError(t *testing.T) { + probe := &stubProbe{name: "dedicated_inference", err: errors.New("scrape failed")} + c := &VLLMCollector{Probe: probe} + c.SetEnabled(true) + + if err := c.Run(context.Background()); err == nil { + t.Fatal("expected error to propagate from probe, got nil") + } +} + +func TestVLLMCollectorGateDefaultsDisabled(t *testing.T) { + c := &VLLMCollector{} + if c.IsEnabled() { + t.Fatal("IsEnabled() = true, want false by default") + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 08ce250..b1777f8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,13 +25,22 @@ type ExporterConfig struct { ScrapeInterval time.Duration } +// DedicatedInferenceConfig has no Enable field: whether to actually scrape is +// decided at runtime by the gateway (VMMetadata.DedicatedInference), not by +// local config, since the agent has no local way to detect it. +type DedicatedInferenceConfig struct { + Endpoint string + ScrapeInterval time.Duration +} + // Config holds environment-driven settings for the agent. type Config struct { Hub HubConfig DefaultInterval time.Duration - Node ExporterConfig - GPU ExporterConfig + Node ExporterConfig + GPU ExporterConfig + DedicatedInference DedicatedInferenceConfig } // Load reads configuration from environment variables with sensible defaults. @@ -51,6 +60,10 @@ func Load() Config { Enable: getEnvBool("HYPERSTACK_ENABLE_GPU", true), ScrapeInterval: 30 * time.Second, }, + DedicatedInference: DedicatedInferenceConfig{ + Endpoint: getEnv("HYPERSTACK_DEDICATED_INFERENCE_URL", "http://localhost:8000/metrics"), + ScrapeInterval: 15 * time.Second, + }, } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 460137d..7dd1dad 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -31,6 +31,24 @@ func TestLoad_DefaultInterval(t *testing.T) { } } +func TestLoad_DedicatedInferenceDefaults(t *testing.T) { + cfg := Load() + if cfg.DedicatedInference.Endpoint != "http://localhost:8000/metrics" { + t.Fatalf("cfg.DedicatedInference.Endpoint = %q, want default localhost:8000/metrics", cfg.DedicatedInference.Endpoint) + } + if cfg.DedicatedInference.ScrapeInterval.String() != "15s" { + t.Fatalf("cfg.DedicatedInference.ScrapeInterval = %v, want 15s", cfg.DedicatedInference.ScrapeInterval) + } +} + +func TestLoad_DedicatedInferenceEndpointOverride(t *testing.T) { + t.Setenv("HYPERSTACK_DEDICATED_INFERENCE_URL", "http://localhost:9000/metrics") + cfg := Load() + if cfg.DedicatedInference.Endpoint != "http://localhost:9000/metrics" { + t.Fatalf("cfg.DedicatedInference.Endpoint = %q, want override", cfg.DedicatedInference.Endpoint) + } +} + func TestSecurityWarningsWarnsForNonLoopbackHTTPHub(t *testing.T) { cfg := Config{Hub: HubConfig{URL: "http://gateway.example.com"}} diff --git a/internal/metrics/filter.go b/internal/metrics/filter.go index 0885c80..e5db708 100644 --- a/internal/metrics/filter.go +++ b/internal/metrics/filter.go @@ -33,6 +33,8 @@ var ImportantPrefixes = []string{ "nvidia_gpu_", "nvidia_smi_", "gpu_", + // Dedicated Inference (vLLM) + "hyperstack_dedicated_inference_", } // FilterSamples applies the important prefix filter and returns the subset of samples. diff --git a/internal/probes/vllm.go b/internal/probes/vllm.go new file mode 100644 index 0000000..ec89db3 --- /dev/null +++ b/internal/probes/vllm.go @@ -0,0 +1,94 @@ +package probes + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/NexGenCloud/hyperstack-agent/internal/metrics" +) + +const diMetricPrefix = "hyperstack_dedicated_inference_" + +// vllmDropSuffixes excludes prometheus_client's auto-generated "_created" +// gauges, which track counter/histogram creation time and carry no signal. +var vllmDropSuffixes = []string{"_created"} + +var defaultVLLMHTTPClient = &http.Client{Timeout: 5 * time.Second} + +// VLLMProbe scrapes a vLLM OpenAI-compatible server's /metrics endpoint and +// re-namespaces its "vllm:" metrics to match the Hyperstack Dedicated +// Inference product naming. +type VLLMProbe struct { + Endpoint string + Client *http.Client +} + +func (VLLMProbe) Name() string { return "dedicated_inference" } + +func (p VLLMProbe) Collect(ctx context.Context) ([]metrics.Sample, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.Endpoint, nil) + if err != nil { + return nil, err + } + + resp, err := p.client().Do(req) + if err != nil { + return nil, err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("dedicated inference scrape status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + parsed := metrics.ParsePrometheusText(string(body)) + return renameVLLMSamples(parsed), nil +} + +func (p VLLMProbe) client() *http.Client { + if p.Client != nil { + return p.Client + } + return defaultVLLMHTTPClient +} + +// renameVLLMSamples keeps only vLLM's own "vllm:"-namespaced series (dropping +// the python_*/process_*/http_* metrics that ship with every prometheus_client +// app) and maps them onto the hyperstack_dedicated_inference_ prefix. +func renameVLLMSamples(samples []metrics.Sample) []metrics.Sample { + out := make([]metrics.Sample, 0, len(samples)) + for _, s := range samples { + if !strings.HasPrefix(s.Name, "vllm:") { + continue + } + if hasAnySuffix(s.Name, vllmDropSuffixes) { + continue + } + out = append(out, metrics.Sample{ + Name: diMetricPrefix + strings.TrimPrefix(s.Name, "vllm:"), + Labels: s.Labels, + Value: s.Value, + }) + } + return out +} + +func hasAnySuffix(s string, suffixes []string) bool { + for _, suf := range suffixes { + if strings.HasSuffix(s, suf) { + return true + } + } + return false +} diff --git a/internal/probes/vllm_test.go b/internal/probes/vllm_test.go new file mode 100644 index 0000000..3d93554 --- /dev/null +++ b/internal/probes/vllm_test.go @@ -0,0 +1,98 @@ +package probes + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" +) + +// sampleVLLMMetrics is a trimmed excerpt of a real vLLM /metrics response, +// covering the shapes that need to be dropped (python_*, process_*, http_*, +// vllm:*_created) alongside the ones that should survive renaming. +const sampleVLLMMetrics = `# HELP python_gc_objects_collected_total Objects collected during gc +# TYPE python_gc_objects_collected_total counter +python_gc_objects_collected_total{generation="0"} 15020.0 +# HELP process_resident_memory_bytes Resident memory size in bytes. +# TYPE process_resident_memory_bytes gauge +process_resident_memory_bytes 7.74701056e+08 +# HELP http_request_duration_highr_seconds_bucket Latency histogram +# TYPE http_request_duration_highr_seconds_bucket histogram +http_request_duration_highr_seconds_bucket{le="0.01"} 0.0 +# HELP vllm:num_requests_running Number of requests in model execution batches. +# TYPE vllm:num_requests_running gauge +vllm:num_requests_running{engine="0",model_name="tiny"} 0.0 +# HELP vllm:estimated_flops_per_gpu_created Estimated number of floating point operations per GPU. +# TYPE vllm:estimated_flops_per_gpu_created gauge +vllm:estimated_flops_per_gpu_created{engine="0",model_name="tiny"} 1.788e+09 +# HELP vllm:request_success_total Count of successfully processed requests. +# TYPE vllm:request_success_total counter +vllm:request_success_total{engine="0",finished_reason="stop",model_name="tiny"} 4.0 +# HELP vllm:time_to_first_token_seconds_bucket Time to first token histogram +# TYPE vllm:time_to_first_token_seconds_bucket histogram +vllm:time_to_first_token_seconds_bucket{engine="0",le="0.01",model_name="tiny"} 2.0 +` + +func TestVLLMProbeRenamesAndFilters(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(sampleVLLMMetrics)) + })) + defer srv.Close() + + probe := VLLMProbe{Endpoint: srv.URL} + samples, err := probe.Collect(context.Background()) + if err != nil { + t.Fatalf("Collect() error = %v", err) + } + + got := map[string]bool{} + for _, s := range samples { + got[s.Name] = true + } + + wantPresent := []string{ + "hyperstack_dedicated_inference_num_requests_running", + "hyperstack_dedicated_inference_request_success_total", + "hyperstack_dedicated_inference_time_to_first_token_seconds_bucket", + } + for _, name := range wantPresent { + if !got[name] { + t.Errorf("expected sample %q to be present, got %v", name, got) + } + } + + wantAbsent := []string{ + "python_gc_objects_collected_total", + "process_resident_memory_bytes", + "http_request_duration_highr_seconds_bucket", + "vllm:estimated_flops_per_gpu_created", + "hyperstack_dedicated_inference_estimated_flops_per_gpu_created", + } + for _, name := range wantAbsent { + if got[name] { + t.Errorf("expected sample %q to be dropped, but it was present", name) + } + } + + if len(samples) != len(wantPresent) { + t.Errorf("len(samples) = %d, want %d (%v)", len(samples), len(wantPresent), got) + } +} + +func TestVLLMProbeName(t *testing.T) { + if got := (VLLMProbe{}).Name(); got != "dedicated_inference" { + t.Fatalf("Name() = %q, want %q", got, "dedicated_inference") + } +} + +func TestVLLMProbeCollectNon200(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + })) + defer srv.Close() + + probe := VLLMProbe{Endpoint: srv.URL} + if _, err := probe.Collect(context.Background()); err == nil { + t.Fatal("expected error for non-200 response, got nil") + } +} From d08351ce3f956eac6f072866398d6787c3e6b1dd Mon Sep 17 00:00:00 2001 From: sheya Date: Tue, 8 Sep 2026 18:05:03 +0100 Subject: [PATCH 2/2] use 127.0.0.1:8000/metrics --- internal/config/config.go | 2 +- internal/config/config_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index b1777f8..5d10c77 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -61,7 +61,7 @@ func Load() Config { ScrapeInterval: 30 * time.Second, }, DedicatedInference: DedicatedInferenceConfig{ - Endpoint: getEnv("HYPERSTACK_DEDICATED_INFERENCE_URL", "http://localhost:8000/metrics"), + Endpoint: getEnv("HYPERSTACK_DEDICATED_INFERENCE_URL", "http://127.0.0.1:8000/metrics"), ScrapeInterval: 15 * time.Second, }, } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7dd1dad..ff857b5 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -33,8 +33,8 @@ func TestLoad_DefaultInterval(t *testing.T) { func TestLoad_DedicatedInferenceDefaults(t *testing.T) { cfg := Load() - if cfg.DedicatedInference.Endpoint != "http://localhost:8000/metrics" { - t.Fatalf("cfg.DedicatedInference.Endpoint = %q, want default localhost:8000/metrics", cfg.DedicatedInference.Endpoint) + if cfg.DedicatedInference.Endpoint != "http://127.0.0.1:8000/metrics" { + t.Fatalf("cfg.DedicatedInference.Endpoint = %q, want default 127.0.0.1:8000/metrics", cfg.DedicatedInference.Endpoint) } if cfg.DedicatedInference.ScrapeInterval.String() != "15s" { t.Fatalf("cfg.DedicatedInference.ScrapeInterval = %v, want 15s", cfg.DedicatedInference.ScrapeInterval)