summaryrefslogtreecommitdiffhomepage
path: root/pkg/metric
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/metric')
-rw-r--r--pkg/metric/BUILD32
-rw-r--r--pkg/metric/metric.go250
-rw-r--r--pkg/metric/metric.proto76
-rw-r--r--pkg/metric/metric_test.go258
4 files changed, 616 insertions, 0 deletions
diff --git a/pkg/metric/BUILD b/pkg/metric/BUILD
new file mode 100644
index 000000000..58305009d
--- /dev/null
+++ b/pkg/metric/BUILD
@@ -0,0 +1,32 @@
+load("//tools:defs.bzl", "go_library", "go_test", "proto_library")
+
+package(licenses = ["notice"])
+
+go_library(
+ name = "metric",
+ srcs = ["metric.go"],
+ visibility = ["//:sandbox"],
+ deps = [
+ ":metric_go_proto",
+ "//pkg/eventchannel",
+ "//pkg/log",
+ "//pkg/sync",
+ ],
+)
+
+proto_library(
+ name = "metric",
+ srcs = ["metric.proto"],
+ visibility = ["//:sandbox"],
+)
+
+go_test(
+ name = "metric_test",
+ srcs = ["metric_test.go"],
+ library = ":metric",
+ deps = [
+ ":metric_go_proto",
+ "//pkg/eventchannel",
+ "@com_github_golang_protobuf//proto:go_default_library",
+ ],
+)
diff --git a/pkg/metric/metric.go b/pkg/metric/metric.go
new file mode 100644
index 000000000..64aa365ce
--- /dev/null
+++ b/pkg/metric/metric.go
@@ -0,0 +1,250 @@
+// Copyright 2018 The gVisor Authors.
+//
+// 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 metric provides primitives for collecting metrics.
+package metric
+
+import (
+ "errors"
+ "fmt"
+ "sync/atomic"
+
+ "gvisor.dev/gvisor/pkg/eventchannel"
+ "gvisor.dev/gvisor/pkg/log"
+ pb "gvisor.dev/gvisor/pkg/metric/metric_go_proto"
+ "gvisor.dev/gvisor/pkg/sync"
+)
+
+var (
+ // ErrNameInUse indicates that another metric is already defined for
+ // the given name.
+ ErrNameInUse = errors.New("metric name already in use")
+
+ // ErrInitializationDone indicates that the caller tried to create a
+ // new metric after initialization.
+ ErrInitializationDone = errors.New("metric cannot be created after initialization is complete")
+)
+
+// Uint64Metric encapsulates a uint64 that represents some kind of metric to be
+// monitored.
+//
+// Metrics are not saved across save/restore and thus reset to zero on restore.
+//
+// TODO(b/67298427): Support metric fields.
+type Uint64Metric struct {
+ // value is the actual value of the metric. It must be accessed atomically.
+ value uint64
+}
+
+var (
+ // initialized indicates that all metrics are registered. allMetrics is
+ // immutable once initialized is true.
+ initialized bool
+
+ // allMetrics are the registered metrics.
+ allMetrics = makeMetricSet()
+)
+
+// Initialize sends a metric registration event over the event channel.
+//
+// Precondition:
+// * All metrics are registered.
+// * Initialize/Disable has not been called.
+func Initialize() {
+ if initialized {
+ panic("Initialize/Disable called more than once")
+ }
+ initialized = true
+
+ m := pb.MetricRegistration{}
+ for _, v := range allMetrics.m {
+ m.Metrics = append(m.Metrics, v.metadata)
+ }
+ eventchannel.Emit(&m)
+}
+
+// Disable sends an empty metric registration event over the event channel,
+// disabling metric collection.
+//
+// Precondition:
+// * All metrics are registered.
+// * Initialize/Disable has not been called.
+func Disable() {
+ if initialized {
+ panic("Initialize/Disable called more than once")
+ }
+ initialized = true
+
+ m := pb.MetricRegistration{}
+ if err := eventchannel.Emit(&m); err != nil {
+ panic("unable to emit metric disable event: " + err.Error())
+ }
+}
+
+type customUint64Metric struct {
+ // metadata describes the metric. It is immutable.
+ metadata *pb.MetricMetadata
+
+ // value returns the current value of the metric.
+ value func() uint64
+}
+
+// RegisterCustomUint64Metric registers a metric with the given name.
+//
+// Register must only be called at init and will return and error if called
+// after Initialized.
+//
+// Preconditions:
+// * name must be globally unique.
+// * Initialize/Disable have not been called.
+func RegisterCustomUint64Metric(name string, cumulative, sync bool, units pb.MetricMetadata_Units, description string, value func() uint64) error {
+ if initialized {
+ return ErrInitializationDone
+ }
+
+ if _, ok := allMetrics.m[name]; ok {
+ return ErrNameInUse
+ }
+
+ allMetrics.m[name] = customUint64Metric{
+ metadata: &pb.MetricMetadata{
+ Name: name,
+ Description: description,
+ Cumulative: cumulative,
+ Sync: sync,
+ Type: pb.MetricMetadata_TYPE_UINT64,
+ Units: units,
+ },
+ value: value,
+ }
+ return nil
+}
+
+// MustRegisterCustomUint64Metric calls RegisterCustomUint64Metric and panics
+// if it returns an error.
+func MustRegisterCustomUint64Metric(name string, cumulative, sync bool, description string, value func() uint64) {
+ if err := RegisterCustomUint64Metric(name, cumulative, sync, pb.MetricMetadata_UNITS_NONE, description, value); err != nil {
+ panic(fmt.Sprintf("Unable to register metric %q: %v", name, err))
+ }
+}
+
+// NewUint64Metric creates and registers a new cumulative metric with the given name.
+//
+// Metrics must be statically defined (i.e., at init).
+func NewUint64Metric(name string, sync bool, units pb.MetricMetadata_Units, description string) (*Uint64Metric, error) {
+ var m Uint64Metric
+ return &m, RegisterCustomUint64Metric(name, true /* cumulative */, sync, units, description, m.Value)
+}
+
+// MustCreateNewUint64Metric calls NewUint64Metric and panics if it returns an error.
+func MustCreateNewUint64Metric(name string, sync bool, description string) *Uint64Metric {
+ m, err := NewUint64Metric(name, sync, pb.MetricMetadata_UNITS_NONE, description)
+ if err != nil {
+ panic(fmt.Sprintf("Unable to create metric %q: %v", name, err))
+ }
+ return m
+}
+
+// MustCreateNewUint64NanosecondsMetric calls NewUint64Metric and panics if it returns an error.
+func MustCreateNewUint64NanosecondsMetric(name string, sync bool, description string) *Uint64Metric {
+ m, err := NewUint64Metric(name, sync, pb.MetricMetadata_UNITS_NANOSECONDS, description)
+ if err != nil {
+ panic(fmt.Sprintf("Unable to create metric %q: %v", name, err))
+ }
+ return m
+}
+
+// Value returns the current value of the metric.
+func (m *Uint64Metric) Value() uint64 {
+ return atomic.LoadUint64(&m.value)
+}
+
+// Increment increments the metric by 1.
+func (m *Uint64Metric) Increment() {
+ atomic.AddUint64(&m.value, 1)
+}
+
+// IncrementBy increments the metric by v.
+func (m *Uint64Metric) IncrementBy(v uint64) {
+ atomic.AddUint64(&m.value, v)
+}
+
+// metricSet holds named metrics.
+type metricSet struct {
+ m map[string]customUint64Metric
+}
+
+// makeMetricSet returns a new metricSet.
+func makeMetricSet() metricSet {
+ return metricSet{
+ m: make(map[string]customUint64Metric),
+ }
+}
+
+// Values returns a snapshot of all values in m.
+func (m *metricSet) Values() metricValues {
+ vals := make(metricValues)
+ for k, v := range m.m {
+ vals[k] = v.value()
+ }
+ return vals
+}
+
+// metricValues contains a copy of the values of all metrics.
+type metricValues map[string]uint64
+
+var (
+ // emitMu protects metricsAtLastEmit and ensures that all emitted
+ // metrics are strongly ordered (older metrics are never emitted after
+ // newer metrics).
+ emitMu sync.Mutex
+
+ // metricsAtLastEmit contains the state of the metrics at the last emit event.
+ metricsAtLastEmit metricValues
+)
+
+// EmitMetricUpdate emits a MetricUpdate over the event channel.
+//
+// Only metrics that have changed since the last call are emitted.
+//
+// EmitMetricUpdate is thread-safe.
+//
+// Preconditions:
+// * Initialize has been called.
+func EmitMetricUpdate() {
+ emitMu.Lock()
+ defer emitMu.Unlock()
+
+ snapshot := allMetrics.Values()
+
+ m := pb.MetricUpdate{}
+ for k, v := range snapshot {
+ // On the first call metricsAtLastEmit will be empty. Include
+ // all metrics then.
+ if prev, ok := metricsAtLastEmit[k]; !ok || prev != v {
+ m.Metrics = append(m.Metrics, &pb.MetricValue{
+ Name: k,
+ Value: &pb.MetricValue_Uint64Value{v},
+ })
+ }
+ }
+
+ metricsAtLastEmit = snapshot
+ if len(m.Metrics) == 0 {
+ return
+ }
+
+ log.Debugf("Emitting metrics: %v", &m)
+ eventchannel.Emit(&m)
+}
diff --git a/pkg/metric/metric.proto b/pkg/metric/metric.proto
new file mode 100644
index 000000000..3cc89047d
--- /dev/null
+++ b/pkg/metric/metric.proto
@@ -0,0 +1,76 @@
+// Copyright 2018 The gVisor Authors.
+//
+// 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.
+
+syntax = "proto3";
+
+package gvisor;
+
+// MetricMetadata contains all of the metadata describing a single metric.
+message MetricMetadata {
+ // name is the unique name of the metric, usually in a "directory" format
+ // (e.g., /foo/count).
+ string name = 1;
+
+ // description is a human-readable description of the metric.
+ string description = 2;
+
+ // cumulative indicates that this metric is never decremented.
+ bool cumulative = 3;
+
+ // sync indicates that values from the final metric event should be
+ // synchronized to the backing monitoring system at exit.
+ //
+ // If sync is false, values are only sent to the monitoring system
+ // periodically. There is no guarantee that values will ever be received by
+ // the monitoring system.
+ bool sync = 4;
+
+ enum Type { TYPE_UINT64 = 0; }
+
+ // type is the type of the metric value.
+ Type type = 5;
+
+ enum Units {
+ UNITS_NONE = 0;
+ UNITS_NANOSECONDS = 1;
+ }
+
+ // units is the units of the metric value.
+ Units units = 6;
+}
+
+// MetricRegistration contains the metadata for all metrics that will be in
+// future MetricUpdates.
+message MetricRegistration {
+ repeated MetricMetadata metrics = 1;
+}
+
+// MetricValue the value of a metric at a single point in time.
+message MetricValue {
+ // name is the unique name of the metric, as in MetricMetadata.
+ string name = 1;
+
+ // value is the value of the metric at a single point in time. The field set
+ // depends on the type of the metric.
+ oneof value {
+ uint64 uint64_value = 2;
+ }
+}
+
+// MetricUpdate contains new values for multiple distinct metrics.
+//
+// Metrics whose values have not changed are not included.
+message MetricUpdate {
+ repeated MetricValue metrics = 1;
+}
diff --git a/pkg/metric/metric_test.go b/pkg/metric/metric_test.go
new file mode 100644
index 000000000..c425ea532
--- /dev/null
+++ b/pkg/metric/metric_test.go
@@ -0,0 +1,258 @@
+// Copyright 2018 The gVisor Authors.
+//
+// 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 metric
+
+import (
+ "testing"
+
+ "github.com/golang/protobuf/proto"
+ "gvisor.dev/gvisor/pkg/eventchannel"
+ pb "gvisor.dev/gvisor/pkg/metric/metric_go_proto"
+)
+
+// sliceEmitter implements eventchannel.Emitter by appending all messages to a
+// slice.
+type sliceEmitter []proto.Message
+
+// Emit implements eventchannel.Emitter.Emit.
+func (s *sliceEmitter) Emit(msg proto.Message) (bool, error) {
+ *s = append(*s, msg)
+ return false, nil
+}
+
+// Emit implements eventchannel.Emitter.Close.
+func (s *sliceEmitter) Close() error {
+ return nil
+}
+
+// Reset clears all events in s.
+func (s *sliceEmitter) Reset() {
+ *s = nil
+}
+
+// emitter is the eventchannel.Emitter used for all tests. Package eventchannel
+// doesn't allow removing Emitters, so we must use one global emitter for all
+// test cases.
+var emitter sliceEmitter
+
+func init() {
+ eventchannel.AddEmitter(&emitter)
+}
+
+// reset clears all global state in the metric package.
+func reset() {
+ initialized = false
+ allMetrics = makeMetricSet()
+ emitter.Reset()
+}
+
+const (
+ fooDescription = "Foo!"
+ barDescription = "Bar Baz"
+)
+
+func TestInitialize(t *testing.T) {
+ defer reset()
+
+ _, err := NewUint64Metric("/foo", false, pb.MetricMetadata_UNITS_NONE, fooDescription)
+ if err != nil {
+ t.Fatalf("NewUint64Metric got err %v want nil", err)
+ }
+
+ _, err = NewUint64Metric("/bar", true, pb.MetricMetadata_UNITS_NANOSECONDS, barDescription)
+ if err != nil {
+ t.Fatalf("NewUint64Metric got err %v want nil", err)
+ }
+
+ Initialize()
+
+ if len(emitter) != 1 {
+ t.Fatalf("Initialize emitted %d events want 1", len(emitter))
+ }
+
+ mr, ok := emitter[0].(*pb.MetricRegistration)
+ if !ok {
+ t.Fatalf("emitter %v got %T want pb.MetricRegistration", emitter[0], emitter[0])
+ }
+
+ if len(mr.Metrics) != 2 {
+ t.Errorf("MetricRegistration got %d metrics want 2", len(mr.Metrics))
+ }
+
+ foundFoo := false
+ foundBar := false
+ for _, m := range mr.Metrics {
+ if m.Type != pb.MetricMetadata_TYPE_UINT64 {
+ t.Errorf("Metadata %+v Type got %v want %v", m, m.Type, pb.MetricMetadata_TYPE_UINT64)
+ }
+ if !m.Cumulative {
+ t.Errorf("Metadata %+v Cumulative got false want true", m)
+ }
+
+ switch m.Name {
+ case "/foo":
+ foundFoo = true
+ if m.Description != fooDescription {
+ t.Errorf("/foo %+v Description got %q want %q", m, m.Description, fooDescription)
+ }
+ if m.Sync {
+ t.Errorf("/foo %+v Sync got true want false", m)
+ }
+ if m.Units != pb.MetricMetadata_UNITS_NONE {
+ t.Errorf("/foo %+v Units got %v want %v", m, m.Units, pb.MetricMetadata_UNITS_NONE)
+ }
+ case "/bar":
+ foundBar = true
+ if m.Description != barDescription {
+ t.Errorf("/bar %+v Description got %q want %q", m, m.Description, barDescription)
+ }
+ if !m.Sync {
+ t.Errorf("/bar %+v Sync got true want false", m)
+ }
+ if m.Units != pb.MetricMetadata_UNITS_NANOSECONDS {
+ t.Errorf("/bar %+v Units got %v want %v", m, m.Units, pb.MetricMetadata_UNITS_NANOSECONDS)
+ }
+ }
+ }
+
+ if !foundFoo {
+ t.Errorf("/foo not found: %+v", emitter)
+ }
+ if !foundBar {
+ t.Errorf("/bar not found: %+v", emitter)
+ }
+}
+
+func TestDisable(t *testing.T) {
+ defer reset()
+
+ _, err := NewUint64Metric("/foo", false, pb.MetricMetadata_UNITS_NONE, fooDescription)
+ if err != nil {
+ t.Fatalf("NewUint64Metric got err %v want nil", err)
+ }
+
+ _, err = NewUint64Metric("/bar", true, pb.MetricMetadata_UNITS_NONE, barDescription)
+ if err != nil {
+ t.Fatalf("NewUint64Metric got err %v want nil", err)
+ }
+
+ Disable()
+
+ if len(emitter) != 1 {
+ t.Fatalf("Initialize emitted %d events want 1", len(emitter))
+ }
+
+ mr, ok := emitter[0].(*pb.MetricRegistration)
+ if !ok {
+ t.Fatalf("emitter %v got %T want pb.MetricRegistration", emitter[0], emitter[0])
+ }
+
+ if len(mr.Metrics) != 0 {
+ t.Errorf("MetricRegistration got %d metrics want 0", len(mr.Metrics))
+ }
+}
+
+func TestEmitMetricUpdate(t *testing.T) {
+ defer reset()
+
+ foo, err := NewUint64Metric("/foo", false, pb.MetricMetadata_UNITS_NONE, fooDescription)
+ if err != nil {
+ t.Fatalf("NewUint64Metric got err %v want nil", err)
+ }
+
+ _, err = NewUint64Metric("/bar", true, pb.MetricMetadata_UNITS_NONE, barDescription)
+ if err != nil {
+ t.Fatalf("NewUint64Metric got err %v want nil", err)
+ }
+
+ Initialize()
+
+ // Don't care about the registration metrics.
+ emitter.Reset()
+ EmitMetricUpdate()
+
+ if len(emitter) != 1 {
+ t.Fatalf("EmitMetricUpdate emitted %d events want 1", len(emitter))
+ }
+
+ update, ok := emitter[0].(*pb.MetricUpdate)
+ if !ok {
+ t.Fatalf("emitter %v got %T want pb.MetricUpdate", emitter[0], emitter[0])
+ }
+
+ if len(update.Metrics) != 2 {
+ t.Errorf("MetricUpdate got %d metrics want 2", len(update.Metrics))
+ }
+
+ // Both are included for their initial values.
+ foundFoo := false
+ foundBar := false
+ for _, m := range update.Metrics {
+ switch m.Name {
+ case "/foo":
+ foundFoo = true
+ case "/bar":
+ foundBar = true
+ }
+ uv, ok := m.Value.(*pb.MetricValue_Uint64Value)
+ if !ok {
+ t.Errorf("%+v: value %v got %T want pb.MetricValue_Uint64Value", m, m.Value, m.Value)
+ continue
+ }
+ if uv.Uint64Value != 0 {
+ t.Errorf("%v: Value got %v want 0", m, uv.Uint64Value)
+ }
+ }
+
+ if !foundFoo {
+ t.Errorf("/foo not found: %+v", emitter)
+ }
+ if !foundBar {
+ t.Errorf("/bar not found: %+v", emitter)
+ }
+
+ // Increment foo. Only it is included in the next update.
+ foo.Increment()
+
+ emitter.Reset()
+ EmitMetricUpdate()
+
+ if len(emitter) != 1 {
+ t.Fatalf("EmitMetricUpdate emitted %d events want 1", len(emitter))
+ }
+
+ update, ok = emitter[0].(*pb.MetricUpdate)
+ if !ok {
+ t.Fatalf("emitter %v got %T want pb.MetricUpdate", emitter[0], emitter[0])
+ }
+
+ if len(update.Metrics) != 1 {
+ t.Errorf("MetricUpdate got %d metrics want 1", len(update.Metrics))
+ }
+
+ m := update.Metrics[0]
+
+ if m.Name != "/foo" {
+ t.Errorf("Metric %+v name got %q want '/foo'", m, m.Name)
+ }
+
+ uv, ok := m.Value.(*pb.MetricValue_Uint64Value)
+ if !ok {
+ t.Errorf("%+v: value %v got %T want pb.MetricValue_Uint64Value", m, m.Value, m.Value)
+ }
+ if uv.Uint64Value != 1 {
+ t.Errorf("%v: Value got %v want 1", m, uv.Uint64Value)
+ }
+}