Skip to content

Commit

Permalink
r1
Browse files Browse the repository at this point in the history
  • Loading branch information
lukedirtwalker committed Jun 18, 2024
1 parent 1f90ab1 commit f929a78
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 311 deletions.
23 changes: 7 additions & 16 deletions doc/dev/style/go.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion pkg/metrics/v2/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ go_library(
name = "go_default_library",
srcs = [
"fakes.go",
"helper.go",
"metrics.go",
],
importpath = "github.com/scionproto/scion/pkg/metrics/v2",
Expand Down
126 changes: 2 additions & 124 deletions pkg/metrics/v2/fakes.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,87 +15,13 @@
package metrics

import (
"fmt"
"sort"
"strings"
"sync"
)

// node represents the shared implementation of gauges and counters. The label namespace of each
// new counter or gauge is modeled by a hierarchy of nodes that is organized as a tree with two
// levels.
// node represents the shared implementation of gauges and counters.
type node struct {
mtx sync.Mutex

// root points to the manually created node from which this node was created. Root nodes will
// point to themselves, while children created by calling With will point to the root of their
// hierarchy.
root *node
// children maps canonicalized label values to child nodes. Only root (that is,
// manually created by the caller) node objects initialize this field. This maintains all
// child nodes that can be traced back to the same root node.
children map[string]*node

// labels maintains the label context (key, value pairs) for this entry. The root node
// starts with an empty label set, but child nodes will always have label data. Children
// of the children will inherit and add to the label sets.
labels map[string]string
v float64
}

func (b *node) with(labels ...string) *node {
b.mtx.Lock()
defer b.mtx.Unlock()

if len(labels)%2 != 0 {
panic("number of labels is odd")
}
if b.children == nil {
b.children = make(map[string]*node)
}
if b.root == nil {
b.root = b
}

labelsMap := createLabelsMap(b.labels, labels)
return b.findCounter(labelsMap)
}

func createLabelsMap(existingLabels map[string]string, newLabels []string) map[string]string {
labelsMap := make(map[string]string)
for k, v := range existingLabels {
labelsMap[k] = v
}
for i := 0; i < len(newLabels)/2; i++ {
k, v := newLabels[2*i], newLabels[2*i+1]
if _, ok := labelsMap[k]; ok {
panic(fmt.Sprintf("duplicate label key: %s", k))
}
labelsMap[k] = v
}
return labelsMap
}

// findCounter returns an existing counter if it matches the labels, or creates a new one if one is
// not found.
func (b *node) findCounter(labelsMap map[string]string) *node {
// To ensure that reading and writing label data on the registry maintained by the root
// counter is safe for concurrent use, we acquire the lock if we're not root.
if b.root != b {
b.root.mtx.Lock()
defer b.root.mtx.Unlock()
}

canonicalLabels := canonicalize(labelsMap)
counter, ok := b.root.children[canonicalLabels]
if ok {
return counter
}
b.root.children[canonicalLabels] = &node{
labels: labelsMap,
root: b.root,
}
return b.root.children[canonicalLabels]
v float64
}

func (b *node) add(delta float64, canBeNegative bool) {
Expand All @@ -119,34 +45,7 @@ func (b *node) value() float64 {
return b.v
}

// canonicalize returns a canonical description of label keys and values.
//
// The format is obtained by sorting the label keys, joining them with their value, and then
// joining all the pairs together. For example, if label key "x" has value "1", and label key "y"
// has value "2", the canonical representation is "x=1.y=2". The canonical format is used by a
// root label namespace (e.g., TestCounter) to manage unique time-series.
func canonicalize(m map[string]string) string {
// This function is horribly inefficient, but it's only used for testing so we don't care.
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)

var keyValues []string
for _, k := range keys {
keyValues = append(keyValues, k+"="+m[k])
}

return strings.Join(keyValues, ".")
}

// TestCounter implements a counter for use in tests.
//
// Each newly created TestCounter is a stand-alone label namespace. That means time-series behave
// as expected, e.g., creating two counters with the same labels by calling With will yield counters
// that represent the same time-series. The examples illustrate how this can be used to write a
// simple test.
type TestCounter struct {
*node
}
Expand All @@ -162,26 +61,13 @@ func (c *TestCounter) Add(delta float64) {
c.add(delta, false)
}

// With creates a new counter that includes the specified labels in addition to any labels the
// parent counter might have.
func (c *TestCounter) With(labels ...string) Counter {
return &TestCounter{
node: c.with(labels...),
}
}

// 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.
//
// Each newly created TestGauge is a stand-alone label namespace. That means time-series behave
// as expected, e.g., creating two gauges with the same labels by calling With will yield gauges
// that represent the same time-series. The examples illustrate how this can be used to write a
// simple test.
type TestGauge struct {
*node
}
Expand All @@ -202,14 +88,6 @@ func (g *TestGauge) Add(delta float64) {
g.add(delta, true)
}

// With creates a new gauge that includes the specified labels in addition to any labels the
// parent gauge might have.
func (g *TestGauge) With(labels ...string) Gauge {
return &TestGauge{
node: g.with(labels...),
}
}

// GaugeValue extracts the value out of a TestGauge. If the argument is not a *TestGauge,
// GaugeValue will panic.
func GaugeValue(g Gauge) float64 {
Expand Down
Loading

0 comments on commit f929a78

Please sign in to comment.