diff --git a/internal/cmd/base/internal/metric/build_info.go b/internal/cmd/base/internal/metric/build_info.go new file mode 100644 index 0000000000..552021f8c4 --- /dev/null +++ b/internal/cmd/base/internal/metric/build_info.go @@ -0,0 +1,50 @@ +// Package metric provides functions to initialize a prometheus metric +// detailing build info +package metric + +import ( + "runtime" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/version" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + labelGoVersion = "goversion" + labelGitRevision = "revision" + labelBoundaryVersion = "version" +) + +// buildInfoVec is a gauge metric whose value is always equal to 1 and whose +// labels contain the current go version, git revision, and boundary version. +var buildInfoVec = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: globals.MetricNamespace, + Name: "build_info", + Help: "Gauge with labels describing go version, git revision hash, and Boundary release version.", + }, + []string{labelGoVersion, labelGitRevision, labelBoundaryVersion}, +) + +func retrieveBuildInfoLabels() map[string]string { + verInfo := version.Get() + + return map[string]string{ + labelGoVersion: runtime.Version(), + labelGitRevision: verInfo.Revision, + labelBoundaryVersion: verInfo.Version, + } +} + +// InitializeBuildInfo registers the boundary_build_info metric with its +// correct labels and sets its value to 1. +func InitializeBuildInfo(r prometheus.Registerer) { + if r == nil { + return + } + + r.MustRegister(buildInfoVec) + l := prometheus.Labels(retrieveBuildInfoLabels()) + buildInfoVec.With(l).Set(float64(1)) +} diff --git a/internal/cmd/base/option.go b/internal/cmd/base/option.go index d5c4735e00..284ae02b34 100644 --- a/internal/cmd/base/option.go +++ b/internal/cmd/base/option.go @@ -4,6 +4,7 @@ import ( "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/boundary/sdk/pbs/plugin" wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/prometheus/client_golang/prometheus" ) // getOpts - iterate the inbound Options and return a struct. @@ -40,6 +41,7 @@ type Options struct { withStatusCode int withHostPlugin func() (string, plugin.HostPluginServiceClient) withEventGating bool + withPrometheusRegisterer prometheus.Registerer } func getDefaultOptions() Options { @@ -188,3 +190,10 @@ func WithEventGating(with bool) Option { o.withEventGating = with } } + +// WithPrometheusRegisterer uses the provided prometheus registerer +func WithPrometheusRegisterer(with prometheus.Registerer) Option { + return func(o *Options) { + o.withPrometheusRegisterer = with + } +} diff --git a/internal/cmd/base/option_test.go b/internal/cmd/base/option_test.go index ef4d913fe0..625c7d0fc2 100644 --- a/internal/cmd/base/option_test.go +++ b/internal/cmd/base/option_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/boundary/internal/observability/event" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" ) @@ -124,4 +125,12 @@ func Test_GetOpts(t *testing.T) { opts := getOpts(WithEventGating(true)) assert.True(opts.withEventGating) }) + t.Run("withPrometheusRegisterer", func(t *testing.T) { + assert := assert.New(t) + pmr := prometheus.NewRegistry() + opts := getOpts(WithPrometheusRegisterer(pmr)) + testOpts := getDefaultOptions() + testOpts.withPrometheusRegisterer = pmr + assert.Equal(opts, testOpts) + }) } diff --git a/internal/cmd/base/servers.go b/internal/cmd/base/servers.go index d1486b32ea..7bad4e1a4e 100644 --- a/internal/cmd/base/servers.go +++ b/internal/cmd/base/servers.go @@ -18,6 +18,7 @@ import ( "time" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/cmd/base/internal/metric" "github.com/hashicorp/boundary/internal/cmd/base/logging" "github.com/hashicorp/boundary/internal/cmd/config" "github.com/hashicorp/boundary/internal/db" @@ -133,7 +134,15 @@ type Server struct { StatusGracePeriodDuration time.Duration } -func NewServer(cmd *Command) *Server { +// The only option used here is WithPrometheusRegisterer; all others are ignored. +func NewServer(cmd *Command, opt ...Option) *Server { + // Create a new prometheus registry here to avoid "duplicate metrics collector + // registration" panics in tests where new servers are called consecutively. + // prometheus.DefaultRegisterer and prometheus.DefaultGatherer vars need to be + // assigned for promhttp package to work correctly. + opts := getOpts(opt...) + metric.InitializeBuildInfo(opts.withPrometheusRegisterer) + return &Server{ Command: cmd, InfoKeys: make([]string, 0, 20), @@ -142,7 +151,7 @@ func NewServer(cmd *Command) *Server { ReloadFuncsLock: new(sync.RWMutex), ReloadFuncs: make(map[string][]reloadutil.ReloadFunc), StderrLock: new(sync.Mutex), - PrometheusRegisterer: prometheus.DefaultRegisterer, + PrometheusRegisterer: opts.withPrometheusRegisterer, } } diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index eb54312c5b..554ba22c29 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -27,6 +27,7 @@ import ( "github.com/hashicorp/boundary/internal/cmd/commands/version" "github.com/mitchellh/cli" + "github.com/prometheus/client_golang/prometheus" ) // Commands is the mapping of all the available commands. @@ -36,14 +37,16 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Commands = map[string]cli.CommandFactory{ "server": func() (cli.Command, error) { return &server.Command{ - Server: base.NewServer(base.NewCommand(serverCmdUi)), + Server: base.NewServer(base.NewCommand(serverCmdUi), + base.WithPrometheusRegisterer(prometheus.DefaultRegisterer)), SighupCh: base.MakeSighupCh(), SigUSR2Ch: MakeSigUSR2Ch(), }, nil }, "dev": func() (cli.Command, error) { return &dev.Command{ - Server: base.NewServer(base.NewCommand(serverCmdUi)), + Server: base.NewServer(base.NewCommand(serverCmdUi), + base.WithPrometheusRegisterer(prometheus.DefaultRegisterer)), SighupCh: base.MakeSighupCh(), SigUSR2Ch: MakeSigUSR2Ch(), }, nil @@ -318,7 +321,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, "database init": func() (cli.Command, error) { return &database.InitCommand{ - Server: base.NewServer(base.NewCommand(ui)), + Server: base.NewServer(base.NewCommand(ui), base.WithPrometheusRegisterer(prometheus.DefaultRegisterer)), }, nil }, "database migrate": func() (cli.Command, error) {