diff --git a/experimental/stats/metricregistry.go b/experimental/stats/metricregistry.go new file mode 100644 index 000000000000..392256942679 --- /dev/null +++ b/experimental/stats/metricregistry.go @@ -0,0 +1,230 @@ +/* + * + * Copyright 2024 gRPC 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 stats + +import ( + "maps" + "testing" + + "google.golang.org/grpc/grpclog" +) + +var logger = grpclog.Component("metrics-registry") + +// DefaultMetrics are the default metrics registered through global metrics +// registry. This is written to at initialization time only, and is read only +// after initialization. +var DefaultMetrics = NewMetrics() + +// MetricDescriptor is the data for a registered metric. +type MetricDescriptor struct { + // The name of this metric. This name must be unique across the whole binary + // (including any per call metrics). See + // https://github.com/grpc/proposal/blob/master/A79-non-per-call-metrics-architecture.md#metric-instrument-naming-conventions + // for metric naming conventions. + Name Metric + // The description of this metric. + Description string + // The unit (e.g. entries, seconds) of this metric. + Unit string + // The required label keys for this metric. These are intended to + // metrics emitted from a stats handler. + Labels []string + // The optional label keys for this metric. These are intended to attached + // to metrics emitted from a stats handler if configured. + OptionalLabels []string + // Whether this metric is on by default. + Default bool + // The type of metric. This is set by the metric registry, and not intended + // to be set by a component registering a metric. + Type MetricType +} + +// MetricType is the type of metric. +type MetricType int + +const ( + MetricTypeIntCount MetricType = iota + MetricTypeFloatCount + MetricTypeIntHisto + MetricTypeFloatHisto + MetricTypeIntGauge +) + +// Int64CountHandle is a typed handle for a int count metric. This handle +// is passed at the recording point in order to know which metric to record +// on. +type Int64CountHandle MetricDescriptor + +// Record records the int64 count value on the metrics recorder provided. +func (h *Int64CountHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) { + recorder.RecordInt64Count(h, incr, labels...) +} + +// Float64CountHandle is a typed handle for a float count metric. This handle is +// passed at the recording point in order to know which metric to record on. +type Float64CountHandle MetricDescriptor + +// Record records the float64 count value on the metrics recorder provided. +func (h *Float64CountHandle) Record(recorder MetricsRecorder, incr float64, labels ...string) { + recorder.RecordFloat64Count(h, incr, labels...) +} + +// Int64HistoHandle is a typed handle for an int histogram metric. This handle +// is passed at the recording point in order to know which metric to record on. +type Int64HistoHandle MetricDescriptor + +// Record records the int64 histo value on the metrics recorder provided. +func (h *Int64HistoHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) { + recorder.RecordInt64Histo(h, incr, labels...) +} + +// Float64HistoHandle is a typed handle for a float histogram metric. This +// handle is passed at the recording point in order to know which metric to +// record on. +type Float64HistoHandle MetricDescriptor + +// Record records the float64 histo value on the metrics recorder provided. +func (h *Float64HistoHandle) Record(recorder MetricsRecorder, incr float64, labels ...string) { + recorder.RecordFloat64Histo(h, incr, labels...) +} + +// Int64GaugeHandle is a typed handle for an int gauge metric. This handle is +// passed at the recording point in order to know which metric to record on. +type Int64GaugeHandle MetricDescriptor + +// Record records the int64 histo value on the metrics recorder provided. +func (h *Int64GaugeHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) { + recorder.RecordInt64Gauge(h, incr, labels...) +} + +// registeredMetrics are the registered metric descriptor names. +var registeredMetrics = make(map[Metric]bool) + +// metricsRegistry contains all of the registered metrics. +// +// This is written to only at init time, and read only after that. +var metricsRegistry = make(map[Metric]*MetricDescriptor) + +// DescriptorForMetric returns the MetricDescriptor from the global registry. +// +// Returns nil if MetricDescriptor not present. +func DescriptorForMetric(metric Metric) *MetricDescriptor { + return metricsRegistry[metric] +} + +func registerMetric(name Metric, def bool) { + if registeredMetrics[name] { + logger.Fatalf("metric %v already registered", name) + } + registeredMetrics[name] = true + if def { + DefaultMetrics = DefaultMetrics.Add(name) + } +} + +// RegisterInt64Count registers the metric description onto the global registry. +// It returns a typed handle to use to recording data. +// +// NOTE: this function must only be called during initialization time (i.e. in +// an init() function), and is not thread-safe. If multiple metrics are +// registered with the same name, this function will panic. +func RegisterInt64Count(descriptor MetricDescriptor) *Int64CountHandle { + registerMetric(descriptor.Name, descriptor.Default) + descriptor.Type = MetricTypeIntCount + descPtr := &descriptor + metricsRegistry[descriptor.Name] = descPtr + return (*Int64CountHandle)(descPtr) +} + +// RegisterFloat64Count registers the metric description onto the global +// registry. It returns a typed handle to use to recording data. +// +// NOTE: this function must only be called during initialization time (i.e. in +// an init() function), and is not thread-safe. If multiple metrics are +// registered with the same name, this function will panic. +func RegisterFloat64Count(descriptor MetricDescriptor) *Float64CountHandle { + registerMetric(descriptor.Name, descriptor.Default) + descriptor.Type = MetricTypeFloatCount + descPtr := &descriptor + metricsRegistry[descriptor.Name] = descPtr + return (*Float64CountHandle)(descPtr) +} + +// RegisterInt64Histo registers the metric description onto the global registry. +// It returns a typed handle to use to recording data. +// +// NOTE: this function must only be called during initialization time (i.e. in +// an init() function), and is not thread-safe. If multiple metrics are +// registered with the same name, this function will panic. +func RegisterInt64Histo(descriptor MetricDescriptor) *Int64HistoHandle { + registerMetric(descriptor.Name, descriptor.Default) + descriptor.Type = MetricTypeIntHisto + descPtr := &descriptor + metricsRegistry[descriptor.Name] = descPtr + return (*Int64HistoHandle)(descPtr) +} + +// RegisterFloat64Histo registers the metric description onto the global +// registry. It returns a typed handle to use to recording data. +// +// NOTE: this function must only be called during initialization time (i.e. in +// an init() function), and is not thread-safe. If multiple metrics are +// registered with the same name, this function will panic. +func RegisterFloat64Histo(descriptor MetricDescriptor) *Float64HistoHandle { + registerMetric(descriptor.Name, descriptor.Default) + descriptor.Type = MetricTypeFloatHisto + descPtr := &descriptor + metricsRegistry[descriptor.Name] = descPtr + return (*Float64HistoHandle)(descPtr) +} + +// RegisterInt64Gauge registers the metric description onto the global registry. +// It returns a typed handle to use to recording data. +// +// NOTE: this function must only be called during initialization time (i.e. in +// an init() function), and is not thread-safe. If multiple metrics are +// registered with the same name, this function will panic. +func RegisterInt64Gauge(descriptor MetricDescriptor) *Int64GaugeHandle { + registerMetric(descriptor.Name, descriptor.Default) + descriptor.Type = MetricTypeIntGauge + descPtr := &descriptor + metricsRegistry[descriptor.Name] = descPtr + return (*Int64GaugeHandle)(descPtr) +} + +// snapshotMetricsRegistryForTesting snapshots the global data of the metrics +// registry. Registers a cleanup function on the provided testing.T that sets +// the metrics registry to its original state. Only called in testing functions. +func snapshotMetricsRegistryForTesting(t *testing.T) { + oldDefaultMetrics := DefaultMetrics + oldRegisteredMetrics := registeredMetrics + oldMetricsRegistry := metricsRegistry + + registeredMetrics = make(map[Metric]bool) + metricsRegistry = make(map[Metric]*MetricDescriptor) + maps.Copy(registeredMetrics, registeredMetrics) + maps.Copy(metricsRegistry, metricsRegistry) + + t.Cleanup(func() { + DefaultMetrics = oldDefaultMetrics + registeredMetrics = oldRegisteredMetrics + metricsRegistry = oldMetricsRegistry + }) +} diff --git a/experimental/stats/metricregistry_test.go b/experimental/stats/metricregistry_test.go new file mode 100644 index 000000000000..bd243648c262 --- /dev/null +++ b/experimental/stats/metricregistry_test.go @@ -0,0 +1,261 @@ +/* + * + * Copyright 2024 gRPC 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 stats + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/internal/grpctest" +) + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +// TestPanic tests that registering two metrics with the same name across any +// type of metric triggers a panic. +func (s) TestPanic(t *testing.T) { + snapshotMetricsRegistryForTesting(t) + want := "metric simple counter already registered" + defer func() { + if r := recover(); !strings.Contains(fmt.Sprint(r), want) { + t.Errorf("expected panic contains %q, got %q", want, r) + } + }() + desc := MetricDescriptor{ + // Type is not expected to be set from the registerer, but meant to be + // set by the metric registry. + Name: "simple counter", + Description: "number of times recorded on tests", + Unit: "calls", + } + RegisterInt64Count(desc) + RegisterInt64Gauge(desc) +} + +// TestInstrumentRegistry tests the metric registry. It registers testing only +// metrics using the metric registry, and creates a fake metrics recorder which +// uses these metrics. Using the handles returned from the metric registry, this +// test records stats using the fake metrics recorder. Then, the test verifies +// the persisted metrics data in the metrics recorder is what is expected. Thus, +// this tests the interactions between the metrics recorder and the metrics +// registry. +func (s) TestMetricRegistry(t *testing.T) { + snapshotMetricsRegistryForTesting(t) + intCountHandle1 := RegisterInt64Count(MetricDescriptor{ + Name: "simple counter", + Description: "sum of all emissions from tests", + Unit: "int", + Labels: []string{"int counter label"}, + OptionalLabels: []string{"int counter optional label"}, + Default: false, + }) + floatCountHandle1 := RegisterFloat64Count(MetricDescriptor{ + Name: "float counter", + Description: "sum of all emissions from tests", + Unit: "float", + Labels: []string{"float counter label"}, + OptionalLabels: []string{"float counter optional label"}, + Default: false, + }) + intHistoHandle1 := RegisterInt64Histo(MetricDescriptor{ + Name: "int histo", + Description: "sum of all emissions from tests", + Unit: "int", + Labels: []string{"int histo label"}, + OptionalLabels: []string{"int histo optional label"}, + Default: false, + }) + floatHistoHandle1 := RegisterFloat64Histo(MetricDescriptor{ + Name: "float histo", + Description: "sum of all emissions from tests", + Unit: "float", + Labels: []string{"float histo label"}, + OptionalLabels: []string{"float histo optional label"}, + Default: false, + }) + intGaugeHandle1 := RegisterInt64Gauge(MetricDescriptor{ + Name: "simple gauge", + Description: "the most recent int emitted by test", + Unit: "int", + Labels: []string{"int gauge label"}, + OptionalLabels: []string{"int gauge optional label"}, + Default: false, + }) + + fmr := newFakeMetricsRecorder(t) + + intCountHandle1.Record(fmr, 1, []string{"some label value", "some optional label value"}...) + // The Metric Descriptor in the handle should be able to identify the metric + // information. This is the key passed to metrics recorder to identify + // metric. + if got := fmr.intValues[(*MetricDescriptor)(intCountHandle1)]; got != 1 { + t.Fatalf("fmr.intValues[intCountHandle1.MetricDescriptor] got %v, want: %v", got, 1) + } + + floatCountHandle1.Record(fmr, 1.2, []string{"some label value", "some optional label value"}...) + if got := fmr.floatValues[(*MetricDescriptor)(floatCountHandle1)]; got != 1.2 { + t.Fatalf("fmr.floatValues[floatCountHandle1.MetricDescriptor] got %v, want: %v", got, 1.2) + } + + intHistoHandle1.Record(fmr, 3, []string{"some label value", "some optional label value"}...) + if got := fmr.intValues[(*MetricDescriptor)(intHistoHandle1)]; got != 3 { + t.Fatalf("fmr.intValues[intHistoHandle1.MetricDescriptor] got %v, want: %v", got, 3) + } + + floatHistoHandle1.Record(fmr, 4.3, []string{"some label value", "some optional label value"}...) + if got := fmr.floatValues[(*MetricDescriptor)(floatHistoHandle1)]; got != 4.3 { + t.Fatalf("fmr.floatValues[floatHistoHandle1.MetricDescriptor] got %v, want: %v", got, 4.3) + } + + intGaugeHandle1.Record(fmr, 7, []string{"some label value", "some optional label value"}...) + if got := fmr.intValues[(*MetricDescriptor)(intGaugeHandle1)]; got != 7 { + t.Fatalf("fmr.intValues[intGaugeHandle1.MetricDescriptor] got %v, want: %v", got, 7) + } +} + +// TestNumerousIntCounts tests numerous int count metrics registered onto the +// metric registry. A component (simulated by test) should be able to record on +// the different registered int count metrics. +func TestNumerousIntCounts(t *testing.T) { + snapshotMetricsRegistryForTesting(t) + intCountHandle1 := RegisterInt64Count(MetricDescriptor{ + Name: "int counter", + Description: "sum of all emissions from tests", + Unit: "int", + Labels: []string{"int counter label"}, + OptionalLabels: []string{"int counter optional label"}, + Default: false, + }) + intCountHandle2 := RegisterInt64Count(MetricDescriptor{ + Name: "int counter 2", + Description: "sum of all emissions from tests", + Unit: "int", + Labels: []string{"int counter label"}, + OptionalLabels: []string{"int counter optional label"}, + Default: false, + }) + intCountHandle3 := RegisterInt64Count(MetricDescriptor{ + Name: "int counter 3", + Description: "sum of all emissions from tests", + Unit: "int", + Labels: []string{"int counter label"}, + OptionalLabels: []string{"int counter optional label"}, + Default: false, + }) + + fmr := newFakeMetricsRecorder(t) + + intCountHandle1.Record(fmr, 1, []string{"some label value", "some optional label value"}...) + got := []int64{fmr.intValues[(*MetricDescriptor)(intCountHandle1)], fmr.intValues[(*MetricDescriptor)(intCountHandle2)], fmr.intValues[(*MetricDescriptor)(intCountHandle3)]} + want := []int64{1, 0, 0} + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("fmr.intValues (-got, +want): %v", diff) + } + + intCountHandle2.Record(fmr, 1, []string{"some label value", "some optional label value"}...) + got = []int64{fmr.intValues[(*MetricDescriptor)(intCountHandle1)], fmr.intValues[(*MetricDescriptor)(intCountHandle2)], fmr.intValues[(*MetricDescriptor)(intCountHandle3)]} + want = []int64{1, 1, 0} + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("fmr.intValues (-got, +want): %v", diff) + } + + intCountHandle3.Record(fmr, 1, []string{"some label value", "some optional label value"}...) + got = []int64{fmr.intValues[(*MetricDescriptor)(intCountHandle1)], fmr.intValues[(*MetricDescriptor)(intCountHandle2)], fmr.intValues[(*MetricDescriptor)(intCountHandle3)]} + want = []int64{1, 1, 1} + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("fmr.intValues (-got, +want): %v", diff) + } + + intCountHandle3.Record(fmr, 1, []string{"some label value", "some optional label value"}...) + got = []int64{fmr.intValues[(*MetricDescriptor)(intCountHandle1)], fmr.intValues[(*MetricDescriptor)(intCountHandle2)], fmr.intValues[(*MetricDescriptor)(intCountHandle3)]} + want = []int64{1, 1, 2} + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("fmr.intValues (-got, +want): %v", diff) + } +} + +type fakeMetricsRecorder struct { + t *testing.T + + intValues map[*MetricDescriptor]int64 + floatValues map[*MetricDescriptor]float64 +} + +// newFakeMetricsRecorder returns a fake metrics recorder based off the current +// state of global metric registry. +func newFakeMetricsRecorder(t *testing.T) *fakeMetricsRecorder { + fmr := &fakeMetricsRecorder{ + t: t, + intValues: make(map[*MetricDescriptor]int64), + floatValues: make(map[*MetricDescriptor]float64), + } + + for _, desc := range metricsRegistry { + switch desc.Type { + case MetricTypeIntCount: + case MetricTypeIntHisto: + case MetricTypeIntGauge: + fmr.intValues[desc] = 0 + case MetricTypeFloatCount: + case MetricTypeFloatHisto: + fmr.floatValues[desc] = 0 + } + } + return fmr +} + +// verifyLabels verifies that the labels received are of the expected length. +func verifyLabels(t *testing.T, labelsWant []string, optionalLabelsWant []string, labelsGot []string) { + if len(labelsWant)+len(optionalLabelsWant) != len(labelsGot) { + t.Fatalf("length of optional labels expected did not match got %v, want %v", len(labelsGot), len(labelsWant)+len(optionalLabelsWant)) + } +} + +func (r *fakeMetricsRecorder) RecordInt64Count(handle *Int64CountHandle, incr int64, labels ...string) { + verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) + r.intValues[(*MetricDescriptor)(handle)] += incr +} + +func (r *fakeMetricsRecorder) RecordFloat64Count(handle *Float64CountHandle, incr float64, labels ...string) { + verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) + r.floatValues[(*MetricDescriptor)(handle)] += incr +} + +func (r *fakeMetricsRecorder) RecordInt64Histo(handle *Int64HistoHandle, incr int64, labels ...string) { + verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) + r.intValues[(*MetricDescriptor)(handle)] += incr +} + +func (r *fakeMetricsRecorder) RecordFloat64Histo(handle *Float64HistoHandle, incr float64, labels ...string) { + verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) + r.floatValues[(*MetricDescriptor)(handle)] += incr +} + +func (r *fakeMetricsRecorder) RecordInt64Gauge(handle *Int64GaugeHandle, incr int64, labels ...string) { + verifyLabels(r.t, (*MetricDescriptor)(handle).Labels, (*MetricDescriptor)(handle).OptionalLabels, labels) + r.intValues[(*MetricDescriptor)(handle)] += incr +} diff --git a/experimental/stats/metrics.go b/experimental/stats/metrics.go new file mode 100644 index 000000000000..3221f7a633a3 --- /dev/null +++ b/experimental/stats/metrics.go @@ -0,0 +1,114 @@ +/* + * + * Copyright 2024 gRPC 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 stats contains experimental metrics/stats API's. +package stats + +import "maps" + +// MetricsRecorder records on metrics derived from metric registry. +type MetricsRecorder interface { + // RecordInt64Count records the measurement alongside labels on the int + // count associated with the provided handle. + RecordInt64Count(handle *Int64CountHandle, incr int64, labels ...string) + // RecordFloat64Count records the measurement alongside labels on the float + // count associated with the provided handle. + RecordFloat64Count(handle *Float64CountHandle, incr float64, labels ...string) + // RecordInt64Histo records the measurement alongside labels on the int + // histo associated with the provided handle. + RecordInt64Histo(handle *Int64HistoHandle, incr int64, labels ...string) + // RecordFloat64Histo records the measurement alongside labels on the float + // histo associated with the provided handle. + RecordFloat64Histo(handle *Float64HistoHandle, incr float64, labels ...string) + // RecordInt64Gauge records the measurement alongside labels on the int + // gauge associated with the provided handle. + RecordInt64Gauge(handle *Int64GaugeHandle, incr int64, labels ...string) +} + +// Metric is an identifier for a metric. +type Metric string + +// Metrics is a set of metrics to record. Once created, Metrics is immutable, +// however Add and Remove can make copies with specific metrics added or +// removed, respectively. +// +// Do not construct directly; use NewMetrics instead. +type Metrics struct { + // metrics are the set of metrics to initialize. + metrics map[Metric]bool +} + +// NewMetrics returns a Metrics containing Metrics. +func NewMetrics(metrics ...Metric) *Metrics { + newMetrics := make(map[Metric]bool) + for _, metric := range metrics { + newMetrics[metric] = true + } + return &Metrics{ + metrics: newMetrics, + } +} + +// Metrics returns the metrics set. The returned map is read-only and must not +// be modified. +func (m *Metrics) Metrics() map[Metric]bool { + return m.metrics +} + +// Add adds the metrics to the metrics set and returns a new copy with the +// additional metrics. +func (m *Metrics) Add(metrics ...Metric) *Metrics { + newMetrics := make(map[Metric]bool) + for metric := range m.metrics { + newMetrics[metric] = true + } + + for _, metric := range metrics { + newMetrics[metric] = true + } + return &Metrics{ + metrics: newMetrics, + } +} + +// Join joins the metrics passed in with the metrics set, and returns a new copy +// with the merged metrics. +func (m *Metrics) Join(metrics *Metrics) *Metrics { + newMetrics := make(map[Metric]bool) + maps.Copy(newMetrics, m.metrics) + maps.Copy(newMetrics, metrics.metrics) + return &Metrics{ + metrics: newMetrics, + } +} + +// Remove removes the metrics from the metrics set and returns a new copy with +// the metrics removed. +func (m *Metrics) Remove(metrics ...Metric) *Metrics { + newMetrics := make(map[Metric]bool) + for metric := range m.metrics { + newMetrics[metric] = true + } + + for _, metric := range metrics { + delete(newMetrics, metric) + } + return &Metrics{ + metrics: newMetrics, + } +} diff --git a/stats/opentelemetry/client_metrics.go b/stats/opentelemetry/client_metrics.go index 8654132153fd..f9bcf466532f 100644 --- a/stats/opentelemetry/client_metrics.go +++ b/stats/opentelemetry/client_metrics.go @@ -22,6 +22,7 @@ import ( "time" "google.golang.org/grpc" + estats "google.golang.org/grpc/experimental/stats" istats "google.golang.org/grpc/internal/stats" "google.golang.org/grpc/metadata" "google.golang.org/grpc/stats" @@ -54,11 +55,11 @@ func (h *clientStatsHandler) initializeMetrics() { metrics = DefaultMetrics } - h.clientMetrics.attemptStarted = createInt64Counter(metrics.metrics, "grpc.client.attempt.started", meter, otelmetric.WithUnit("attempt"), otelmetric.WithDescription("Number of client call attempts started.")) - h.clientMetrics.attemptDuration = createFloat64Histogram(metrics.metrics, "grpc.client.attempt.duration", meter, otelmetric.WithUnit("s"), otelmetric.WithDescription("End-to-end time taken to complete a client call attempt."), otelmetric.WithExplicitBucketBoundaries(DefaultLatencyBounds...)) - h.clientMetrics.attemptSentTotalCompressedMessageSize = createInt64Histogram(metrics.metrics, "grpc.client.attempt.sent_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes sent per client call attempt."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) - h.clientMetrics.attemptRcvdTotalCompressedMessageSize = createInt64Histogram(metrics.metrics, "grpc.client.attempt.rcvd_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes received per call attempt."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) - h.clientMetrics.callDuration = createFloat64Histogram(metrics.metrics, "grpc.client.call.duration", meter, otelmetric.WithUnit("s"), otelmetric.WithDescription("Time taken by gRPC to complete an RPC from application's perspective."), otelmetric.WithExplicitBucketBoundaries(DefaultLatencyBounds...)) + h.clientMetrics.attemptStarted = createInt64Counter(metrics.Metrics(), "grpc.client.attempt.started", meter, otelmetric.WithUnit("attempt"), otelmetric.WithDescription("Number of client call attempts started.")) + h.clientMetrics.attemptDuration = createFloat64Histogram(metrics.Metrics(), "grpc.client.attempt.duration", meter, otelmetric.WithUnit("s"), otelmetric.WithDescription("End-to-end time taken to complete a client call attempt."), otelmetric.WithExplicitBucketBoundaries(DefaultLatencyBounds...)) + h.clientMetrics.attemptSentTotalCompressedMessageSize = createInt64Histogram(metrics.Metrics(), "grpc.client.attempt.sent_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes sent per client call attempt."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) + h.clientMetrics.attemptRcvdTotalCompressedMessageSize = createInt64Histogram(metrics.Metrics(), "grpc.client.attempt.rcvd_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes received per call attempt."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) + h.clientMetrics.callDuration = createFloat64Histogram(metrics.Metrics(), "grpc.client.call.duration", meter, otelmetric.WithUnit("s"), otelmetric.WithDescription("Time taken by gRPC to complete an RPC from application's perspective."), otelmetric.WithExplicitBucketBoundaries(DefaultLatencyBounds...)) } func (h *clientStatsHandler) unaryInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { @@ -238,17 +239,17 @@ func (h *clientStatsHandler) processRPCEnd(ctx context.Context, ai *attemptInfo, const ( // ClientAttemptStarted is the number of client call attempts started. - ClientAttemptStarted Metric = "grpc.client.attempt.started" + ClientAttemptStarted estats.Metric = "grpc.client.attempt.started" // ClientAttemptDuration is the end-to-end time taken to complete a client // call attempt. - ClientAttemptDuration Metric = "grpc.client.attempt.duration" + ClientAttemptDuration estats.Metric = "grpc.client.attempt.duration" // ClientAttemptSentCompressedTotalMessageSize is the compressed message // bytes sent per client call attempt. - ClientAttemptSentCompressedTotalMessageSize Metric = "grpc.client.attempt.sent_total_compressed_message_size" + ClientAttemptSentCompressedTotalMessageSize estats.Metric = "grpc.client.attempt.sent_total_compressed_message_size" // ClientAttemptRcvdCompressedTotalMessageSize is the compressed message // bytes received per call attempt. - ClientAttemptRcvdCompressedTotalMessageSize Metric = "grpc.client.attempt.rcvd_total_compressed_message_size" + ClientAttemptRcvdCompressedTotalMessageSize estats.Metric = "grpc.client.attempt.rcvd_total_compressed_message_size" // ClientCallDuration is the time taken by gRPC to complete an RPC from // application's perspective. - ClientCallDuration Metric = "grpc.client.call.duration" + ClientCallDuration estats.Metric = "grpc.client.call.duration" ) diff --git a/stats/opentelemetry/example_test.go b/stats/opentelemetry/example_test.go index b4d6755b2299..156918e2496c 100644 --- a/stats/opentelemetry/example_test.go +++ b/stats/opentelemetry/example_test.go @@ -19,6 +19,7 @@ package opentelemetry_test import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + stats2 "google.golang.org/grpc/experimental/stats" "google.golang.org/grpc/stats/opentelemetry" "go.opentelemetry.io/otel/sdk/metric" @@ -102,7 +103,7 @@ func ExampleMetrics_disableAll() { // To disable all metrics, initialize Options as follows: opts := opentelemetry.Options{ MetricsOptions: opentelemetry.MetricsOptions{ - Metrics: opentelemetry.NewMetrics(), // Distinct to nil, which creates default metrics. This empty set creates no metrics. + Metrics: stats2.NewMetrics(), // Distinct to nil, which creates default metrics. This empty set creates no metrics. }, } do := opentelemetry.DialOption(opts) @@ -117,7 +118,7 @@ func ExampleMetrics_enableSome() { // To only create specific metrics, initialize Options as follows: opts := opentelemetry.Options{ MetricsOptions: opentelemetry.MetricsOptions{ - Metrics: opentelemetry.NewMetrics(opentelemetry.ClientAttemptDuration, opentelemetry.ClientAttemptRcvdCompressedTotalMessageSize), // only create these metrics + Metrics: stats2.NewMetrics(opentelemetry.ClientAttemptDuration, opentelemetry.ClientAttemptRcvdCompressedTotalMessageSize), // only create these metrics }, } do := opentelemetry.DialOption(opts) diff --git a/stats/opentelemetry/opentelemetry.go b/stats/opentelemetry/opentelemetry.go index 4bc195b4e12c..aa5354d7a264 100644 --- a/stats/opentelemetry/opentelemetry.go +++ b/stats/opentelemetry/opentelemetry.go @@ -25,6 +25,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" + estats "google.golang.org/grpc/experimental/stats" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/internal" otelinternal "google.golang.org/grpc/stats/opentelemetry/internal" @@ -45,60 +46,6 @@ var canonicalString = internal.CanonicalString.(func(codes.Code) string) var joinDialOptions = internal.JoinDialOptions.(func(...grpc.DialOption) grpc.DialOption) -// Metric is an identifier for a metric provided by this package. -type Metric string - -// Metrics is a set of metrics to record. Once created, Metrics is immutable, -// however Add and Remove can make copies with specific metrics added or -// removed, respectively. -type Metrics struct { - // metrics are the set of metrics to initialize. - metrics map[Metric]bool -} - -// NewMetrics returns a Metrics containing Metrics. -func NewMetrics(metrics ...Metric) *Metrics { - newMetrics := make(map[Metric]bool) - for _, metric := range metrics { - newMetrics[metric] = true - } - return &Metrics{ - metrics: newMetrics, - } -} - -// Add adds the metrics to the metrics set and returns a new copy with the -// additional metrics. -func (m *Metrics) Add(metrics ...Metric) *Metrics { - newMetrics := make(map[Metric]bool) - for metric := range m.metrics { - newMetrics[metric] = true - } - - for _, metric := range metrics { - newMetrics[metric] = true - } - return &Metrics{ - metrics: newMetrics, - } -} - -// Remove removes the metrics from the metrics set and returns a new copy with -// the metrics removed. -func (m *Metrics) Remove(metrics ...Metric) *Metrics { - newMetrics := make(map[Metric]bool) - for metric := range m.metrics { - newMetrics[metric] = true - } - - for _, metric := range metrics { - delete(newMetrics, metric) - } - return &Metrics{ - metrics: newMetrics, - } -} - // Options are the options for OpenTelemetry instrumentation. type Options struct { // MetricsOptions are the metrics options for OpenTelemetry instrumentation. @@ -118,7 +65,7 @@ type MetricsOptions struct { // for corresponding metric supported by the client and server // instrumentation components if applicable. If not set, the default metrics // will be recorded. - Metrics *Metrics + Metrics *estats.Metrics // MethodAttributeFilter is to record the method name of RPCs handled by // grpc.UnknownServiceHandler, but take care to limit the values allowed, as @@ -260,7 +207,7 @@ type serverMetrics struct { callDuration metric.Float64Histogram } -func createInt64Counter(setOfMetrics map[Metric]bool, metricName Metric, meter metric.Meter, options ...metric.Int64CounterOption) metric.Int64Counter { +func createInt64Counter(setOfMetrics map[estats.Metric]bool, metricName estats.Metric, meter metric.Meter, options ...metric.Int64CounterOption) metric.Int64Counter { if _, ok := setOfMetrics[metricName]; !ok { return noop.Int64Counter{} } @@ -272,7 +219,7 @@ func createInt64Counter(setOfMetrics map[Metric]bool, metricName Metric, meter m return ret } -func createInt64Histogram(setOfMetrics map[Metric]bool, metricName Metric, meter metric.Meter, options ...metric.Int64HistogramOption) metric.Int64Histogram { +func createInt64Histogram(setOfMetrics map[estats.Metric]bool, metricName estats.Metric, meter metric.Meter, options ...metric.Int64HistogramOption) metric.Int64Histogram { if _, ok := setOfMetrics[metricName]; !ok { return noop.Int64Histogram{} } @@ -284,7 +231,7 @@ func createInt64Histogram(setOfMetrics map[Metric]bool, metricName Metric, meter return ret } -func createFloat64Histogram(setOfMetrics map[Metric]bool, metricName Metric, meter metric.Meter, options ...metric.Float64HistogramOption) metric.Float64Histogram { +func createFloat64Histogram(setOfMetrics map[estats.Metric]bool, metricName estats.Metric, meter metric.Meter, options ...metric.Float64HistogramOption) metric.Float64Histogram { if _, ok := setOfMetrics[metricName]; !ok { return noop.Float64Histogram{} } @@ -307,5 +254,5 @@ var ( // DefaultSizeBounds are the default bounds for metrics which record size. DefaultSizeBounds = []float64{0, 1024, 2048, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456, 1073741824, 4294967296} // DefaultMetrics are the default metrics provided by this module. - DefaultMetrics = NewMetrics(ClientAttemptStarted, ClientAttemptDuration, ClientAttemptSentCompressedTotalMessageSize, ClientAttemptRcvdCompressedTotalMessageSize, ClientCallDuration, ServerCallStarted, ServerCallSentCompressedTotalMessageSize, ServerCallRcvdCompressedTotalMessageSize, ServerCallDuration) + DefaultMetrics = estats.NewMetrics(ClientAttemptStarted, ClientAttemptDuration, ClientAttemptSentCompressedTotalMessageSize, ClientAttemptRcvdCompressedTotalMessageSize, ClientCallDuration, ServerCallStarted, ServerCallSentCompressedTotalMessageSize, ServerCallRcvdCompressedTotalMessageSize, ServerCallDuration) ) diff --git a/stats/opentelemetry/server_metrics.go b/stats/opentelemetry/server_metrics.go index a20d6a261fc6..6f9ad7a80929 100644 --- a/stats/opentelemetry/server_metrics.go +++ b/stats/opentelemetry/server_metrics.go @@ -22,6 +22,7 @@ import ( "time" "google.golang.org/grpc" + estats "google.golang.org/grpc/experimental/stats" "google.golang.org/grpc/internal" "google.golang.org/grpc/metadata" "google.golang.org/grpc/stats" @@ -53,10 +54,10 @@ func (h *serverStatsHandler) initializeMetrics() { metrics = DefaultMetrics } - h.serverMetrics.callStarted = createInt64Counter(metrics.metrics, "grpc.server.call.started", meter, otelmetric.WithUnit("call"), otelmetric.WithDescription("Number of server calls started.")) - h.serverMetrics.callSentTotalCompressedMessageSize = createInt64Histogram(metrics.metrics, "grpc.server.call.sent_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes sent per server call."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) - h.serverMetrics.callRcvdTotalCompressedMessageSize = createInt64Histogram(metrics.metrics, "grpc.server.call.rcvd_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes received per server call."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) - h.serverMetrics.callDuration = createFloat64Histogram(metrics.metrics, "grpc.server.call.duration", meter, otelmetric.WithUnit("s"), otelmetric.WithDescription("End-to-end time taken to complete a call from server transport's perspective."), otelmetric.WithExplicitBucketBoundaries(DefaultLatencyBounds...)) + h.serverMetrics.callStarted = createInt64Counter(metrics.Metrics(), "grpc.server.call.started", meter, otelmetric.WithUnit("call"), otelmetric.WithDescription("Number of server calls started.")) + h.serverMetrics.callSentTotalCompressedMessageSize = createInt64Histogram(metrics.Metrics(), "grpc.server.call.sent_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes sent per server call."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) + h.serverMetrics.callRcvdTotalCompressedMessageSize = createInt64Histogram(metrics.Metrics(), "grpc.server.call.rcvd_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes received per server call."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) + h.serverMetrics.callDuration = createFloat64Histogram(metrics.Metrics(), "grpc.server.call.duration", meter, otelmetric.WithUnit("s"), otelmetric.WithDescription("End-to-end time taken to complete a call from server transport's perspective."), otelmetric.WithExplicitBucketBoundaries(DefaultLatencyBounds...)) } // attachLabelsTransportStream intercepts SetHeader and SendHeader calls of the @@ -254,14 +255,14 @@ func (h *serverStatsHandler) processRPCEnd(ctx context.Context, ai *attemptInfo, const ( // ServerCallStarted is the number of server calls started. - ServerCallStarted Metric = "grpc.server.call.started" + ServerCallStarted estats.Metric = "grpc.server.call.started" // ServerCallSentCompressedTotalMessageSize is the compressed message bytes // sent per server call. - ServerCallSentCompressedTotalMessageSize Metric = "grpc.server.call.sent_total_compressed_message_size" + ServerCallSentCompressedTotalMessageSize estats.Metric = "grpc.server.call.sent_total_compressed_message_size" // ServerCallRcvdCompressedTotalMessageSize is the compressed message bytes // received per server call. - ServerCallRcvdCompressedTotalMessageSize Metric = "grpc.server.call.rcvd_total_compressed_message_size" + ServerCallRcvdCompressedTotalMessageSize estats.Metric = "grpc.server.call.rcvd_total_compressed_message_size" // ServerCallDuration is the end-to-end time taken to complete a call from // server transport's perspective. - ServerCallDuration Metric = "grpc.server.call.duration" + ServerCallDuration estats.Metric = "grpc.server.call.duration" )