diff --git a/README.md b/README.md index fbfd45e..115e888 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ reaches a different upstream with no change to the Worker. For rules neither covers, delegate the decision to an extension server you run. It is told what the call is addressing (the gRPC method, and the Namespace the proxy resolved from the request rather than from anything the caller claims), so it can decide per Namespace and per method rather than only whether the caller is who it says it is. +- **Prometheus metrics.** Expose request latency and counts, routing decisions, and encryption activity on `/metrics`. + The listen address and the namespace prefixed onto every metric are set under `metrics:` in the config. - **Codec-transparent.** The gateway never parses payloads. It peeks the Namespace, picks an upstream, and relays raw frames in both directions. - **Multiple deployment options.** Ship as a Go binary, a container image, or a Helm chart. diff --git a/cmd/proxy/serve.go b/cmd/proxy/serve.go index 0088983..4b88dc6 100644 --- a/cmd/proxy/serve.go +++ b/cmd/proxy/serve.go @@ -57,18 +57,6 @@ func serve() *cli.Command { Value: "info", Sources: cli.EnvVars("LOG_LEVEL"), }, - &cli.StringFlag{ - Name: "metrics-addr", - Usage: "The host:port on which to serve /metrics", - Value: ":9090", - Sources: cli.EnvVars("METRICS_ADDR"), - }, - &cli.StringFlag{ - Name: "metrics-namespace", - Usage: "The prometheus namespace for metrics", - Value: "tmprl_proxy", - Sources: cli.EnvVars("METRICS_NAMESPACE"), - }, }, Action: func(ctx context.Context, cmd *cli.Command) error { log := logger.NewZeroLogger(os.Stderr, logger.ParseLevel(cmd.String("level"))) @@ -77,8 +65,6 @@ func serve() *cli.Command { fx.Supply( fx.Annotate(ctx, fx.As(new(context.Context))), fx.Annotate(cmd.String("config"), config.ConfigFileTag), - fx.Annotate(cmd.String("metrics-addr"), metrics.AddrTag), - fx.Annotate(cmd.String("metrics-namespace"), metrics.NamespaceTag), fx.Annotate(protoregistry.GlobalFiles, fx.As(new(protoutil.Files))), fx.Annotate(protoregistry.GlobalTypes, fx.As(new(protoutil.Types))), // Services whose request and response types have their namespace diff --git a/internal/config/config.go b/internal/config/config.go index c98f13d..267e9b0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "cmp" "errors" "fmt" "io" @@ -17,17 +18,19 @@ type ( Config struct { Listen ListenConfig `yaml:",inline"` AllowedServices Services `yaml:"allowedServices"` + Auth *AuthConfig `yaml:"auth"` Encryption Encryption `yaml:"encryption"` ExtensionServers ExtensionServerList `yaml:"extensionServers"` + Metrics Metrics `yaml:"metrics"` Routing Routing `yaml:"routing"` Upstreams UpstreamList `yaml:"upstreams"` - Auth *AuthConfig `yaml:"auth"` } ) // Load reads and parses the YAML config specified in the Reader. // Values of the form ${VAR} are replaced with the corresponding environment -// variable. A config that names no allowed services gets the default set. +// variable. A config that names no allowed services gets the default set, and +// one that leaves a metrics field empty gets that field's default. func Load(r io.Reader) (*Config, error) { data, err := io.ReadAll(r) if err != nil { @@ -46,6 +49,13 @@ func Load(r io.Reader) (*Config, error) { // configs are written. cfg.AllowedServices = cfg.AllowedServices.Allowed() + // Defaulted here for the same reason as the allowlist: an absent metrics + // block never reaches an unmarshaler, and most configs omit it entirely. + // The config is the only way to set these, so defaulting is what keeps + // /metrics served for a config that says nothing about it. + cfg.Metrics.HostPort = cmp.Or(cfg.Metrics.HostPort, ":9090") + cfg.Metrics.Namespace = cmp.Or(cfg.Metrics.Namespace, "tmprl_proxy") + return &cfg, nil } @@ -84,6 +94,7 @@ func (c *Config) Validate() error { validation.Nested("", &c.AllowedServices), validation.Nested("encryption", &c.Encryption), validation.Nested("extensionServers", &c.ExtensionServers), + validation.Nested("metrics", &c.Metrics), validation.Nested("routing", &c.Routing), validation.WhenRules(func() bool { return c.Auth != nil }, validation.Nested("auth", c.Auth)), validation.Nested("upstreams", &c.Upstreams), diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 85fc834..03fe95b 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -33,6 +33,7 @@ func TestLoad(t *testing.T) { want: &config.Config{ Listen: config.ListenConfig{HostPort: ":8080"}, AllowedServices: config.Services(services.Default()), + Metrics: defaultMetrics(), }, }, { @@ -43,7 +44,10 @@ func TestLoad(t *testing.T) { { name: "empty hostPort", yaml: "hostPort: \"\"\n", - want: &config.Config{AllowedServices: config.Services(services.Default())}, + want: &config.Config{ + AllowedServices: config.Services(services.Default()), + Metrics: defaultMetrics(), + }, }, } @@ -143,6 +147,7 @@ func TestLoadFile(t *testing.T) { want: &config.Config{ Listen: config.ListenConfig{HostPort: ":7233"}, AllowedServices: config.Services(services.Default()), + Metrics: defaultMetrics(), }, }, { @@ -199,6 +204,7 @@ func TestConfig_Validate(t *testing.T) { { name: "valid hostPort, no TLS", cfg: &config.Config{ + Metrics: defaultMetrics(), Listen: config.ListenConfig{HostPort: ":8080"}, Upstreams: validUpstreams, }, @@ -206,6 +212,7 @@ func TestConfig_Validate(t *testing.T) { { name: "invalid hostPort surfaces from ListenConfig", cfg: &config.Config{ + Metrics: defaultMetrics(), Listen: config.ListenConfig{HostPort: "localhost"}, Upstreams: validUpstreams, }, @@ -214,6 +221,7 @@ func TestConfig_Validate(t *testing.T) { { name: "broken TLS surfaces with tls subject stamped by parent", cfg: &config.Config{ + Metrics: defaultMetrics(), Listen: config.ListenConfig{ HostPort: ":8080", TLS: &config.TLSConfig{}, // empty -> "a server certificate is required" @@ -227,6 +235,7 @@ func TestConfig_Validate(t *testing.T) { { name: "hostPort and TLS failures aggregate", cfg: &config.Config{ + Metrics: defaultMetrics(), Listen: config.ListenConfig{ HostPort: "localhost", TLS: &config.TLSConfig{}, @@ -241,14 +250,16 @@ func TestConfig_Validate(t *testing.T) { { name: "no upstreams surfaces on the upstreams field", cfg: &config.Config{ - Listen: config.ListenConfig{HostPort: ":8080"}, + Metrics: defaultMetrics(), + Listen: config.ListenConfig{HostPort: ":8080"}, }, wantTuples: [][2]string{{"", "upstreams"}}, }, { name: "missing upstream hostPort surfaces with indexed upstream subject", cfg: &config.Config{ - Listen: config.ListenConfig{HostPort: ":8080"}, + Metrics: defaultMetrics(), + Listen: config.ListenConfig{HostPort: ":8080"}, Upstreams: []config.Upstream{{ Name: "primary", }}, @@ -258,7 +269,8 @@ func TestConfig_Validate(t *testing.T) { { name: "empty upstream name surfaces with indexed upstream subject", cfg: &config.Config{ - Listen: config.ListenConfig{HostPort: ":8080"}, + Metrics: defaultMetrics(), + Listen: config.ListenConfig{HostPort: ":8080"}, Upstreams: []config.Upstream{{ Listen: config.ListenConfig{HostPort: "127.0.0.1:7233"}, }}, @@ -268,7 +280,8 @@ func TestConfig_Validate(t *testing.T) { { name: "duplicate upstream names surface on the upstreams[name] field", cfg: &config.Config{ - Listen: config.ListenConfig{HostPort: ":8080"}, + Metrics: defaultMetrics(), + Listen: config.ListenConfig{HostPort: ":8080"}, Upstreams: []config.Upstream{ {Name: "dup", Listen: config.ListenConfig{HostPort: "127.0.0.1:7233"}}, {Name: "dup", Listen: config.ListenConfig{HostPort: "127.0.0.1:7234"}}, @@ -279,6 +292,7 @@ func TestConfig_Validate(t *testing.T) { { name: "enabled encryption without default surfaces with encryption subject", cfg: &config.Config{ + Metrics: defaultMetrics(), Listen: config.ListenConfig{HostPort: ":8080"}, Encryption: config.Encryption{Enabled: true}, Upstreams: validUpstreams, @@ -288,6 +302,7 @@ func TestConfig_Validate(t *testing.T) { { name: "invalid default policy surfaces with composed encryption.default subject", cfg: &config.Config{ + Metrics: defaultMetrics(), Listen: config.ListenConfig{HostPort: ":8080"}, Encryption: config.Encryption{Default: &badPolicy}, Upstreams: validUpstreams, @@ -297,7 +312,8 @@ func TestConfig_Validate(t *testing.T) { { name: "templated upstream hostPort is accepted", cfg: &config.Config{ - Listen: config.ListenConfig{HostPort: ":8080"}, + Metrics: defaultMetrics(), + Listen: config.ListenConfig{HostPort: ":8080"}, Upstreams: []config.Upstream{{ Name: "templated", Listen: config.ListenConfig{HostPort: "{{ .RemoteNamespace }}.acme-cloud.tmprl.cloud:7233"}, @@ -334,6 +350,7 @@ func TestConfig_Validate_RoutingReferences(t *testing.T) { base := func(r config.Routing) *config.Config { return &config.Config{ Listen: config.ListenConfig{HostPort: ":8080"}, + Metrics: defaultMetrics(), Routing: r, Upstreams: []config.Upstream{ {Name: "primary", Listen: config.ListenConfig{HostPort: "127.0.0.1:7233"}}, @@ -413,6 +430,7 @@ func TestConfig_Validate_ExternalAuthReferences(t *testing.T) { base := func(ext *config.ExternalAuthConfig) *config.Config { return &config.Config{ Listen: config.ListenConfig{HostPort: ":8080"}, + Metrics: defaultMetrics(), Upstreams: []config.Upstream{{Name: "primary", Listen: config.ListenConfig{HostPort: "127.0.0.1:7233"}}}, ExtensionServers: config.ExtensionServerList{ {Name: "policy", Listen: config.ListenConfig{HostPort: "127.0.0.1:9000"}}, @@ -457,7 +475,8 @@ func TestConfig_ValidateRejectsDuplicateHostPorts(t *testing.T) { t.Parallel() cfg := &config.Config{ - Listen: config.ListenConfig{HostPort: "127.0.0.1:8443"}, + Listen: config.ListenConfig{HostPort: "127.0.0.1:8443"}, + Metrics: defaultMetrics(), Upstreams: []config.Upstream{ {Name: "a", Listen: config.ListenConfig{HostPort: "127.0.0.1:7233"}}, {Name: "b", Listen: config.ListenConfig{HostPort: "127.0.0.1:7233"}}, @@ -490,6 +509,10 @@ func TestUpstream_IsTemplated(t *testing.T) { func (e *errReader) Read(_ []byte) (int, error) { return 0, e.err } +func defaultMetrics() config.Metrics { + return config.Metrics{HostPort: ":9090", Namespace: "tmprl_proxy"} +} + func urlStrings(us []url.URL) []string { out := make([]string, len(us)) for i := range us { diff --git a/internal/config/encryption_test.go b/internal/config/encryption_test.go index a737bb8..19efb54 100644 --- a/internal/config/encryption_test.go +++ b/internal/config/encryption_test.go @@ -210,6 +210,7 @@ func TestConfig_ValidateExtensionKeyReferences(t *testing.T) { return &config.Config{ Listen: config.ListenConfig{HostPort: ":8080"}, Encryption: e, + Metrics: defaultMetrics(), ExtensionServers: config.ExtensionServerList{ {Name: "audit", Listen: config.ListenConfig{HostPort: "127.0.0.1:9090"}}, }, diff --git a/internal/config/extensions_test.go b/internal/config/extensions_test.go index 0ce2922..790d413 100644 --- a/internal/config/extensions_test.go +++ b/internal/config/extensions_test.go @@ -257,6 +257,7 @@ func TestConfig_Validate_ExtensionServers(t *testing.T) { return &config.Config{ Listen: config.ListenConfig{HostPort: ":8080"}, ExtensionServers: servers, + Metrics: defaultMetrics(), Upstreams: config.UpstreamList{ {Name: "primary", Listen: config.ListenConfig{HostPort: "127.0.0.1:7233"}}, }, diff --git a/internal/config/fx_test.go b/internal/config/fx_test.go index 88bb8d0..126fddb 100644 --- a/internal/config/fx_test.go +++ b/internal/config/fx_test.go @@ -30,6 +30,7 @@ func TestModule_ProvidesConfig(t *testing.T) { require.Equal(t, &config.Config{ Listen: config.ListenConfig{HostPort: ":7233"}, AllowedServices: config.Services(services.Default()), + Metrics: defaultMetrics(), }, got) } diff --git a/internal/config/metrics.go b/internal/config/metrics.go new file mode 100644 index 0000000..99b1e58 --- /dev/null +++ b/internal/config/metrics.go @@ -0,0 +1,23 @@ +package config + +import "github.com/temporalio/temporal-proxy/pkg/validation" + +// Metrics configures the Prometheus endpoint. HostPort is the address the +// /metrics handler listens on, and Namespace is the prefix stamped onto every +// collector: a Prometheus namespace, unrelated to a Temporal namespace. Load +// defaults both, so neither is empty in a loaded config. +type Metrics struct { + HostPort string `yaml:"hostPort"` + Namespace string `yaml:"namespace"` +} + +// Validate requires a valid host:port and a non-empty namespace. Load defaults +// both, so a namespace failure is only reachable for a Metrics built directly, +// and a hostPort failure only for a config that sets one that will not parse. +func (m *Metrics) Validate() error { + return validation.Validate( + "", + validation.Field("hostPort", m.HostPort, validation.IsHostPort()), + validation.Field("namespace", m.Namespace, validation.Required[string]()), + ) +} diff --git a/internal/config/metrics_test.go b/internal/config/metrics_test.go new file mode 100644 index 0000000..0168cef --- /dev/null +++ b/internal/config/metrics_test.go @@ -0,0 +1,86 @@ +package config_test + +import ( + "errors" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/temporalio/temporal-proxy/internal/config" + "github.com/temporalio/temporal-proxy/pkg/validation" +) + +func TestMetrics_Validate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cfg *config.Metrics + wantErrs []validation.Error + }{} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := tt.cfg.Validate() + if len(tt.wantErrs) == 0 { + require.NoError(t, err) + return + } + + var errs validation.Errors + require.True(t, errors.As(err, &errs), "expected validation.Errors, got %T", err) + require.ElementsMatch(t, tt.wantErrs, []validation.Error(errs)) + }) + } +} + +func TestLoad_MetricsDefaults(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + yaml string + want config.Metrics + }{ + { + name: "absent metrics block gets both defaults", + yaml: "hostPort: :8080\n", + want: config.Metrics{HostPort: ":9090", Namespace: "tmprl_proxy"}, + }, + { + name: "explicit values are preserved", + yaml: "metrics:\n hostPort: 127.0.0.1:8888\n namespace: acme\n", + want: config.Metrics{HostPort: "127.0.0.1:8888", Namespace: "acme"}, + }, + { + name: "each field defaults on its own", + yaml: "metrics:\n hostPort: :7070\n", + want: config.Metrics{HostPort: ":7070", Namespace: "tmprl_proxy"}, + }, + { + name: "namespace only", + yaml: "metrics:\n namespace: acme\n", + want: config.Metrics{HostPort: ":9090", Namespace: "acme"}, + }, + { + // cmp.Or cannot tell an explicit empty string from an absent key, so + // writing "" is not a way to opt out of the default. + name: "explicit empty strings still default", + yaml: "metrics:\n hostPort: \"\"\n namespace: \"\"\n", + want: config.Metrics{HostPort: ":9090", Namespace: "tmprl_proxy"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := config.Load(strings.NewReader(tt.yaml)) + require.NoError(t, err) + require.Equal(t, tt.want, got.Metrics) + }) + } +} diff --git a/internal/config/services_test.go b/internal/config/services_test.go index a08ff4b..5bbab0a 100644 --- a/internal/config/services_test.go +++ b/internal/config/services_test.go @@ -164,6 +164,7 @@ func TestConfig_Validate_AllowedServices(t *testing.T) { return &config.Config{ Listen: config.ListenConfig{HostPort: ":8080"}, AllowedServices: svcs, + Metrics: defaultMetrics(), Upstreams: config.UpstreamList{ {Name: "primary", Listen: config.ListenConfig{HostPort: "127.0.0.1:7233"}}, }, diff --git a/internal/dataplane/dataplane_test.go b/internal/dataplane/dataplane_test.go index 0901797..58b6b45 100644 --- a/internal/dataplane/dataplane_test.go +++ b/internal/dataplane/dataplane_test.go @@ -141,7 +141,6 @@ func TestNewRejectsConfiguredKeysWithoutVault(t *testing.T) { }) } } - func TestNewTwiceOverOneMetricsFactoryDoesNotPanic(t *testing.T) { t.Parallel() @@ -196,10 +195,12 @@ func (d testDeps) opts(omit ...string) []dataplane.Option { } // testConfig is a minimal valid configuration: one gateway listener and one -// static upstream. +// static upstream. Metrics is populated because Config.Validate requires it; +// nothing in these tests serves it. func testConfig() *config.Config { return &config.Config{ Listen: config.ListenConfig{HostPort: "127.0.0.1:0"}, + Metrics: config.Metrics{HostPort: "127.0.0.1:0", Namespace: "test"}, Routing: config.Routing{DefaultUpstream: "primary"}, Upstreams: config.UpstreamList{{ Name: "primary", diff --git a/internal/dataplane/dataplanetest/dataplanetest.go b/internal/dataplane/dataplanetest/dataplanetest.go index 9a4debf..792f2ab 100644 --- a/internal/dataplane/dataplanetest/dataplanetest.go +++ b/internal/dataplane/dataplanetest/dataplanetest.go @@ -148,8 +148,6 @@ func StartApp(t *testing.T, cfg *config.Config) *Fixture { app := fx.New( fx.Supply(fx.Annotate(t.Context(), fx.As(new(context.Context)))), fx.Supply(cfg), - fx.Supply(fx.Annotate("127.0.0.1:0", metrics.AddrTag)), - fx.Supply(fx.Annotate("test", metrics.NamespaceTag)), fx.Provide( func() logger.Logger { return logger.NewNoopLogger() }, func() prometheus.Gatherer { return reg }, @@ -229,6 +227,17 @@ func applyDefaults(cfg *config.Config) { if len(cfg.AllowedServices) == 0 { cfg.AllowedServices = config.Services(services.Known()) } + + // Also a Load default, and Config.Validate requires both. The address is + // ephemeral so parallel apps never contend for a port, and the namespace + // matches the factory Start builds directly. + if cfg.Metrics.HostPort == "" { + cfg.Metrics.HostPort = "127.0.0.1:0" + } + + if cfg.Metrics.Namespace == "" { + cfg.Metrics.Namespace = "test" + } } // newFixture dials the running gateway. gRPC connects lazily, so this opens diff --git a/internal/metrics/factory_test.go b/internal/metrics/factory_test.go index d407288..fcac939 100644 --- a/internal/metrics/factory_test.go +++ b/internal/metrics/factory_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/fx" + "github.com/temporalio/temporal-proxy/internal/config" "github.com/temporalio/temporal-proxy/internal/metrics" "github.com/temporalio/temporal-proxy/pkg/logger" ) @@ -176,8 +177,7 @@ func TestModuleProvidesNamespacedMetrics(t *testing.T) { var m *metrics.Factory app := fx.New( fx.Supply( - fx.Annotate(freeAddr(t), metrics.AddrTag), - fx.Annotate("wired", metrics.NamespaceTag), + &config.Config{Metrics: config.Metrics{HostPort: freeAddr(t), Namespace: "wired"}}, fx.Annotate(reg, fx.As(new(goprom.Registerer))), fx.Annotate(reg, fx.As(new(goprom.Gatherer))), fx.Annotate(logger.NewNoopLogger(), fx.As(new(logger.Logger))), @@ -189,8 +189,8 @@ func TestModuleProvidesNamespacedMetrics(t *testing.T) { require.NoError(t, app.Err()) require.NotNil(t, m) - // The provider must feed the "metricsNamespace" value into New, so a counter - // built from the injected Metrics carries the "wired" prefix. + // The provider must feed the config's metrics namespace into New, so a + // counter built from the injected Factory carries the "wired" prefix. m.NewCounter(goprom.CounterOpts{Name: "wired_total", Help: "Wired."}, nil).WithLabelValues().Inc() require.NoError(t, testutil.GatherAndCompare(reg, strings.NewReader(` diff --git a/internal/metrics/fx.go b/internal/metrics/fx.go index 37ac523..96930f8 100644 --- a/internal/metrics/fx.go +++ b/internal/metrics/fx.go @@ -11,23 +11,14 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/fx" + "github.com/temporalio/temporal-proxy/internal/config" "github.com/temporalio/temporal-proxy/pkg/logger" "github.com/temporalio/temporal-proxy/pkg/logger/tag" ) -var ( - // AddrTag annotates the host:port the metrics HTTP server listens on, - // supplied to fx as the named value "metricsAddr". - AddrTag = fx.ResultTags(`name:"metricsAddr"`) - - // NamespaceTag annotates the Prometheus namespace prefixed onto every - // collector, supplied to fx as the named value "metricsNamespace". - NamespaceTag = fx.ResultTags(`name:"metricsNamespace"`) -) - // Module provides a namespaced [Factory] bound to the injected Prometheus -// registry and serves the registry at /metrics on the address named -// "metricsAddr". Consumers inject the [Factory] to declare their collectors, +// registry and serves the registry at /metrics on the address the injected +// config names. Consumers inject the [Factory] to declare their collectors, // which auto-register under the configured namespace, and should pre-resolve // labeled handles once at setup rather than per request to keep the emit path // lock-free and allocation-free. @@ -38,10 +29,10 @@ var ( // down with a non-zero exit code. var Module = fx.Options( fx.Provide(func(p MetricsParams) *Factory { - return New(p.Namespace, promauto.With(p.Registerer)) + return New(p.Config.Metrics.Namespace, promauto.With(p.Registerer)) }), fx.Invoke(func(p MetricsParams) error { - if p.Addr == "" { + if p.Config.Metrics.HostPort == "" { return errors.New("metrics addr not set") } @@ -51,7 +42,7 @@ var Module = fx.Options( })) svr := &http.Server{ - Addr: p.Addr, + Addr: p.Config.Metrics.HostPort, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, @@ -59,7 +50,7 @@ var Module = fx.Options( log := p.Logger.With( tag.Component("metrics"), - tag.String("addr", p.Addr), + tag.String("addr", p.Config.Metrics.HostPort), ) p.Lifecycle.Append(fx.Hook{ @@ -91,19 +82,18 @@ var Module = fx.Options( ) // MetricsParams holds the fx-injected dependencies needed to run the metrics -// HTTP server and build the namespaced [Factory]. Addr is the named -// "metricsAddr" listen address and Namespace is the named "metricsNamespace" -// Prometheus prefix. Registerer is where collectors register and Gatherer is -// what the /metrics handler scrapes; supplying both lets callers (and tests) -// choose between the package-global registry and an isolated one. +// HTTP server and build the namespaced [Factory]. Config supplies the listen +// address and the Prometheus prefix through its Metrics block. Registerer is +// where collectors register and Gatherer is what the /metrics handler scrapes; +// supplying both lets callers (and tests) choose between the package-global +// registry and an isolated one. type MetricsParams struct { fx.In Lifecycle fx.Lifecycle Shutdowner fx.Shutdowner - Addr string `name:"metricsAddr"` - Namespace string `name:"metricsNamespace"` - Logger logger.Logger + Config *config.Config + Logger logger.Logger Gatherer prometheus.Gatherer Registerer prometheus.Registerer diff --git a/internal/metrics/fx_test.go b/internal/metrics/fx_test.go index 4733905..733b613 100644 --- a/internal/metrics/fx_test.go +++ b/internal/metrics/fx_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/fx" + "github.com/temporalio/temporal-proxy/internal/config" "github.com/temporalio/temporal-proxy/internal/metrics" "github.com/temporalio/temporal-proxy/pkg/logger" ) @@ -94,16 +95,11 @@ func TestModule(t *testing.T) { t.Run("requires the metrics address", func(t *testing.T) { t.Parallel() - reg := goprom.NewRegistry() - app := fx.New( - fx.Supply( - fx.Annotate(reg, fx.As(new(goprom.Registerer))), - fx.Annotate(reg, fx.As(new(goprom.Gatherer))), - ), - metrics.Module, - fx.NopLogger, - ) - require.Error(t, app.Err()) + // Every other dependency is supplied, so the empty hostPort is the only + // thing that can fail the app, and the message proves it was the guard + // rather than an unsatisfied constructor. + app := newTestApp(t, "") + require.ErrorContains(t, app.Err(), "metrics addr not set") }) } @@ -114,8 +110,7 @@ func newTestApp(t *testing.T, addr string, opts ...fx.Option) *fx.App { base := []fx.Option{ fx.Supply( - fx.Annotate(addr, metrics.AddrTag), - fx.Annotate("tmprl_proxy", metrics.NamespaceTag), + &config.Config{Metrics: config.Metrics{HostPort: addr, Namespace: "tmprl_proxy"}}, fx.Annotate(reg, fx.As(new(goprom.Registerer))), fx.Annotate(reg, fx.As(new(goprom.Gatherer))), fx.Annotate(logger.NewNoopLogger(), fx.As(new(logger.Logger))),