diff --git a/node/cmd/node/main.go b/node/cmd/node/main.go index 235fedc2d..de0d7b47f 100644 --- a/node/cmd/node/main.go +++ b/node/cmd/node/main.go @@ -37,6 +37,7 @@ import ( "morph-l2/node/sequencer/mock" "morph-l2/node/sync" "morph-l2/node/types" + "morph-l2/node/version" ) func main() { @@ -81,6 +82,12 @@ func L2NodeMain(ctx *cli.Context) error { return err } + // Publish build/version metadata as morphnode_info{...} 1 (mirrors morph-reth's + // reth_info{} gauge). Registers on the default Prometheus registry, so it's + // exposed regardless of which branch below brings up /metrics (own listener in + // layer1-verify mode, or Tendermint's instrumentation in every other mode). + version.PrometheusMetrics("morphnode", Version, GitCommit, BuildTime).Register() + // Wire the centralized-sequencer upgrade time into the consensus upgrade package before any // consensus / blocksync routine runs. A user-provided --sequencerUpgradeTime wins. If it is // absent, --mainnet / --hoodi selects the corresponding network default. With no network and no diff --git a/node/go.mod b/node/go.mod index c0e8107b4..a4232f72d 100644 --- a/node/go.mod +++ b/node/go.mod @@ -16,6 +16,7 @@ require ( github.com/morph-l2/go-ethereum v1.10.14-0.20260908092055-4012f174b967 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.17.0 + github.com/prometheus/client_model v0.5.0 github.com/spf13/viper v1.13.0 github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a @@ -98,7 +99,6 @@ require ( github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/tsdb v0.10.0 // indirect diff --git a/node/version/metrics.go b/node/version/metrics.go new file mode 100644 index 000000000..827ab7960 --- /dev/null +++ b/node/version/metrics.go @@ -0,0 +1,54 @@ +// Package version exposes morphnode's build/version metadata as a Prometheus +// info-style gauge, mirroring morph-reth's reth_info{} gauge: a static gauge +// permanently set to 1, whose Prometheus labels (not its value) carry the +// actual information. Scraping tools use this to build a "which build/version +// is running where" view (e.g. `max by (version) (morphnode_info{})`). +package version + +import ( + "runtime" + + "github.com/go-kit/kit/metrics" + "github.com/go-kit/kit/metrics/discard" + "github.com/go-kit/kit/metrics/prometheus" + stdprometheus "github.com/prometheus/client_golang/prometheus" +) + +// Info is a static Prometheus gauge carrying morphnode's build metadata as +// labels. Its value is always 1; only the labels are meaningful. +type Info struct { + gauge metrics.Gauge +} + +// PrometheusMetrics registers the morphnode_info gauge on the default +// Prometheus registry under the given namespace (no subsystem: the resulting +// metric name is "_info", e.g. "morphnode_info"). version, +// gitCommit and buildTime should be the values injected via -ldflags at build +// time (see cmd/node/version.go); goVersion/os/arch are read from the runtime. +func PrometheusMetrics(namespace, version, gitCommit, buildTime string) *Info { + labels := []string{"version", "git_commit", "build_time", "go_version", "os", "arch"} + g := prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Name: "info", + Help: "Static info metric (always 1) carrying morphnode build/version metadata as labels.", + }, labels).With( + "version", version, + "git_commit", gitCommit, + "build_time", buildTime, + "go_version", runtime.Version(), + "os", runtime.GOOS, + "arch", runtime.GOARCH, + ) + return &Info{gauge: g} +} + +// NopMetrics returns an Info that discards its observation (no registration). +// Mirrors the NopMetrics helpers used by the other metrics packages (e.g. +// hakeeper.NopMetrics) for tests that don't need a live Prometheus registry. +func NopMetrics() *Info { + return &Info{gauge: discard.NewGauge()} +} + +// Register sets the gauge to 1, publishing the labels attached at +// construction time. Call once at startup. +func (i *Info) Register() { i.gauge.Set(1) } diff --git a/node/version/metrics_test.go b/node/version/metrics_test.go new file mode 100644 index 000000000..c4421c51e --- /dev/null +++ b/node/version/metrics_test.go @@ -0,0 +1,53 @@ +package version + +import ( + "testing" + + stdprometheus "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" +) + +// TestPrometheusMetrics_RegistersInfoGaugeSetToOne verifies that Register +// publishes a single gauge, always valued 1, carrying the build metadata +// passed to PrometheusMetrics as labels. +func TestPrometheusMetrics_RegistersInfoGaugeSetToOne(t *testing.T) { + // Scoped per-test namespace: dodges "duplicate metrics collector + // registration" panics against the shared default registry, matching the + // pattern used in derivation/base_client_test.go. + namespace := "morphnode_test_" + t.Name() + + info := PrometheusMetrics(namespace, "v1.2.3", "abc1234", "2026-09-22T00:00:00Z") + info.Register() + + mfs, err := stdprometheus.DefaultGatherer.Gather() + require.NoError(t, err) + + var found *dto.Metric + for _, mf := range mfs { + if mf.GetName() == namespace+"_info" { + require.Len(t, mf.GetMetric(), 1) + found = mf.GetMetric()[0] + break + } + } + require.NotNil(t, found, "expected %s_info gauge to be registered", namespace) + require.Equal(t, float64(1), found.GetGauge().GetValue()) + + labels := map[string]string{} + for _, l := range found.GetLabel() { + labels[l.GetName()] = l.GetValue() + } + require.Equal(t, "v1.2.3", labels["version"]) + require.Equal(t, "abc1234", labels["git_commit"]) + require.Equal(t, "2026-09-22T00:00:00Z", labels["build_time"]) + require.NotEmpty(t, labels["go_version"]) + require.NotEmpty(t, labels["os"]) + require.NotEmpty(t, labels["arch"]) +} + +// TestNopMetrics_DoesNotPanic verifies NopMetrics().Register() is a safe +// no-op, for callers (e.g. tests) that don't want a live Prometheus registry. +func TestNopMetrics_DoesNotPanic(t *testing.T) { + require.NotPanics(t, func() { NopMetrics().Register() }) +}