summaryrefslogtreecommitdiffhomepage
path: root/pkg/metric
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/metric')
-rw-r--r--pkg/metric/BUILD40
-rw-r--r--pkg/metric/metric.go226
-rw-r--r--pkg/metric/metric.proto68
-rw-r--r--pkg/metric/metric_test.go252
4 files changed, 586 insertions, 0 deletions
diff --git a/pkg/metric/BUILD b/pkg/metric/BUILD
new file mode 100644
index 000000000..e3f50d528
--- /dev/null
+++ b/pkg/metric/BUILD
@@ -0,0 +1,40 @@
+package(licenses = ["notice"]) # Apache 2.0
+
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "metric",
+ srcs = ["metric.go"],
+ importpath = "gvisor.googlesource.com/gvisor/pkg/metric",
+ visibility = ["//:sandbox"],
+ deps = [
+ ":metric_go_proto",
+ "//pkg/eventchannel",
+ "//pkg/log",
+ ],
+)
+
+proto_library(
+ name = "metric_proto",
+ srcs = ["metric.proto"],
+ visibility = ["//:sandbox"],
+)
+
+go_proto_library(
+ name = "metric_go_proto",
+ importpath = "gvisor.googlesource.com/gvisor/pkg/metric/metric_go_proto",
+ proto = ":metric_proto",
+ visibility = ["//:sandbox"],
+)
+
+go_test(
+ name = "metric_test",
+ srcs = ["metric_test.go"],
+ embed = [":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..0743612f0
--- /dev/null
+++ b/pkg/metric/metric.go
@@ -0,0 +1,226 @@
+// Copyright 2018 Google Inc.
+//
+// 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"
+ "sync/atomic"
+
+ "gvisor.googlesource.com/gvisor/pkg/eventchannel"
+ "gvisor.googlesource.com/gvisor/pkg/log"
+ pb "gvisor.googlesource.com/gvisor/pkg/metric/metric_go_proto"
+)
+
+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.
+//
+// All metrics must be cumulative, meaning that their values will only increase
+// over time.
+//
+// Metrics are not saved across save/restore and thus reset to zero on restore.
+//
+// TODO: Support non-cumulative metrics.
+// TODO: Support metric fields.
+//
+type Uint64Metric struct {
+ // metadata describes the metric. It is immutable.
+ metadata *pb.MetricMetadata
+
+ // 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())
+ }
+}
+
+// NewUint64Metric creates a new metric with the given name.
+//
+// Metrics must be statically defined (i.e., at startup). NewUint64Metric will
+// return an error if called after Initialized.
+//
+// Preconditions:
+// * name must be globally unique.
+// * Initialize/Disable have not been called.
+func NewUint64Metric(name string, sync bool, description string) (*Uint64Metric, error) {
+ if initialized {
+ return nil, ErrInitializationDone
+ }
+
+ if _, ok := allMetrics.m[name]; ok {
+ return nil, ErrNameInUse
+ }
+
+ m := &Uint64Metric{
+ metadata: &pb.MetricMetadata{
+ Name: name,
+ Description: description,
+ Cumulative: true,
+ Sync: sync,
+ Type: pb.MetricMetadata_UINT64,
+ },
+ }
+ allMetrics.m[name] = m
+ return m, nil
+}
+
+// MustCreateNewUint64Metric calls NewUint64Metric and panics if it returns an
+// error.
+func MustCreateNewUint64Metric(name string, sync bool, description string) *Uint64Metric {
+ m, err := NewUint64Metric(name, sync, 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]*Uint64Metric
+}
+
+// makeMetricSet returns a new metricSet.
+func makeMetricSet() metricSet {
+ return metricSet{
+ m: make(map[string]*Uint64Metric),
+ }
+}
+
+// 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..6108cb7c0
--- /dev/null
+++ b/pkg/metric/metric.proto
@@ -0,0 +1,68 @@
+// Copyright 2018 Google Inc.
+//
+// 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 { UINT64 = 0; }
+
+ // type is the type of the metric value.
+ Type type = 5;
+}
+
+// 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..7d156e4a5
--- /dev/null
+++ b/pkg/metric/metric_test.go
@@ -0,0 +1,252 @@
+// Copyright 2018 Google Inc.
+//
+// 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.googlesource.com/gvisor/pkg/eventchannel"
+ pb "gvisor.googlesource.com/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, fooDescription)
+ if err != nil {
+ t.Fatalf("NewUint64Metric got err %v want nil", err)
+ }
+
+ _, err = NewUint64Metric("/bar", true, 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_UINT64 {
+ t.Errorf("Metadata %+v Type got %v want %v", m, m.Type, pb.MetricMetadata_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)
+ }
+ 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 !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, fooDescription)
+ if err != nil {
+ t.Fatalf("NewUint64Metric got err %v want nil", err)
+ }
+
+ _, err = NewUint64Metric("/bar", true, 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, fooDescription)
+ if err != nil {
+ t.Fatalf("NewUint64Metric got err %v want nil", err)
+ }
+
+ _, err = NewUint64Metric("/bar", true, 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)
+ }
+}