diff --git a/CHANGELOG.md b/CHANGELOG.md index 38494fe2c16..58b59ff5e28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added +- Define a general Resources Detector Interface and allow resources to be detected from environment variables. (#939) - Github action to generate protobuf Go bindings locally in `internal/opentelemetry-proto-gen`. (#938) - OTLP .proto files from `open-telemetry/opentelemetry-proto` imported as a git submodule under `internal/opentelemetry-proto`. References to `github.com/open-telemetry/opentelemetry-proto` changed to `go.opentelemetry.io/otel/internal/opentelemetry-proto-gen`. (#942) diff --git a/sdk/resource/auto.go b/sdk/resource/auto.go new file mode 100644 index 00000000000..4a1f765fbac --- /dev/null +++ b/sdk/resource/auto.go @@ -0,0 +1,67 @@ +// 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 resource + +import ( + "context" + "errors" + "fmt" +) + +var ( + // ErrMissingResource is returned by a detector when source information + // is unavailable for a Resource. + ErrMissingResource = errors.New("missing resource") + // ErrPartialResource is returned by a detector when complete source + // information for a Resource is unavailable or the source information + // contains invalid values that are omitted from the returned Resource. + ErrPartialResource = errors.New("partial resource") +) + +// Detector detects OpenTelemetry resource information +type Detector interface { + // Detect returns an initialized Resource based on gathered information. + // If source information to construct a Resource is inaccessible, an + // uninitialized Resource is returned with an appropriately wrapped + // ErrMissingResource error is returned. If the source information to + // construct a Resource contains invalid values, a Resource is returned + // with the valid parts of the source information used for + // initialization along with an appropriately wrapped ErrPartialResource + // error. + Detect(ctx context.Context) (*Resource, error) +} + +// Detect calls all input detectors sequentially and merges each result with the previous one. +// It returns the merged error too. +func Detect(ctx context.Context, detectors ...Detector) (*Resource, error) { + var autoDetectedRes *Resource + var errInfo []string + for _, detector := range detectors { + res, err := detector.Detect(ctx) + if err != nil { + errInfo = append(errInfo, err.Error()) + if !errors.Is(err, ErrPartialResource) { + continue + } + } + autoDetectedRes = Merge(autoDetectedRes, res) + } + + var aggregatedError error + if len(errInfo) > 0 { + aggregatedError = fmt.Errorf("detecting resources: %s", errInfo) + } + return autoDetectedRes, aggregatedError +} diff --git a/sdk/resource/doc.go b/sdk/resource/doc.go new file mode 100644 index 00000000000..c3a43c7886f --- /dev/null +++ b/sdk/resource/doc.go @@ -0,0 +1,28 @@ +// 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 resource provides detecting and representing resources. +// +// The fundamental struct is a Resource which holds identifying information +// about the entities for which telemetry is exported. +// +// To automatically construct Resources from an environment a Detector +// interface is defined. Implementations of this interface can be passed to +// the Detect function to generate a Resource from the merged information. +// +// To load a user defined Resource from the environment variable +// OTEL_RESOURCE_LABELS the FromEnv Detector can be used. It will interpret +// the value as a list of comma delimited key/value pairs +// (e.g. `=,=,...`). +package resource diff --git a/sdk/resource/env.go b/sdk/resource/env.go new file mode 100644 index 00000000000..a03e387ba5b --- /dev/null +++ b/sdk/resource/env.go @@ -0,0 +1,69 @@ +// 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 resource + +import ( + "context" + "fmt" + "os" + "strings" + + "go.opentelemetry.io/otel/api/kv" +) + +// envVar is the environment variable name OpenTelemetry Resource information can be assigned to. +const envVar = "OTEL_RESOURCE_LABELS" + +var ( + //errMissingValue is returned when a resource value is missing. + errMissingValue = fmt.Errorf("%w: missing value", ErrPartialResource) +) + +// FromEnv is a detector that implements the Detector and collects resources +// from environment +type FromEnv struct{} + +// compile time assertion that FromEnv implements Detector interface +var _ Detector = (*FromEnv)(nil) + +// Detect collects resources from environment +func (d *FromEnv) Detect(context.Context) (*Resource, error) { + labels := strings.TrimSpace(os.Getenv(envVar)) + + if labels == "" { + return Empty(), ErrMissingResource + } + return constructOTResources(labels) +} + +func constructOTResources(s string) (*Resource, error) { + pairs := strings.Split(s, ",") + labels := []kv.KeyValue{} + var invalid []string + for _, p := range pairs { + field := strings.SplitN(p, "=", 2) + if len(field) != 2 { + invalid = append(invalid, p) + continue + } + k, v := strings.TrimSpace(field[0]), strings.TrimSpace(field[1]) + labels = append(labels, kv.String(k, v)) + } + var err error + if len(invalid) > 0 { + err = fmt.Errorf("%w: %v", errMissingValue, invalid) + } + return New(labels...), err +} diff --git a/sdk/resource/env_test.go b/sdk/resource/env_test.go new file mode 100644 index 00000000000..5302976540a --- /dev/null +++ b/sdk/resource/env_test.go @@ -0,0 +1,73 @@ +// 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 resource + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/api/kv" +) + +func TestDetectOnePair(t *testing.T) { + os.Setenv(envVar, "key=value") + + detector := &FromEnv{} + res, err := detector.Detect(context.Background()) + require.NoError(t, err) + assert.Equal(t, New(kv.String("key", "value")), res) +} + +func TestDetectMultiPairs(t *testing.T) { + os.Setenv("x", "1") + os.Setenv(envVar, "key=value, k = v , a= x, a=z") + + detector := &FromEnv{} + res, err := detector.Detect(context.Background()) + require.NoError(t, err) + assert.Equal(t, res, New( + kv.String("key", "value"), + kv.String("k", "v"), + kv.String("a", "x"), + kv.String("a", "z"), + )) +} + +func TestEmpty(t *testing.T) { + os.Setenv(envVar, "") + + detector := &FromEnv{} + res, err := detector.Detect(context.Background()) + require.Error(t, err) + assert.Equal(t, err, ErrMissingResource) + assert.Equal(t, Empty(), res) +} + +func TestMissingKeyError(t *testing.T) { + os.Setenv(envVar, "key=value,key") + + detector := &FromEnv{} + res, err := detector.Detect(context.Background()) + assert.Error(t, err) + assert.Equal(t, err, fmt.Errorf("%w: %v", errMissingValue, "[key]")) + assert.Equal(t, res, New( + kv.String("key", "value"), + )) +} diff --git a/sdk/resource/resource.go b/sdk/resource/resource.go index 608fd4d0edf..1f53e50db1c 100644 --- a/sdk/resource/resource.go +++ b/sdk/resource/resource.go @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package resource provides functionality for resource, which capture -// identifying information about the entities for which signals are exported. package resource import ( @@ -34,7 +32,7 @@ type Resource struct { var emptyResource Resource -// Key creates a resource from a set of attributes. If there are +// New creates a resource from a set of attributes. If there are // duplicate keys present in the list of attributes, then the last // value found for the key is preserved. func New(kvs ...kv.KeyValue) *Resource {