update the as per the comment

This commit is contained in:
Manendra Pal Singh
2025-12-15 12:12:33 +05:30
parent c367a970d1
commit 17d9ca865d
8 changed files with 96 additions and 81 deletions

View File

@@ -201,7 +201,7 @@ log:
**Required**: No **Required**: No
**Description**: OpenTelemetry configuration controlling whether the Prometheus exporter is enabled. **Description**: OpenTelemetry configuration controlling whether the Prometheus exporter is enabled.
**Important**: This block is optional—omit it to run without telemetry. When present, the `/metrics` endpoint is exposed only if `enableMetrics: true`. **Important**: This block is optional—omit it to run without telemetry. When present, the `/metrics` endpoint is exposed on a separate port (configurable via `metricsPort`) only if `enableMetrics: true`.
##### Parameters: ##### Parameters:
@@ -238,6 +238,12 @@ log:
**Default**: `"development"` **Default**: `"development"`
**Description**: Sets the `deployment.environment` attribute (e.g., `development`, `staging`, `production`). **Description**: Sets the `deployment.environment` attribute (e.g., `development`, `staging`, `production`).
###### `config.metricsPort`
**Type**: `string`
**Required**: No
**Default**: `"9090"`
**Description**: Port on which the metrics HTTP server will listen. The metrics endpoint is hosted on a separate server from the main application.
**Example - Enable Metrics** (matches `config/local-simple.yaml`): **Example - Enable Metrics** (matches `config/local-simple.yaml`):
```yaml ```yaml
plugins: plugins:
@@ -248,22 +254,25 @@ plugins:
serviceVersion: "1.0.0" serviceVersion: "1.0.0"
enableMetrics: "true" enableMetrics: "true"
environment: "development" environment: "development"
metricsPort: "9090"
``` ```
### Accessing Metrics ### Accessing Metrics
When `plugins.otelsetup.config.enableMetrics: "true"`, scrape metrics at: When `plugins.otelsetup.config.enableMetrics: "true"`, the metrics endpoint is hosted on a separate HTTP server. Scrape metrics at:
``` ```
http://your-server:port/metrics http://your-server:9090/metrics
``` ```
**Note**: The metrics server runs on the port specified by `config.metricsPort` (default: `9090`), which is separate from the main application port configured in `http.port`.
### Metrics Collected ### Metrics Collected
Metrics are organized by module for better maintainability and encapsulation: Metrics are organized by module for better maintainability and encapsulation:
#### OTel Setup (from `otelsetup` plugin) #### OTel Setup (from `otelsetup` plugin)
- Prometheus exporter & `/metrics` handler registration - Prometheus exporter & `/metrics` endpoint on separate HTTP server
- Go runtime instrumentation (`go_*`), resource attributes, and meter provider wiring - Go runtime instrumentation (`go_*`), resource attributes, and meter provider wiring
#### Step Execution Metrics (from `telemetry` package) #### Step Execution Metrics (from `telemetry` package)
@@ -1141,7 +1150,7 @@ routingRules:
- Embedded Ed25519 keys - Embedded Ed25519 keys
- Local Redis - Local Redis
- Simplified routing - Simplified routing
- Optional metrics collection (available at `/metrics` when enabled) - Optional metrics collection (available on separate port when enabled)
**Use Case**: Quick local development and testing **Use Case**: Quick local development and testing
@@ -1164,7 +1173,7 @@ modules:
config: {} config: {}
``` ```
**Metrics Access**: When enabled, access metrics at `http://localhost:8081/metrics` **Metrics Access**: When enabled, access metrics at `http://localhost:9090/metrics` (default metrics port, configurable via `plugins.otelsetup.config.metricsPort`)
### 2. Local Development (Vault Mode) ### 2. Local Development (Vault Mode)
@@ -1199,7 +1208,7 @@ modules:
- Production Redis - Production Redis
- Remote plugin loading - Remote plugin loading
- Pub/Sub integration - Pub/Sub integration
- OpenTelemetry metrics enabled (available at `/metrics` endpoint) - OpenTelemetry metrics enabled (available on separate port, default: 9090)
**Use Case**: Single deployment serving both roles **Use Case**: Single deployment serving both roles
@@ -1237,7 +1246,7 @@ modules:
``` ```
**Metrics Access**: **Metrics Access**:
- Prometheus scraping: `http://your-server:port/metrics` - Prometheus scraping: `http://your-server:9090/metrics` (default metrics port, configurable via `plugins.otelsetup.config.metricsPort`)
### 4. Production BAP-Only Mode ### 4. Production BAP-Only Mode

View File

@@ -17,7 +17,6 @@ import (
"github.com/beckn-one/beckn-onix/core/module/handler" "github.com/beckn-one/beckn-onix/core/module/handler"
"github.com/beckn-one/beckn-onix/pkg/log" "github.com/beckn-one/beckn-onix/pkg/log"
"github.com/beckn-one/beckn-onix/pkg/plugin" "github.com/beckn-one/beckn-onix/pkg/plugin"
"github.com/beckn-one/beckn-onix/pkg/telemetry"
) )
// ApplicationPlugins holds application-level plugin configurations. // ApplicationPlugins holds application-level plugin configurations.
@@ -99,29 +98,24 @@ func validateConfig(cfg *Config) error {
} }
// initPlugins initializes application-level plugins including telemetry. // initPlugins initializes application-level plugins including telemetry.
func initPlugins(ctx context.Context, mgr *plugin.Manager, otelSetupCfg *plugin.Config) (*telemetry.Provider, error) { func initPlugins(ctx context.Context, mgr *plugin.Manager, otelSetupCfg *plugin.Config) error {
if otelSetupCfg == nil { if otelSetupCfg == nil {
log.Info(ctx, "Telemetry config not provided; skipping OpenTelemetry setup") log.Info(ctx, "Telemetry config not provided; skipping OpenTelemetry setup")
return nil, nil return nil
} }
log.Infof(ctx, "Initializing telemetry via plugin id=%s", otelSetupCfg.ID) log.Infof(ctx, "Initializing telemetry via plugin id=%s", otelSetupCfg.ID)
otelProvider, err := mgr.OtelSetup(ctx, otelSetupCfg) _, err := mgr.OtelSetup(ctx, otelSetupCfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize telemetry plugin: %w", err) return fmt.Errorf("failed to initialize telemetry plugin: %w", err)
} }
return otelProvider, nil return nil
} }
// newServer creates and initializes the HTTP server. // newServer creates and initializes the HTTP server.
func newServer(ctx context.Context, mgr handler.PluginManager, cfg *Config, otelProvider *telemetry.Provider) (http.Handler, error) { func newServer(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error) {
mux := http.NewServeMux() mux := http.NewServeMux()
if otelProvider != nil && otelProvider.MetricsHandler != nil {
mux.Handle("/metrics", otelProvider.MetricsHandler)
log.Infof(ctx, "Metrics endpoint registered at /metrics")
}
if err := module.Register(ctx, cfg.Modules, mux, mgr); err != nil { if err := module.Register(ctx, cfg.Modules, mux, mgr); err != nil {
return nil, fmt.Errorf("failed to register modules: %w", err) return nil, fmt.Errorf("failed to register modules: %w", err)
} }
@@ -154,14 +148,13 @@ func run(ctx context.Context, configPath string) error {
log.Debug(ctx, "Plugin manager loaded.") log.Debug(ctx, "Plugin manager loaded.")
// Initialize plugins including telemetry. // Initialize plugins including telemetry.
otelProvider, err := initPlugins(ctx, mgr, cfg.Plugins.OtelSetup) if err := initPlugins(ctx, mgr, cfg.Plugins.OtelSetup); err != nil {
if err != nil {
return fmt.Errorf("failed to initialize plugins: %w", err) return fmt.Errorf("failed to initialize plugins: %w", err)
} }
// Initialize HTTP server. // Initialize HTTP server.
log.Infof(ctx, "Initializing HTTP server") log.Infof(ctx, "Initializing HTTP server")
srv, err := newServerFunc(ctx, mgr, cfg, otelProvider) srv, err := newServerFunc(ctx, mgr, cfg)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize server: %w", err) return fmt.Errorf("failed to initialize server: %w", err)
} }

View File

@@ -15,7 +15,6 @@ import (
"github.com/beckn-one/beckn-onix/core/module/handler" "github.com/beckn-one/beckn-onix/core/module/handler"
"github.com/beckn-one/beckn-onix/pkg/plugin" "github.com/beckn-one/beckn-onix/pkg/plugin"
"github.com/beckn-one/beckn-onix/pkg/plugin/definition" "github.com/beckn-one/beckn-onix/pkg/plugin/definition"
"github.com/beckn-one/beckn-onix/pkg/telemetry"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
@@ -125,7 +124,7 @@ func TestRunSuccess(t *testing.T) {
defer func() { newManagerFunc = originalNewManager }() defer func() { newManagerFunc = originalNewManager }()
originalNewServer := newServerFunc originalNewServer := newServerFunc
newServerFunc = func(ctx context.Context, mgr handler.PluginManager, cfg *Config, provider *telemetry.Provider) (http.Handler, error) { newServerFunc = func(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error) {
return http.NewServeMux(), nil return http.NewServeMux(), nil
} }
defer func() { newServerFunc = originalNewServer }() defer func() { newServerFunc = originalNewServer }()
@@ -176,14 +175,19 @@ func TestRunFailure(t *testing.T) {
// Mock dependencies // Mock dependencies
originalNewManager := newManagerFunc originalNewManager := newManagerFunc
// newManagerFunc = func(ctx context.Context, cfg *plugin.ManagerConfig) (*plugin.Manager, func(), error) { // Ensure newManagerFunc is never nil to avoid panic if invoked.
// return tt.mockMgr() newManagerFunc = func(ctx context.Context, cfg *plugin.ManagerConfig) (*plugin.Manager, func(), error) {
// } _, closer, err := tt.mockMgr()
newManagerFunc = nil if err != nil {
return nil, closer, err
}
// Return a deterministic error so the code path exits cleanly if reached.
return nil, closer, errors.New("mock manager error")
}
defer func() { newManagerFunc = originalNewManager }() defer func() { newManagerFunc = originalNewManager }()
originalNewServer := newServerFunc originalNewServer := newServerFunc
newServerFunc = func(ctx context.Context, mgr handler.PluginManager, cfg *Config, provider *telemetry.Provider) (http.Handler, error) { newServerFunc = func(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error) {
return tt.mockServer(ctx, mgr, cfg) return tt.mockServer(ctx, mgr, cfg)
} }
defer func() { newServerFunc = originalNewServer }() defer func() { newServerFunc = originalNewServer }()
@@ -314,7 +318,7 @@ func TestNewServerSuccess(t *testing.T) {
}, },
} }
handler, err := newServer(context.Background(), mockMgr, cfg, nil) handler, err := newServer(context.Background(), mockMgr, cfg)
if err != nil { if err != nil {
t.Errorf("Expected no error, but got: %v", err) t.Errorf("Expected no error, but got: %v", err)
@@ -359,7 +363,7 @@ func TestNewServerFailure(t *testing.T) {
}, },
} }
handler, err := newServer(context.Background(), mockMgr, cfg, nil) handler, err := newServer(context.Background(), mockMgr, cfg)
if err == nil { if err == nil {
t.Errorf("Expected an error, but got nil") t.Errorf("Expected an error, but got nil")

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"net/http/httptest"
"testing" "testing"
"github.com/beckn-one/beckn-onix/pkg/plugin/implementation/otelsetup" "github.com/beckn-one/beckn-onix/pkg/plugin/implementation/otelsetup"
@@ -21,12 +20,6 @@ func TestMetricsEndpointExposesPrometheus(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer provider.Shutdown(context.Background()) defer provider.Shutdown(context.Background())
rec := httptest.NewRecorder() // Metrics are served by the plugins own HTTP server; just ensure provider is initialized.
req := httptest.NewRequest("GET", "/metrics", nil) require.NotNil(t, provider.MeterProvider)
provider.MetricsHandler.ServeHTTP(rec, req)
require.Equal(t, 200, rec.Code)
body := rec.Body.String()
require.Contains(t, body, "# HELP")
require.Contains(t, body, "# TYPE")
} }

View File

@@ -27,6 +27,7 @@ func (m metricsProvider) New(ctx context.Context, config map[string]string) (*te
ServiceName: config["serviceName"], ServiceName: config["serviceName"],
ServiceVersion: config["serviceVersion"], ServiceVersion: config["serviceVersion"],
Environment: config["environment"], Environment: config["environment"],
MetricsPort: config["metricsPort"],
} }
// Parse enableMetrics as boolean // Parse enableMetrics as boolean

View File

@@ -56,7 +56,7 @@ func TestMetricsProviderNew_Success(t *testing.T) {
require.NoError(t, err, "New() should not return error") require.NoError(t, err, "New() should not return error")
require.NotNil(t, telemetryProvider, "New() should return non-nil provider") require.NotNil(t, telemetryProvider, "New() should return non-nil provider")
// Test cleanup function if it exists // Metrics server is started inside provider when enabled; MetricsHandler is not exposed.
if cleanup != nil { if cleanup != nil {
err := cleanup() err := cleanup()
assert.NoError(t, err, "cleanup() should not return error") assert.NoError(t, err, "cleanup() should not return error")
@@ -165,12 +165,6 @@ func TestMetricsProviderNew_ConfigConversion(t *testing.T) {
require.NoError(t, err, "New() should not return error") require.NoError(t, err, "New() should not return error")
require.NotNil(t, telemetryProvider, "New() should return non-nil provider") require.NotNil(t, telemetryProvider, "New() should return non-nil provider")
// Verify that the provider was created (we can't directly check internal config,
// but we can verify the provider is functional)
if tt.expectedConfig.EnableMetrics {
assert.NotNil(t, telemetryProvider.MetricsHandler, "MetricsHandler should be set when metrics enabled")
}
if cleanup != nil { if cleanup != nil {
err := cleanup() err := cleanup()
assert.NoError(t, err, "cleanup() should not return error") assert.NoError(t, err, "cleanup() should not return error")
@@ -231,11 +225,6 @@ func TestMetricsProviderNew_BooleanParsing(t *testing.T) {
require.NoError(t, err, "New() should not return error") require.NoError(t, err, "New() should not return error")
require.NotNil(t, telemetryProvider, "New() should return non-nil provider") require.NotNil(t, telemetryProvider, "New() should return non-nil provider")
// Verify metrics handler is set based on enableMetrics
if tt.expected {
assert.NotNil(t, telemetryProvider.MetricsHandler, "MetricsHandler should be set when metrics enabled")
}
if cleanup != nil { if cleanup != nil {
err := cleanup() err := cleanup()
assert.NoError(t, err, "cleanup() should not return error") assert.NoError(t, err, "cleanup() should not return error")
@@ -300,9 +289,6 @@ func TestMetricsProviderNew_DefaultValues(t *testing.T) {
require.NoError(t, err, "New() should not return error with empty config") require.NoError(t, err, "New() should not return error with empty config")
require.NotNil(t, telemetryProvider, "New() should return non-nil provider") require.NotNil(t, telemetryProvider, "New() should return non-nil provider")
// Verify defaults are applied by checking that provider is functional
assert.NotNil(t, telemetryProvider.MetricsHandler, "MetricsHandler should be set with defaults")
if cleanup != nil { if cleanup != nil {
err := cleanup() err := cleanup()
assert.NoError(t, err, "cleanup() should not return error") assert.NoError(t, err, "cleanup() should not return error")

View File

@@ -3,6 +3,10 @@ package otelsetup
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"net/http"
"sync"
"time"
clientprom "github.com/prometheus/client_golang/prometheus" clientprom "github.com/prometheus/client_golang/prometheus"
clientpromhttp "github.com/prometheus/client_golang/prometheus/promhttp" clientpromhttp "github.com/prometheus/client_golang/prometheus/promhttp"
@@ -28,6 +32,7 @@ type Config struct {
ServiceVersion string `yaml:"serviceVersion"` ServiceVersion string `yaml:"serviceVersion"`
EnableMetrics bool `yaml:"enableMetrics"` EnableMetrics bool `yaml:"enableMetrics"`
Environment string `yaml:"environment"` Environment string `yaml:"environment"`
MetricsPort string `yaml:"metricsPort"`
} }
// DefaultConfig returns sensible defaults for telemetry configuration. // DefaultConfig returns sensible defaults for telemetry configuration.
@@ -37,6 +42,7 @@ func DefaultConfig() *Config {
ServiceVersion: "dev", ServiceVersion: "dev",
EnableMetrics: true, EnableMetrics: true,
Environment: "development", Environment: "development",
MetricsPort: "9090",
} }
} }
@@ -49,6 +55,7 @@ func ToPluginConfig(cfg *Config) *plugin.Config {
"serviceVersion": cfg.ServiceVersion, "serviceVersion": cfg.ServiceVersion,
"enableMetrics": fmt.Sprintf("%t", cfg.EnableMetrics), "enableMetrics": fmt.Sprintf("%t", cfg.EnableMetrics),
"environment": cfg.Environment, "environment": cfg.Environment,
"metricsPort": cfg.MetricsPort,
}, },
} }
} }
@@ -71,6 +78,9 @@ func (Setup) New(ctx context.Context, cfg *Config) (*telemetry.Provider, error)
if cfg.Environment == "" { if cfg.Environment == "" {
cfg.Environment = DefaultConfig().Environment cfg.Environment = DefaultConfig().Environment
} }
if cfg.MetricsPort == "" {
cfg.MetricsPort = DefaultConfig().MetricsPort
}
if !cfg.EnableMetrics { if !cfg.EnableMetrics {
log.Info(ctx, "OpenTelemetry metrics disabled") log.Info(ctx, "OpenTelemetry metrics disabled")
@@ -115,11 +125,44 @@ func (Setup) New(ctx context.Context, cfg *Config) (*telemetry.Provider, error)
log.Warnf(ctx, "Failed to start Go runtime instrumentation: %v", err) log.Warnf(ctx, "Failed to start Go runtime instrumentation: %v", err)
} }
// Create metrics handler
metricsHandler := clientpromhttp.HandlerFor(registry, clientpromhttp.HandlerOpts{})
// Create and start metrics HTTP server
metricsMux := http.NewServeMux()
metricsMux.Handle("/metrics", metricsHandler)
metricsServer := &http.Server{
Addr: net.JoinHostPort("", cfg.MetricsPort),
Handler: metricsMux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
var serverWg sync.WaitGroup
serverWg.Add(1)
go func() {
defer serverWg.Done()
log.Infof(ctx, "Metrics server listening on %s", metricsServer.Addr)
if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Errorf(ctx, fmt.Errorf("metrics server ListenAndServe: %w", err), "error listening and serving metrics")
}
}()
return &telemetry.Provider{ return &telemetry.Provider{
MeterProvider: meterProvider, MeterProvider: meterProvider,
MetricsHandler: clientpromhttp.HandlerFor(registry, clientpromhttp.HandlerOpts{}), Shutdown: func(shutdownCtx context.Context) error {
Shutdown: func(ctx context.Context) error { log.Infof(ctx, "Shutting down metrics server...")
return meterProvider.Shutdown(ctx) // Shutdown the metrics server
serverShutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
defer cancel()
if err := metricsServer.Shutdown(serverShutdownCtx); err != nil {
log.Errorf(ctx, fmt.Errorf("metrics server shutdown: %w", err), "error shutting down metrics server")
}
serverWg.Wait()
// Shutdown the meter provider
return meterProvider.Shutdown(shutdownCtx)
}, },
}, nil }, nil
} }

View File

@@ -2,8 +2,6 @@ package otelsetup
import ( import (
"context" "context"
"net/http"
"net/http/httptest"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -56,16 +54,7 @@ func TestSetup_New_Success(t *testing.T) {
require.NotNil(t, provider.Shutdown, "Provider should have shutdown function") require.NotNil(t, provider.Shutdown, "Provider should have shutdown function")
if tt.cfg.EnableMetrics { if tt.cfg.EnableMetrics {
assert.NotNil(t, provider.MetricsHandler, "MetricsHandler should be set when metrics enabled")
assert.NotNil(t, provider.MeterProvider, "MeterProvider should be set when metrics enabled") assert.NotNil(t, provider.MeterProvider, "MeterProvider should be set when metrics enabled")
// Test that metrics handler works
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics", nil)
provider.MetricsHandler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
} else {
assert.Nil(t, provider.MetricsHandler, "MetricsHandler should be nil when metrics disabled")
} }
// Test shutdown // Test shutdown
@@ -123,15 +112,8 @@ func TestSetup_New_DefaultValues(t *testing.T) {
require.NotNil(t, provider) require.NotNil(t, provider)
// Verify defaults are applied by checking that provider is functional // Verify defaults are applied by checking that provider is functional
assert.NotNil(t, provider.MetricsHandler, "MetricsHandler should be set with defaults")
assert.NotNil(t, provider.MeterProvider, "MeterProvider should be set with defaults") assert.NotNil(t, provider.MeterProvider, "MeterProvider should be set with defaults")
// Test metrics endpoint
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics", nil)
provider.MetricsHandler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
// Cleanup // Cleanup
err = provider.Shutdown(ctx) err = provider.Shutdown(ctx)
assert.NoError(t, err) assert.NoError(t, err)
@@ -152,8 +134,7 @@ func TestSetup_New_MetricsDisabled(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, provider) require.NotNil(t, provider)
// When metrics are disabled, MetricsHandler should be nil // When metrics are disabled, MetricsHandler should be nil and MeterProvider should be nil
assert.Nil(t, provider.MetricsHandler, "MetricsHandler should be nil when metrics disabled")
assert.Nil(t, provider.MeterProvider, "MeterProvider should be nil when metrics disabled") assert.Nil(t, provider.MeterProvider, "MeterProvider should be nil when metrics disabled")
// Shutdown should still work // Shutdown should still work
@@ -182,6 +163,7 @@ func TestToPluginConfig_Success(t *testing.T) {
"serviceVersion": "1.0.0", "serviceVersion": "1.0.0",
"enableMetrics": "true", "enableMetrics": "true",
"environment": "test", "environment": "test",
"metricsPort": "",
}, },
}, },
{ {
@@ -198,6 +180,7 @@ func TestToPluginConfig_Success(t *testing.T) {
"serviceVersion": "2.0.0", "serviceVersion": "2.0.0",
"enableMetrics": "false", "enableMetrics": "false",
"environment": "production", "environment": "production",
"metricsPort": "",
}, },
}, },
{ {
@@ -214,6 +197,7 @@ func TestToPluginConfig_Success(t *testing.T) {
"serviceVersion": "", "serviceVersion": "",
"enableMetrics": "true", "enableMetrics": "true",
"environment": "", "environment": "",
"metricsPort": "",
}, },
}, },
} }
@@ -263,11 +247,13 @@ func TestToPluginConfig_BooleanConversion(t *testing.T) {
ServiceVersion: "1.0.0", ServiceVersion: "1.0.0",
EnableMetrics: tt.enableMetrics, EnableMetrics: tt.enableMetrics,
Environment: "test", Environment: "test",
MetricsPort: "",
} }
result := ToPluginConfig(cfg) result := ToPluginConfig(cfg)
require.NotNil(t, result) require.NotNil(t, result)
assert.Equal(t, tt.expected, result.Config["enableMetrics"], "enableMetrics should be converted to string correctly") assert.Equal(t, tt.expected, result.Config["enableMetrics"], "enableMetrics should be converted to string correctly")
assert.Equal(t, "", result.Config["metricsPort"], "metricsPort should be included even when empty")
}) })
} }
} }