From d37d851bbce65577ab340849ec6c540fdc6e3096 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Wed, 6 Dec 2023 10:36:21 -0800 Subject: [PATCH] Add the internal experimental metric feature package (#4715) * Add the internal experimental metric feature pkg * Interpret empty envvar same as unset * Test Exemplars and CardinalityLimit * Fix empty test for CardinalityLimit * Abstract common testing patterns * Add test cases from review feedback * Rename assertions based on review feedback --------- Co-authored-by: Chester Cheung --- sdk/metric/internal/x/x.go | 93 +++++++++++++++++++++++++++++++++ sdk/metric/internal/x/x_test.go | 81 ++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 sdk/metric/internal/x/x.go create mode 100644 sdk/metric/internal/x/x_test.go diff --git a/sdk/metric/internal/x/x.go b/sdk/metric/internal/x/x.go new file mode 100644 index 000000000..289139572 --- /dev/null +++ b/sdk/metric/internal/x/x.go @@ -0,0 +1,93 @@ +// Copyright The OpenTelemetry 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 x contains support for OTel metric SDK experimental features. +// +// This package should only be used for features defined in the specification. +// It should not be used for experiments or new project ideas. +package x // import "go.opentelemetry.io/otel/sdk/metric/internal/x" + +import ( + "os" + "strconv" + "strings" +) + +var ( + // Exemplars is an experimental feature flag that defines if exemplars + // should be recorded for metric data-points. + // + // To enable this feature set the OTEL_GO_X_EXEMPLAR environment variable + // to the case-insensitive string value of "true" (i.e. "True" and "TRUE" + // will also enable this). + Exemplars = newFeature("EXEMPLAR", func(v string) (string, bool) { + if strings.ToLower(v) == "true" { + return v, true + } + return "", false + }) + + // CardinalityLimit is an experimental feature flag that defines if + // cardinality limits should be applied to the recorded metric data-points. + // + // To enable this feature set the OTEL_GO_X_CARDINALITY_LIMIT environment + // variable to the integer limit value you want to use. + CardinalityLimit = newFeature("CARDINALITY_LIMIT", func(v string) (int, bool) { + n, err := strconv.Atoi(v) + if err != nil { + return 0, false + } + return n, true + }) +) + +// Feature is an experimental feature control flag. It provides a uniform way +// to interact with these feature flags and parse their values. +type Feature[T any] struct { + key string + parse func(v string) (T, bool) +} + +func newFeature[T any](suffix string, parse func(string) (T, bool)) Feature[T] { + const envKeyRoot = "OTEL_GO_X_" + return Feature[T]{ + key: envKeyRoot + suffix, + parse: parse, + } +} + +// Key returns the environment variable key that needs to be set to enable the +// feature. +func (f Feature[T]) Key() string { return f.key } + +// Lookup returns the user configured value for the feature and true if the +// user has enabled the feature. Otherwise, if the feature is not enabled, a +// zero-value and false are returned. +func (f Feature[T]) Lookup() (v T, ok bool) { + // https://github.com/open-telemetry/opentelemetry-specification/blob/62effed618589a0bec416a87e559c0a9d96289bb/specification/configuration/sdk-environment-variables.md#parsing-empty-value + // + // > The SDK MUST interpret an empty value of an environment variable the + // > same way as when the variable is unset. + vRaw := os.Getenv(f.key) + if vRaw == "" { + return v, ok + } + return f.parse(vRaw) +} + +// Enabled returns if the feature is enabled. +func (f Feature[T]) Enabled() bool { + _, ok := f.Lookup() + return ok +} diff --git a/sdk/metric/internal/x/x_test.go b/sdk/metric/internal/x/x_test.go new file mode 100644 index 000000000..b643fe265 --- /dev/null +++ b/sdk/metric/internal/x/x_test.go @@ -0,0 +1,81 @@ +// Copyright The OpenTelemetry 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 x + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExemplars(t *testing.T) { + const key = "OTEL_GO_X_EXEMPLAR" + require.Equal(t, key, Exemplars.Key()) + + t.Run("true", run(setenv(key, "true"), assertEnabled(Exemplars, "true"))) + t.Run("True", run(setenv(key, "True"), assertEnabled(Exemplars, "True"))) + t.Run("TRUE", run(setenv(key, "TRUE"), assertEnabled(Exemplars, "TRUE"))) + t.Run("false", run(setenv(key, "false"), assertDisabled(Exemplars))) + t.Run("1", run(setenv(key, "1"), assertDisabled(Exemplars))) + t.Run("empty", run(assertDisabled(Exemplars))) +} + +func TestCardinalityLimit(t *testing.T) { + const key = "OTEL_GO_X_CARDINALITY_LIMIT" + require.Equal(t, key, CardinalityLimit.Key()) + + t.Run("100", run(setenv(key, "100"), assertEnabled(CardinalityLimit, 100))) + t.Run("-1", run(setenv(key, "-1"), assertEnabled(CardinalityLimit, -1))) + t.Run("false", run(setenv(key, "false"), assertDisabled(CardinalityLimit))) + t.Run("empty", run(assertDisabled(CardinalityLimit))) +} + +func run(steps ...func(*testing.T)) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + for _, step := range steps { + step(t) + } + } +} + +func setenv(k, v string) func(t *testing.T) { + return func(t *testing.T) { t.Setenv(k, v) } +} + +func assertEnabled[T any](f Feature[T], want T) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + assert.True(t, f.Enabled(), "not enabled") + + v, ok := f.Lookup() + assert.True(t, ok, "Lookup state") + assert.Equal(t, want, v, "Lookup value") + } +} + +func assertDisabled[T any](f Feature[T]) func(*testing.T) { + var zero T + return func(t *testing.T) { + t.Helper() + + assert.False(t, f.Enabled(), "enabled") + + v, ok := f.Lookup() + assert.False(t, ok, "Lookup state") + assert.Equal(t, zero, v, "Lookup value") + } +}