diff options
Diffstat (limited to 'pkg/metric')
-rw-r--r-- | pkg/metric/BUILD | 40 | ||||
-rw-r--r-- | pkg/metric/metric.go | 226 | ||||
-rw-r--r-- | pkg/metric/metric.proto | 68 | ||||
-rw-r--r-- | pkg/metric/metric_test.go | 252 |
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) + } +} |