Repository: scionproto/scion
Pull request #4530 (2024-05-23 12:14:26)

pkg/metrics: introduce v2
Introduce a v2 version of pkg/metrics. The current metrics package is not optimal:
- Using objects that have been called with With allocated on each increment/set value call.
- Having the option to call With ties consumer code with initialization code.
- There is no way whether a counter is fully initialized with all labels.

A full discussion can be found in
https://github.com/scionproto/scion/pull/4527.

The new v2 version is compatible with prometheus, so no explicit conversion functions are offered, instead the prometheus creation functions can be used directly.

So far the new library has been plugged in selected places. The intention is that it will eventually replace the current library completely, however doing so in a single sweep would be too extensive.

matzf: 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/4530)
<!-- Reviewable:end -->


Patch:
diff --git a/control/BUILD.bazel b/control/BUILD.bazel
index 59193ad48..57b4489eb 100644
--- a/control/BUILD.bazel
+++ b/control/BUILD.bazel
@@ -28,6 +28,7 @@ go_library(
         "//pkg/grpc:go_default_library",
         "//pkg/log:go_default_library",
         "//pkg/metrics:go_default_library",
+        "//pkg/metrics/v2:go_default_library",
         "//pkg/private/ctrl/path_mgmt:go_default_library",
         "//pkg/private/prom:go_default_library",
         "//pkg/private/serrors:go_default_library",
diff --git a/control/observability.go b/control/observability.go
index 512eaabac..2ddabc1fc 100644
--- a/control/observability.go
+++ b/control/observability.go
@@ -28,6 +28,7 @@ import (
 	cstrust "github.com/scionproto/scion/control/trust"
 	"github.com/scionproto/scion/pkg/addr"
 	"github.com/scionproto/scion/pkg/metrics"
+	metrics2 "github.com/scionproto/scion/pkg/metrics/v2"
 	"github.com/scionproto/scion/pkg/private/prom"
 	"github.com/scionproto/scion/pkg/private/serrors"
 	"github.com/scionproto/scion/pkg/scrypto"
@@ -82,7 +83,7 @@ type Metrics struct {
 	TrustTRCFileWritesTotal                *prometheus.CounterVec
 	SCIONNetworkMetrics                    snet.SCIONNetworkMetrics
 	SCIONPacketConnMetrics                 snet.SCIONPacketConnMetrics
-	SCMPErrors                             metrics.Counter
+	SCMPErrors                             metrics2.Counter
 	TopoLoader                             topology.LoaderMetrics
 	DRKeySecretValueQueriesTotal           *prometheus.CounterVec
 	DRKeyLevel1QueriesTotal                *prometheus.CounterVec
diff --git a/doc/dev/style/go.rst b/doc/dev/style/go.rst
index efc4063a6..e3be4d665 100644
--- a/doc/dev/style/go.rst
+++ b/doc/dev/style/go.rst
@@ -113,29 +113,20 @@ metrics, and for operators to understand where metrics are coming from. As a
 bonus, we should leverage the type system to help us spot as many errors as
 possible.
 
-To write code that both includes metrics, and is testable, we use the
-`metric recommendations from the go-kit project <https://godoc.org/github.com/go-kit/kit/metrics>`__.
+To write code that both includes metrics, and is testable, we use the metric
+interfaces defined in the ``pkg/metrics/v2`` package.
 
-A simple example with labels (note that ``Foo``'s metrics can be unit tested by mocking the counter):
+A simple example with labels (note that ``Giant``'s metrics can be unit tested by
+mocking the counter):
 
-.. literalinclude:: /../pkg/metrics/metrics_test.go
-   :language: Go
-   :dedent: 1
-   :start-after: LITERALINCLUDE ExampleCounter_Interface START
-   :end-before: LITERALINCLUDE ExampleCounter_Interface END
-
-Calling code can later create ``Giant`` objects with Prometheus metric reporting
-by plugging a prometheus counter as the ``Counter``. The Prometheus objects can be
-obtained from the metrics packages in the following way:
-
-.. literalinclude:: /../pkg/metrics/metrics_test.go
+.. literalinclude:: /../pkg/metrics/v2/metrics_test.go
    :language: Go
    :dedent: 1
    :start-after: LITERALINCLUDE ExampleCounter_Implementation START
    :end-before: LITERALINCLUDE ExampleCounter_Implementation END
 
-In cases where performance is a concern, consider applying the labels outside of
-the performance-critical section.
+Calling code can later create ``Giant`` objects with Prometheus metric reporting
+by plugging a prometheus counter as the ``Counter`` as shown in the example.
 
 .. note::
    Some packages have ``metrics`` packages that define labels and initialize
diff --git a/gateway/BUILD.bazel b/gateway/BUILD.bazel
index b258a5598..a41c7e365 100644
--- a/gateway/BUILD.bazel
+++ b/gateway/BUILD.bazel
@@ -25,6 +25,7 @@ go_library(
         "//pkg/grpc:go_default_library",
         "//pkg/log:go_default_library",
         "//pkg/metrics:go_default_library",
+        "//pkg/metrics/v2:go_default_library",
         "//pkg/private/serrors:go_default_library",
         "//pkg/private/util:go_default_library",
         "//pkg/proto/gateway:go_default_library",
diff --git a/gateway/metrics.go b/gateway/metrics.go
index bdf1d80a1..ebf647d4f 100644
--- a/gateway/metrics.go
+++ b/gateway/metrics.go
@@ -19,7 +19,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus/promauto"
 
 	"github.com/scionproto/scion/pkg/addr"
-	"github.com/scionproto/scion/pkg/metrics"
+	"github.com/scionproto/scion/pkg/metrics/v2"
 	"github.com/scionproto/scion/pkg/snet"
 	snetmetrics "github.com/scionproto/scion/pkg/snet/metrics"
 )
diff --git a/gateway/pathhealth/BUILD.bazel b/gateway/pathhealth/BUILD.bazel
index 2bf91721d..96a0dc6a5 100644
--- a/gateway/pathhealth/BUILD.bazel
+++ b/gateway/pathhealth/BUILD.bazel
@@ -17,6 +17,7 @@ go_library(
         "//pkg/addr:go_default_library",
         "//pkg/log:go_default_library",
         "//pkg/metrics:go_default_library",
+        "//pkg/metrics/v2:go_default_library",
         "//pkg/private/common:go_default_library",
         "//pkg/private/ctrl/path_mgmt:go_default_library",
         "//pkg/private/serrors:go_default_library",
diff --git a/gateway/pathhealth/pathwatcher.go b/gateway/pathhealth/pathwatcher.go
index ac8befb02..a61ae80ca 100644
--- a/gateway/pathhealth/pathwatcher.go
+++ b/gateway/pathhealth/pathwatcher.go
@@ -25,6 +25,7 @@ import (
 	"github.com/scionproto/scion/pkg/addr"
 	"github.com/scionproto/scion/pkg/log"
 	"github.com/scionproto/scion/pkg/metrics"
+	metrics2 "github.com/scionproto/scion/pkg/metrics/v2"
 	"github.com/scionproto/scion/pkg/private/common"
 	"github.com/scionproto/scion/pkg/private/serrors"
 	"github.com/scionproto/scion/pkg/slayers/path/scion"
@@ -59,8 +60,9 @@ type DefaultPathWatcherFactory struct {
 	ProbesReceived func(remote addr.IA) metrics.Counter
 	// ProbesSendErrors keeps track of how many time sending probes failed per
 	// remote.
-	ProbesSendErrors       func(remote addr.IA) metrics.Counter
-	SCMPErrors             metrics.Counter
+	ProbesSendErrors func(remote addr.IA) metrics.Counter
+
+	SCMPErrors             metrics2.Counter
 	SCIONPacketConnMetrics snet.SCIONPacketConnMetrics
 }
 
diff --git a/pkg/log/BUILD.bazel b/pkg/log/BUILD.bazel
index cce1ad312..541611205 100644
--- a/pkg/log/BUILD.bazel
+++ b/pkg/log/BUILD.bazel
@@ -15,7 +15,7 @@ go_library(
     importpath = "github.com/scionproto/scion/pkg/log",
     visibility = ["//visibility:public"],
     deps = [
-        "//pkg/metrics:go_default_library",
+        "//pkg/metrics/v2:go_default_library",
         "//pkg/private/common:go_default_library",
         "//pkg/private/serrors:go_default_library",
         "//private/config:go_default_library",
diff --git a/pkg/log/options.go b/pkg/log/options.go
index 68be3cc12..4a4f63aec 100644
--- a/pkg/log/options.go
+++ b/pkg/log/options.go
@@ -18,7 +18,7 @@ import (
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
 
-	"github.com/scionproto/scion/pkg/metrics"
+	"github.com/scionproto/scion/pkg/metrics/v2"
 )
 
 type options struct {
diff --git a/pkg/metrics/v2/BUILD.bazel b/pkg/metrics/v2/BUILD.bazel
new file mode 100644
index 000000000..8c7495b6e
--- /dev/null
+++ b/pkg/metrics/v2/BUILD.bazel
@@ -0,0 +1,24 @@
+load("//tools/lint:go.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "fakes.go",
+        "metrics.go",
+    ],
+    importpath = "github.com/scionproto/scion/pkg/metrics/v2",
+    visibility = ["//visibility:public"],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = [
+        "fakes_test.go",
+        "metrics_test.go",
+    ],
+    deps = [
+        ":go_default_library",
+        "@com_github_prometheus_client_golang//prometheus:go_default_library",
+        "@com_github_stretchr_testify//assert:go_default_library",
+    ],
+)
diff --git a/pkg/metrics/v2/fakes.go b/pkg/metrics/v2/fakes.go
new file mode 100644
index 000000000..cd1ce2afc
--- /dev/null
+++ b/pkg/metrics/v2/fakes.go
@@ -0,0 +1,95 @@
+// Copyright 2020 Anapaya Systems
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics
+
+import (
+	"sync"
+)
+
+// node represents the shared implementation of gauges and counters.
+type node struct {
+	mtx sync.Mutex
+	v   float64
+}
+
+func (b *node) add(delta float64, canBeNegative bool) {
+	b.mtx.Lock()
+	defer b.mtx.Unlock()
+	if !canBeNegative && delta < 0 {
+		panic("counter increment value is < 0")
+	}
+	b.v += delta
+}
+
+func (b *node) set(v float64) {
+	b.mtx.Lock()
+	defer b.mtx.Unlock()
+	b.v = v
+}
+
+func (b *node) value() float64 {
+	b.mtx.Lock()
+	defer b.mtx.Unlock()
+	return b.v
+}
+
+// TestCounter implements a counter for use in tests.
+type TestCounter struct {
+	*node
+}
+
+// NewTestCounter creates a new counter for use in tests. See the examples for more information on
+// how to use this.
+func NewTestCounter() *TestCounter {
+	return &TestCounter{node: &node{}}
+}
+
+// Add increases the internal value of the counter by the specified delta. Value can be negative.
+func (c *TestCounter) Add(delta float64) {
+	c.add(delta, false)
+}
+
+// CounterValue extracts the value out of a TestCounter. If the argument is not a *TestCounter,
+// CounterValue will panic.
+func CounterValue(c Counter) float64 {
+	return c.(*TestCounter).value()
+}
+
+// TestGauge implements a gauge for use in tests.
+type TestGauge struct {
+	*node
+}
+
+// NewTestGauge creates a new gauge for use in tests. See the examples for more information on
+// how to use this.
+func NewTestGauge() *TestGauge {
+	return &TestGauge{node: &node{}}
+}
+
+// Set sets the internal value of the gauge to the specified value.
+func (g *TestGauge) Set(v float64) {
+	g.set(v)
+}
+
+// Add increases the internal value of the gauge by the specified delta. The delta must be positive.
+func (g *TestGauge) Add(delta float64) {
+	g.add(delta, true)
+}
+
+// GaugeValue extracts the value out of a TestGauge. If the argument is not a *TestGauge,
+// GaugeValue will panic.
+func GaugeValue(g Gauge) float64 {
+	return g.(*TestGauge).value()
+}
diff --git a/pkg/metrics/v2/metrics.go b/pkg/metrics/v2/metrics.go
new file mode 100644
index 000000000..00374b3bc
--- /dev/null
+++ b/pkg/metrics/v2/metrics.go
@@ -0,0 +1,134 @@
+// Copyright 2020 Anapaya Systems
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The MIT License (MIT)
+//
+// Copyright (c) 2015 Peter Bourgon
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+// Package metrics contains interfaces for generic metrics primitives, to
+// facilitate mocking metrics in unit tests.
+//
+// Most packages will want to use the types in this package, leaving the choice
+// of metric implementation (e.g., Prometheus) to the application main function.
+//
+// The types closely follow the Prometheus Go client library interfaces, for
+// easy plugging of Prometheus metrics.
+package metrics
+
+import (
+	"time"
+)
+
+// Counter describes a metric that accumulates values monotonically.
+// An example of a counter is the number of received HTTP requests.
+type Counter interface {
+	Add(delta float64)
+}
+
+// Gauge describes a metric that takes specific values over time.
+// An example of a gauge is the current depth of a job queue.
+type Gauge interface {
+	Set(value float64)
+	Add(delta float64)
+}
+
+// Histogram describes a metric that takes repeated observations of the same
+// kind of thing, and produces a statistical summary of those observations,
+// typically expressed as quantiles or buckets. An example of a histogram is
+// HTTP request latencies.
+type Histogram interface {
+	Observe(value float64)
+}
+
+// CounterAdd increases the passed in counter by the amount specified.
+// This is a no-op if c is nil.
+func CounterAdd(c Counter, delta float64) {
+	if c != nil {
+		c.Add(delta)
+	}
+}
+
+// CounterInc increases the passed in counter by 1.
+// This is a no-op if c is nil.
+func CounterInc(c Counter) {
+	CounterAdd(c, 1)
+}
+
+// GaugeSet sets the passed in gauge to the value specified.
+// This is a no-op if g is nil.
+func GaugeSet(g Gauge, value float64) {
+	if g != nil {
+		g.Set(value)
+	}
+}
+
+// GaugeSetTimestamp sets the passed gauge to the specified time stamp.
+// This is a no-op if g is nil.
+func GaugeSetTimestamp(g Gauge, ts time.Time) {
+	timestamp := func(ts time.Time) float64 {
+		if ts.IsZero() {
+			return 0
+		}
+		return float64(ts.UnixNano()) / 1e9
+	}
+	if g != nil {
+		g.Set(timestamp(ts))
+	}
+}
+
+// GaugeSetCurrentTime sets the passed gauge to the current time.
+// This is a no-op if g is nil.
+func GaugeSetCurrentTime(g Gauge) {
+	GaugeSetTimestamp(g, time.Now())
+}
+
+// GaugeAdd increases the passed in gauge by the amount specified.
+// This is a no-op if g is nil.
+func GaugeAdd(g Gauge, delta float64) {
+	if g != nil {
+		g.Add(delta)
+	}
+}
+
+// GaugeInc increases the passed in gauge by 1.
+// This is a no-op if g is nil.
+func GaugeInc(g Gauge) {
+	GaugeAdd(g, 1)
+}
+
+// HistogramObserve adds an observation to the histogram.
+// This is a no-op if h is nil.
+func HistogramObserve(h Histogram, value float64) {
+	if h != nil {
+		h.Observe(value)
+	}
+}
diff --git a/pkg/snet/BUILD.bazel b/pkg/snet/BUILD.bazel
index d9f7e410e..568cc98d7 100644
--- a/pkg/snet/BUILD.bazel
+++ b/pkg/snet/BUILD.bazel
@@ -22,7 +22,7 @@ go_library(
     deps = [
         "//pkg/addr:go_default_library",
         "//pkg/log:go_default_library",
-        "//pkg/metrics:go_default_library",
+        "//pkg/metrics/v2:go_default_library",
         "//pkg/private/common:go_default_library",
         "//pkg/private/ctrl/path_mgmt:go_default_library",
         "//pkg/private/serrors:go_default_library",
diff --git a/pkg/snet/metrics/BUILD.bazel b/pkg/snet/metrics/BUILD.bazel
index b910aecad..dcb34ed68 100644
--- a/pkg/snet/metrics/BUILD.bazel
+++ b/pkg/snet/metrics/BUILD.bazel
@@ -6,7 +6,7 @@ go_library(
     importpath = "github.com/scionproto/scion/pkg/snet/metrics",
     visibility = ["//visibility:public"],
     deps = [
-        "//pkg/metrics:go_default_library",
+        "//pkg/metrics/v2:go_default_library",
         "//pkg/snet:go_default_library",
         "@com_github_prometheus_client_golang//prometheus:go_default_library",
         "@com_github_prometheus_client_golang//prometheus/promauto:go_default_library",
diff --git a/pkg/snet/metrics/metrics.go b/pkg/snet/metrics/metrics.go
index 0498703bf..b8e564b02 100644
--- a/pkg/snet/metrics/metrics.go
+++ b/pkg/snet/metrics/metrics.go
@@ -20,7 +20,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
 
-	"github.com/scionproto/scion/pkg/metrics"
+	"github.com/scionproto/scion/pkg/metrics/v2"
 	"github.com/scionproto/scion/pkg/snet"
 )
 
@@ -50,12 +50,12 @@ func NewSCIONNetworkMetrics(opts ...Option) snet.SCIONNetworkMetrics {
 	auto := promauto.With(o.registry)
 
 	return snet.SCIONNetworkMetrics{
-		Dials: metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+		Dials: auto.NewCounter(prometheus.CounterOpts{
 			Name: "lib_snet_dials_total",
-			Help: "Total number of Dial calls."}, []string{})),
-		Listens: metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+			Help: "Total number of Dial calls."}),
+		Listens: auto.NewCounter(prometheus.CounterOpts{
 			Name: "lib_snet_listens_total",
-			Help: "Total number of Listen calls."}, []string{})),
+			Help: "Total number of Listen calls."}),
 	}
 }
 
@@ -63,27 +63,27 @@ func NewSCIONPacketConnMetrics(opts ...Option) snet.SCIONPacketConnMetrics {
 	o := apply(opts)
 	auto := promauto.With(o.registry)
 	return snet.SCIONPacketConnMetrics{
-		Closes: metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+		Closes: auto.NewCounter(prometheus.CounterOpts{
 			Name: "lib_snet_closes_total",
-			Help: "Total number of Close calls."}, []string{})),
-		ReadBytes: metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+			Help: "Total number of Close calls."}),
+		ReadBytes: auto.NewCounter(prometheus.CounterOpts{
 			Name: "lib_snet_read_total_bytes",
-			Help: "Total number of bytes read"}, []string{})),
-		ReadPackets: metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+			Help: "Total number of bytes read"}),
+		ReadPackets: auto.NewCounter(prometheus.CounterOpts{
 			Name: "lib_snet_read_total_pkts",
-			Help: "Total number of packetes read"}, []string{})),
-		WriteBytes: metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+			Help: "Total number of packetes read"}),
+		WriteBytes: auto.NewCounter(prometheus.CounterOpts{
 			Name: "lib_snet_write_total_bytes",
-			Help: "Total number of bytes written"}, []string{})),
-		WritePackets: metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+			Help: "Total number of bytes written"}),
+		WritePackets: auto.NewCounter(prometheus.CounterOpts{
 			Name: "lib_snet_write_total_pkts",
-			Help: "Total number of packets written"}, []string{})),
-		UnderlayConnectionErrors: metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+			Help: "Total number of packets written"}),
+		UnderlayConnectionErrors: auto.NewCounter(prometheus.CounterOpts{
 			Name: "lib_snet_underlay_error_total",
-			Help: "Total number of underlay connection errors"}, []string{})),
-		ParseErrors: metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+			Help: "Total number of underlay connection errors"}),
+		ParseErrors: auto.NewCounter(prometheus.CounterOpts{
 			Name: "lib_snet_parse_error_total",
-			Help: "Total number of parse errors"}, []string{})),
+			Help: "Total number of parse errors"}),
 		SCMPErrors: NewSCMPErrors(opts...),
 	}
 }
@@ -92,7 +92,7 @@ func NewSCMPErrors(opts ...Option) metrics.Counter {
 	o := apply(opts)
 	auto := promauto.With(o.registry)
 
-	return metrics.NewPromCounter(auto.NewCounterVec(prometheus.CounterOpts{
+	return auto.NewCounter(prometheus.CounterOpts{
 		Name: "lib_snet_scmp_error_total",
-		Help: "Total number of SCMP errors"}, []string{}))
+		Help: "Total number of SCMP errors"})
 }
diff --git a/pkg/snet/packet_conn.go b/pkg/snet/packet_conn.go
index 4c2f57777..8e545f73c 100644
--- a/pkg/snet/packet_conn.go
+++ b/pkg/snet/packet_conn.go
@@ -21,7 +21,7 @@ import (
 	"time"
 
 	"github.com/scionproto/scion/pkg/addr"
-	"github.com/scionproto/scion/pkg/metrics"
+	"github.com/scionproto/scion/pkg/metrics/v2"
 	"github.com/scionproto/scion/pkg/private/common"
 	"github.com/scionproto/scion/pkg/private/serrors"
 	"github.com/scionproto/scion/pkg/slayers"
diff --git a/pkg/snet/scmp.go b/pkg/snet/scmp.go
index 45f1940c4..ddbbf3826 100644
--- a/pkg/snet/scmp.go
+++ b/pkg/snet/scmp.go
@@ -19,7 +19,7 @@ import (
 	"time"
 
 	"github.com/scionproto/scion/pkg/log"
-	"github.com/scionproto/scion/pkg/metrics"
+	"github.com/scionproto/scion/pkg/metrics/v2"
 	"github.com/scionproto/scion/pkg/private/common"
 	"github.com/scionproto/scion/pkg/private/ctrl/path_mgmt"
 	"github.com/scionproto/scion/pkg/private/serrors"
diff --git a/pkg/snet/snet.go b/pkg/snet/snet.go
index e74235ecf..2abe8d61f 100644
--- a/pkg/snet/snet.go
+++ b/pkg/snet/snet.go
@@ -45,7 +45,7 @@ import (
 
 	"github.com/scionproto/scion/pkg/addr"
 	"github.com/scionproto/scion/pkg/log"
-	"github.com/scionproto/scion/pkg/metrics"
+	"github.com/scionproto/scion/pkg/metrics/v2"
 	"github.com/scionproto/scion/pkg/private/serrors"
 )
 
diff --git a/private/app/launcher/BUILD.bazel b/private/app/launcher/BUILD.bazel
index 5b4d3d730..659cdf3eb 100644
--- a/private/app/launcher/BUILD.bazel
+++ b/private/app/launcher/BUILD.bazel
@@ -10,7 +10,6 @@ go_library(
     visibility = ["//visibility:public"],
     deps = [
         "//pkg/log:go_default_library",
-        "//pkg/metrics:go_default_library",
         "//pkg/private/prom:go_default_library",
         "//pkg/private/serrors:go_default_library",
         "//private/app:go_default_library",
diff --git a/private/app/launcher/launcher.go b/private/app/launcher/launcher.go
index 5a2aedb8f..6e9fd85e2 100644
--- a/private/app/launcher/launcher.go
+++ b/private/app/launcher/launcher.go
@@ -32,7 +32,6 @@ import (
 	"github.com/spf13/viper"
 
 	"github.com/scionproto/scion/pkg/log"
-	"github.com/scionproto/scion/pkg/metrics"
 	"github.com/scionproto/scion/pkg/private/prom"
 	"github.com/scionproto/scion/pkg/private/serrors"
 	"github.com/scionproto/scion/private/app"
@@ -174,7 +173,7 @@ func (a *Application) executeCommand(ctx context.Context, shortName string) erro
 	}
 	a.TOMLConfig.InitDefaults()
 
-	logEntriesTotal := metrics.NewPromCounterFrom(
+	logEntriesTotal := prometheus.NewCounterVec(
 		prometheus.CounterOpts{
 			Name: "lib_log_emitted_entries_total",
 			Help: "Total number of log entries emitted.",
@@ -182,9 +181,9 @@ func (a *Application) executeCommand(ctx context.Context, shortName string) erro
 		[]string{"level"},
 	)
 	opt := log.WithEntriesCounter(log.EntriesCounter{
-		Debug: logEntriesTotal.With("level", "debug"),
-		Info:  logEntriesTotal.With("level", "info"),
-		Error: logEntriesTotal.With("level", "error"),
+		Debug: logEntriesTotal.With(prometheus.Labels{"level": "debug"}),
+		Info:  logEntriesTotal.With(prometheus.Labels{"level": "info"}),
+		Error: logEntriesTotal.With(prometheus.Labels{"level": "error"}),
 	})
 
 	if err := log.Setup(a.getLogging(), opt); err != nil {
diff --git a/private/periodic/BUILD.bazel b/private/periodic/BUILD.bazel
index 4c7a0ac1c..f913fe631 100644
--- a/private/periodic/BUILD.bazel
+++ b/private/periodic/BUILD.bazel
@@ -2,14 +2,18 @@ load("//tools/lint:go.bzl", "go_library", "go_test")
 
 go_library(
     name = "go_default_library",
-    srcs = ["periodic.go"],
+    srcs = [
+        "legacy_metrics.go",
+        "periodic.go",
+    ],
     importpath = "github.com/scionproto/scion/private/periodic",
     visibility = ["//visibility:public"],
     deps = [
         "//pkg/log:go_default_library",
-        "//pkg/metrics:go_default_library",
-        "//private/periodic/internal/metrics:go_default_library",
+        "//pkg/metrics/v2:go_default_library",
+        "@com_github_iancoleman_strcase//:go_default_library",
         "@com_github_opentracing_opentracing_go//:go_default_library",
+        "@com_github_prometheus_client_golang//prometheus:go_default_library",
     ],
 )
 
@@ -18,7 +22,7 @@ go_test(
     srcs = ["periodic_test.go"],
     deps = [
         ":go_default_library",
-        "//pkg/metrics:go_default_library",
+        "//pkg/metrics/v2:go_default_library",
         "//pkg/private/xtest:go_default_library",
         "@com_github_stretchr_testify//assert:go_default_library",
     ],
diff --git a/private/periodic/internal/metrics/BUILD.bazel b/private/periodic/internal/metrics/BUILD.bazel
deleted file mode 100644
index e8affe93a..000000000
--- a/private/periodic/internal/metrics/BUILD.bazel
+++ /dev/null
@@ -1,13 +0,0 @@
-load("//tools/lint:go.bzl", "go_library")
-
-go_library(
-    name = "go_default_library",
-    srcs = ["metrics.go"],
-    importpath = "github.com/scionproto/scion/private/periodic/internal/metrics",
-    visibility = ["//visibility:public"],
-    deps = [
-        "//pkg/metrics:go_default_library",
-        "@com_github_iancoleman_strcase//:go_default_library",
-        "@com_github_prometheus_client_golang//prometheus:go_default_library",
-    ],
-)
diff --git a/private/periodic/internal/metrics/metrics.go b/private/periodic/legacy_metrics.go
similarity index 58%
rename from private/periodic/internal/metrics/metrics.go
rename to private/periodic/legacy_metrics.go
index 7f89aa9d8..7c0c33184 100644
--- a/private/periodic/internal/metrics/metrics.go
+++ b/private/periodic/legacy_metrics.go
@@ -1,4 +1,4 @@
-// Copyright 2019 Anapaya Systems
+// Copyright 2024 Anapaya Systems
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,30 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package metrics
+package periodic
 
 import (
 	"strings"
 
 	"github.com/iancoleman/strcase"
 	"github.com/prometheus/client_golang/prometheus"
-
-	"github.com/scionproto/scion/pkg/metrics"
 )
 
-// Metrics is the standard metrics used in periodic.Runner
-
-// Deprecated: Metrics is used only in the deprecated function periodic.Start
-// which exists only for compatibility reasons. Use periodic.StartWithMetrics
-// along with periodic.Metrics instead.
-type Metrics struct {
-	Events    func(string) metrics.Counter
-	Runtime   metrics.Gauge
-	Timestamp metrics.Gauge
-	Period    metrics.Gauge
-}
-
-func NewMetric(prefix string) Metrics {
+// newLegacyMetrics creates the metrics for the deprecated Start function. It
+// uses the same old metrics as prior to the introduction of the
+// StartWithMetrics function.
+func newLegacyMetrics(prefix string) Metrics {
 	namespace := strcase.ToSnake(strings.Replace(prefix, ".", "_", -1))
 	subsystem := "periodic"
 
@@ -48,38 +37,32 @@ func NewMetric(prefix string) Metrics {
 		[]string{"event_type"},
 	)
 
-	runtime := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+	runtime := prometheus.NewGauge(prometheus.GaugeOpts{
 		Namespace: namespace,
 		Subsystem: subsystem,
 		Name:      "runtime_duration_seconds_total",
 		Help:      "Total time spend on every periodic run.",
-	},
-		[]string{},
-	)
+	})
 
-	timestamp := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+	timestamp := prometheus.NewGauge(prometheus.GaugeOpts{
 		Namespace: namespace,
 		Subsystem: subsystem,
 		Name:      "runtime_timestamp_seconds",
 		Help:      "The unix timestamp when the periodic run started.",
-	},
-		[]string{},
-	)
-	period := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+	})
+	period := prometheus.NewGauge(prometheus.GaugeOpts{
 		Namespace: namespace,
 		Subsystem: subsystem,
 		Name:      "period_duration_seconds",
 		Help:      "The period of this job.",
-	},
-		[]string{},
-	)
+	})
 
 	return Metrics{
-		Events: func(s string) metrics.Counter {
-			return metrics.NewPromCounter(events).With("event_type", s)
-		},
-		Runtime:   metrics.NewPromGauge(runtime),
-		Timestamp: metrics.NewPromGauge(timestamp),
-		Period:    metrics.NewPromGauge(period),
+		StopEvents:    events.With(prometheus.Labels{"event_type": "stop"}),
+		KillEvents:    events.With(prometheus.Labels{"event_type": "kill"}),
+		TriggerEvents: events.With(prometheus.Labels{"event_type": "trigger"}),
+		Runtime:       runtime,
+		StartTime:     timestamp,
+		Period:        period,
 	}
 }
diff --git a/private/periodic/periodic.go b/private/periodic/periodic.go
index 4a4530152..52a1ca368 100644
--- a/private/periodic/periodic.go
+++ b/private/periodic/periodic.go
@@ -21,8 +21,7 @@ import (
 	"github.com/opentracing/opentracing-go"
 
 	"github.com/scionproto/scion/pkg/log"
-	"github.com/scionproto/scion/pkg/metrics"
-	legacymetrics "github.com/scionproto/scion/private/periodic/internal/metrics"
+	"github.com/scionproto/scion/pkg/metrics/v2"
 )
 
 // A Task that has to be periodically executed.
@@ -46,8 +45,12 @@ const (
 
 // Metrics contains the relevant metrics for a periodic task.
 type Metrics struct {
-	// Events tracks the amount of occurrences of Events defined above.
-	Events func(string) metrics.Counter
+	// StopEvents tracks the amount of stop events.
+	StopEvents metrics.Counter
+	// StopEvents tracks the amount of kill events.
+	KillEvents metrics.Counter
+	// TriggerEvents tracks the amount of trigger events.
+	TriggerEvents metrics.Counter
 	// Period is a Gauge describing the current Period.
 	Period metrics.Gauge
 	// Runtime tracks how long the task has been running.
@@ -68,10 +71,6 @@ func (m *Metrics) setRuntime(d time.Duration) {
 	metrics.GaugeAdd(m.Runtime, float64(d)/1e9)
 }
 
-func (m *Metrics) event(s string) {
-	metrics.CounterAdd(m.Events(s), 1)
-}
-
 // Func implements the Task interface.
 type Func struct {
 	// Task is the function that is executed on Run.
@@ -110,13 +109,7 @@ type Runner struct {
 //
 // Deprecated: Start exists for compatibility reasons, use StartWithMetrics instead.
 func Start(task Task, period, timeout time.Duration) *Runner {
-	genMetric := legacymetrics.NewMetric(task.Name())
-	metric := Metrics{
-		Events:    genMetric.Events,
-		Period:    genMetric.Period,
-		Runtime:   genMetric.Runtime,
-		StartTime: genMetric.Timestamp,
-	}
+	metric := newLegacyMetrics(task.Name())
 	return StartWithMetrics(task, &metric, period, timeout)
 }
 
@@ -153,7 +146,7 @@ func (r *Runner) Stop() {
 	r.ticker.Stop()
 	close(r.stop)
 	<-r.loopFinished
-	r.metric.event(EventStop)
+	metrics.CounterInc(r.metric.StopEvents)
 }
 
 // Kill is like stop but it also cancels the context of the current running method.
@@ -165,7 +158,7 @@ func (r *Runner) Kill() {
 	close(r.stop)
 	r.cancelF()
 	<-r.loopFinished
-	r.metric.event(EventKill)
+	metrics.CounterInc(r.metric.KillEvents)
 }
 
 // TriggerRun triggers the periodic task to run now.
@@ -181,7 +174,7 @@ func (r *Runner) TriggerRun() {
 	case <-r.stop:
 	case r.trigger <- struct{}{}:
 	}
-	r.metric.event(EventTrigger)
+	metrics.CounterInc(r.metric.TriggerEvents)
 }
 
 func (r *Runner) runLoop() {


Test patch:
diff --git a/pkg/metrics/v2/fakes_test.go b/pkg/metrics/v2/fakes_test.go
new file mode 100644
index 000000000..64c187803
--- /dev/null
+++ b/pkg/metrics/v2/fakes_test.go
@@ -0,0 +1,183 @@
+// Copyright 2020 Anapaya Systems
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/scionproto/scion/pkg/metrics/v2"
+)
+
+func TestTestCounterAdd(t *testing.T) {
+	c := metrics.NewTestCounter()
+
+	assert.Equal(t, float64(0), metrics.CounterValue(c))
+
+	c.Add(2)
+	assert.Equal(t, float64(2), metrics.CounterValue(c))
+
+	c.Add(4)
+	assert.Equal(t, float64(6), metrics.CounterValue(c))
+}
+
+func ExampleTestCounter_simple() {
+	// This example shows how to write a simple test using a TestCounter.
+	type Server struct {
+		RequestsHandled metrics.Counter
+	}
+
+	Run := func(s *Server) {
+		// server logic
+		s.RequestsHandled.Add(1)
+		s.RequestsHandled.Add(1)
+	}
+
+	c := metrics.NewTestCounter()
+
+	s := &Server{
+		RequestsHandled: c,
+	}
+	Run(s)
+
+	// Check metrics
+	fmt.Println(metrics.CounterValue(c) == 2)
+	// Output:
+	// true
+}
+
+func ExampleTestCounter_labels() {
+	// This example shows how to write a test with labels using a TestCounter.
+	type Server struct {
+		RequestsHandledNormal metrics.Counter
+		RequestsHandledError  metrics.Counter
+		RequestsHandledAuth   metrics.Counter
+	}
+
+	Run := func(s *Server) {
+		// server logic
+		s.RequestsHandledNormal.Add(1)
+		s.RequestsHandledNormal.Add(1)
+		s.RequestsHandledAuth.Add(1)
+	}
+
+	s := &Server{
+		RequestsHandledNormal: metrics.NewTestCounter(),
+		RequestsHandledError:  metrics.NewTestCounter(),
+		RequestsHandledAuth:   metrics.NewTestCounter(),
+	}
+	Run(s)
+
+	// Check metrics
+	fmt.Println(metrics.CounterValue(s.RequestsHandledNormal) == 2)
+	fmt.Println(metrics.CounterValue(s.RequestsHandledAuth) == 1)
+	fmt.Println(metrics.CounterValue(s.RequestsHandledError) == 0)
+	// Output:
+	// true
+	// true
+	// true
+}
+
+func TestTestGaugeSet(t *testing.T) {
+	g := metrics.NewTestGauge()
+
+	assert.Equal(t, float64(0), metrics.GaugeValue(g))
+
+	g.Set(2)
+	assert.Equal(t, float64(2), metrics.GaugeValue(g))
+
+	g.Add(4)
+	assert.Equal(t, float64(6), metrics.GaugeValue(g))
+}
+
+func TestTestGaugeAdd(t *testing.T) {
+	g := metrics.NewTestGauge()
+
+	assert.Equal(t, float64(0), metrics.GaugeValue(g))
+
+	g.Add(2)
+	assert.Equal(t, float64(2), metrics.GaugeValue(g))
+
+	g.Add(-4)
+	assert.Equal(t, float64(-2), metrics.GaugeValue(g))
+}
+
+func ExampleTestGauge_simple() {
+	// This example shows how to write a simple test using a TestGauge.
+	type Server struct {
+		RequestsHandled metrics.Gauge
+	}
+
+	Run := func(s *Server) {
+		// server logic
+		s.RequestsHandled.Set(6)
+	}
+
+	g := metrics.NewTestGauge()
+
+	s := &Server{
+		RequestsHandled: g,
+	}
+	Run(s)
+
+	// Check metrics
+	fmt.Println(metrics.GaugeValue(g) == 6)
+	// Output:
+	// true
+}
+
+func ExampleTestGauge_labels() {
+	// For actual metrics initialization, labels would be fixed on construction.
+	var (
+		httpGauge  = metrics.NewTestGauge()
+		httpsGauge = metrics.NewTestGauge()
+		otherGauge = metrics.NewTestGauge()
+	)
+	// This example shows how to write a test with labels using a TestGauge.
+	type Server struct {
+		RunningWorkers func(typ string) metrics.Gauge
+	}
+
+	Run := func(s *Server) {
+		// server logic
+		s.RunningWorkers("http").Set(8)
+		s.RunningWorkers("https").Set(5)
+	}
+
+	s := &Server{
+		RunningWorkers: func(typ string) metrics.Gauge {
+			switch typ {
+			case "http":
+				return httpGauge
+			case "https":
+				return httpsGauge
+			default:
+				return otherGauge
+			}
+		},
+	}
+	Run(s)
+
+	// Check metrics
+	fmt.Println(metrics.GaugeValue(httpGauge) == 8)
+	fmt.Println(metrics.GaugeValue(httpsGauge) == 5)
+	fmt.Println(metrics.GaugeValue(otherGauge) == 0)
+	// Output:
+	// true
+	// true
+	// true
+}
diff --git a/pkg/metrics/v2/metrics_test.go b/pkg/metrics/v2/metrics_test.go
new file mode 100644
index 000000000..3dfe67e0a
--- /dev/null
+++ b/pkg/metrics/v2/metrics_test.go
@@ -0,0 +1,51 @@
+// Copyright 2020 Anapaya Systems
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics_test
+
+import (
+	"github.com/prometheus/client_golang/prometheus"
+
+	"github.com/scionproto/scion/pkg/metrics/v2"
+)
+
+func ExampleCounter_implementation() {
+	// LITERALINCLUDE ExampleCounter_Implementation START
+	type Giant struct {
+		MagicBeansEaten metrics.Counter
+		RedPillsEaten   metrics.Counter
+		BluePillsEaten  metrics.Counter
+	}
+
+	counter := prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "magic_beans_eaten_total",
+		Help: "Number of magic beans eaten.",
+	})
+	pillCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "pills_eaten_total",
+		Help: "Number of pills eaten.",
+	}, []string{"color"})
+
+	giant := Giant{
+		MagicBeansEaten: counter,
+		RedPillsEaten:   pillCounter.WithLabelValues("red"),
+		BluePillsEaten: pillCounter.With(prometheus.Labels{
+			"color": "blue",
+		}),
+	}
+	giant.MagicBeansEaten.Add(4)
+	giant.RedPillsEaten.Add(2)
+	giant.BluePillsEaten.Add(1)
+	// LITERALINCLUDE ExampleCounter_Implementation END
+}
diff --git a/private/periodic/periodic_test.go b/private/periodic/periodic_test.go
index 524fe449d..2b2c16569 100644
--- a/private/periodic/periodic_test.go
+++ b/private/periodic/periodic_test.go
@@ -22,7 +22,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 
-	"github.com/scionproto/scion/pkg/metrics"
+	"github.com/scionproto/scion/pkg/metrics/v2"
 	"github.com/scionproto/scion/pkg/private/xtest"
 	"github.com/scionproto/scion/private/periodic"
 )
@@ -38,11 +38,10 @@ func (tf taskFunc) Name() string {
 }
 
 func TestPeriodicExecution(t *testing.T) {
-	events := metrics.NewTestCounter()
 	m := periodic.Metrics{
-		Events: func(s string) metrics.Counter {
-			return events.With("event_type", s)
-		},
+		StopEvents:    metrics.NewTestCounter(),
+		KillEvents:    metrics.NewTestCounter(),
+		TriggerEvents: metrics.NewTestCounter(),
 		// Without additional metrics
 	}
 
@@ -78,17 +77,16 @@ func TestPeriodicExecution(t *testing.T) {
 	err := runWithTimeout(r.Stop, 2*time.Second)
 	assert.NoError(t, err, "r.Stop() action timed out")
 	// Check that  metrics work as expected
-	assert.Equal(t, float64(1), metrics.CounterValue(m.Events(periodic.EventStop)))
-	assert.Equal(t, float64(0), metrics.CounterValue(m.Events(periodic.EventKill)))
-	assert.Equal(t, float64(0), metrics.CounterValue(m.Events(periodic.EventTrigger)))
+	assert.Equal(t, float64(1), metrics.CounterValue(m.StopEvents))
+	assert.Equal(t, float64(0), metrics.CounterValue(m.KillEvents))
+	assert.Equal(t, float64(0), metrics.CounterValue(m.TriggerEvents))
 }
 
 func TestKillExitsLongRunningFunc(t *testing.T) {
-	events := metrics.NewTestCounter()
 	m := periodic.Metrics{
-		Events: func(s string) metrics.Counter {
-			return events.With("event_type", s)
-		},
+		StopEvents:    metrics.NewTestCounter(),
+		KillEvents:    metrics.NewTestCounter(),
+		TriggerEvents: metrics.NewTestCounter(),
 		// Without additional metrics
 	}
 	done, errChan := make(chan struct{}), make(chan error, 1)
@@ -115,17 +113,16 @@ func TestKillExitsLongRunningFunc(t *testing.T) {
 		t.Fatalf("time out while waiting on err")
 	}
 	// Check that  metrics work as expected
-	assert.Equal(t, float64(0), metrics.CounterValue(m.Events(periodic.EventStop)))
-	assert.Equal(t, float64(1), metrics.CounterValue(m.Events(periodic.EventKill)))
-	assert.Equal(t, float64(0), metrics.CounterValue(m.Events(periodic.EventTrigger)))
+	assert.Equal(t, float64(0), metrics.CounterValue(m.StopEvents))
+	assert.Equal(t, float64(1), metrics.CounterValue(m.KillEvents))
+	assert.Equal(t, float64(0), metrics.CounterValue(m.TriggerEvents))
 }
 
 func TestTaskDoesNotRunAfterKill(t *testing.T) {
-	events := metrics.NewTestCounter()
 	m := periodic.Metrics{
-		Events: func(s string) metrics.Counter {
-			return events.With("event_type", s)
-		},
+		StopEvents:    metrics.NewTestCounter(),
+		KillEvents:    metrics.NewTestCounter(),
+		TriggerEvents: metrics.NewTestCounter(),
 		// With additional metrics
 		Period:    metrics.NewTestGauge(),
 		Runtime:   metrics.NewTestGauge(),
@@ -156,9 +153,9 @@ func TestTaskDoesNotRunAfterKill(t *testing.T) {
 	xtest.AssertReadReturnsBefore(t, done, time.Second)
 	assert.Equal(t, len(cnt), 0, "No other run within a period")
 	// Check that  metrics work as expected
-	assert.Equal(t, float64(0), metrics.CounterValue(m.Events(periodic.EventStop)))
-	assert.Equal(t, float64(1), metrics.CounterValue(m.Events(periodic.EventKill)))
-	assert.Equal(t, float64(0), metrics.CounterValue(m.Events(periodic.EventTrigger)))
+	assert.Equal(t, float64(0), metrics.CounterValue(m.StopEvents))
+	assert.Equal(t, float64(1), metrics.CounterValue(m.KillEvents))
+	assert.Equal(t, float64(0), metrics.CounterValue(m.TriggerEvents))
 
 	assert.Equal(t, p.Seconds(), metrics.GaugeValue(m.Period))
 
@@ -175,11 +172,10 @@ func TestTaskDoesNotRunAfterKill(t *testing.T) {
 }
 
 func TestTriggerNow(t *testing.T) {
-	events := metrics.NewTestCounter()
 	m := periodic.Metrics{
-		Events: func(s string) metrics.Counter {
-			return events.With("event_type", s)
-		},
+		StopEvents:    metrics.NewTestCounter(),
+		KillEvents:    metrics.NewTestCounter(),
+		TriggerEvents: metrics.NewTestCounter(),
 		// With additional metrics
 		Period:    metrics.NewTestGauge(),
 		Runtime:   metrics.NewTestGauge(),
@@ -213,13 +209,9 @@ func TestTriggerNow(t *testing.T) {
 	xtest.AssertReadReturnsBefore(t, done, time.Second)
 	assert.GreaterOrEqual(t, len(cnt), want-1, "Must run %v times within short time", want-1)
 	// Check that  metrics work as expected
-	assert.Equal(t, float64(0), metrics.CounterValue(m.Events(periodic.EventStop)))
-	assert.Equal(t, float64(0), metrics.CounterValue(m.Events(periodic.EventKill)))
-	assert.Equal(
-		t,
-		float64(want),
-		metrics.CounterValue(m.Events(periodic.EventTrigger)),
-	)
+	assert.Equal(t, float64(0), metrics.CounterValue(m.StopEvents))
+	assert.Equal(t, float64(0), metrics.CounterValue(m.KillEvents))
+	assert.Equal(t, float64(want), metrics.CounterValue(m.TriggerEvents))
 
 	assert.Equal(t, p.Seconds(), metrics.GaugeValue(m.Period))