Skip to content
This repository has been archived by the owner on Jul 31, 2023. It is now read-only.

Commit

Permalink
Add resource package (#878)
Browse files Browse the repository at this point in the history
* Add resource package

This package adds resources as a core concept to OpenCensus.

Signed-off-by: Fabian Reinartz <[email protected]>

* Address comments

Signed-off-by: Fabian Reinartz <[email protected]>

* Added and fixed tests

Signed-off-by: Fabian Reinartz <[email protected]>

* Make Detectall unexported

Signed-off-by: Fabian Reinartz <[email protected]>

* Address comments

Signed-off-by: Fabian Reinartz <[email protected]>

* Rename Tags to Labels, expose envvar names

Signed-off-by: Fabian Reinartz <[email protected]>

* Adjust label parsing to new syntax spec

Signed-off-by: Fabian Reinartz <[email protected]>

* Fix replace error

Signed-off-by: Fabian Reinartz <[email protected]>

* Address comments

Signed-off-by: Fabian Reinartz <[email protected]>

* Address comments

Signed-off-by: Fabian Reinartz <[email protected]>
  • Loading branch information
fabxc authored Oct 29, 2018
1 parent 96e75b8 commit 4f7fcb4
Show file tree
Hide file tree
Showing 2 changed files with 326 additions and 0 deletions.
163 changes: 163 additions & 0 deletions resource/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright 2018, OpenCensus 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 functionality for resource, which capture
// identifying information about the entities for which signals are exported.
package resource

import (
"context"
"fmt"
"os"
"regexp"
"sort"
"strconv"
"strings"
)

const (
EnvVarType = "OC_RESOURCE_TYPE"
EnvVarLabels = "OC_RESOURCE_LABELS"
)

// Resource describes an entity about which identifying information and metadata is exposed.
// For example, a type "k8s.io/container" may hold labels describing the pod name and namespace.
type Resource struct {
Type string
Labels map[string]string
}

// EncodeLabels encodes a labels map to a string as provided via the OC_RESOURCE_LABELS environment variable.
func EncodeLabels(labels map[string]string) string {
sortedKeys := make([]string, 0, len(labels))
for k := range labels {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)

s := ""
for i, k := range sortedKeys {
if i > 0 {
s += ","
}
s += k + "=" + strconv.Quote(labels[k])
}
return s
}

var labelRegex = regexp.MustCompile(`^\s*([[:ascii:]]{1,256}?)=("[[:ascii:]]{0,256}?")\s*,`)

// DecodeLabels decodes a serialized label map as used in the OC_RESOURCE_LABELS variable.
// A list of labels of the form `<key1>="<value1>",<key2>="<value2>",...` is accepted.
// Domain names and paths are accepted as label keys.
// Most users will want to use FromEnv instead.
func DecodeLabels(s string) (map[string]string, error) {
m := map[string]string{}
// Ensure a trailing comma, which allows us to keep the regex simpler
s = strings.TrimRight(strings.TrimSpace(s), ",") + ","

for len(s) > 0 {
match := labelRegex.FindStringSubmatch(s)
if len(match) == 0 {
return nil, fmt.Errorf("invalid label formatting, remainder: %s", s)
}
v := match[2]
if v == "" {
v = match[3]
} else {
var err error
if v, err = strconv.Unquote(v); err != nil {
return nil, fmt.Errorf("invalid label formatting, remainder: %s, err: %s", s, err)
}
}
m[match[1]] = v

s = s[len(match[0]):]
}
return m, nil
}

// FromEnv is a detector that loads resource information from the OC_RESOURCE_TYPE
// and OC_RESOURCE_labelS environment variables.
func FromEnv(context.Context) (*Resource, error) {
res := &Resource{
Type: strings.TrimSpace(os.Getenv(EnvVarType)),
}
labels := strings.TrimSpace(os.Getenv(EnvVarLabels))
if labels == "" {
return res, nil
}
var err error
if res.Labels, err = DecodeLabels(labels); err != nil {
return nil, err
}
return res, nil
}

var _ Detector = FromEnv

// merge resource information from b into a. In case of a collision, a takes precedence.
func merge(a, b *Resource) *Resource {
if a == nil {
return b
}
if b == nil {
return a
}
res := &Resource{
Type: a.Type,
Labels: map[string]string{},
}
if res.Type == "" {
res.Type = b.Type
}
for k, v := range b.Labels {
res.Labels[k] = v
}
// Labels from resource a overwrite labels from resource b.
for k, v := range a.Labels {
res.Labels[k] = v
}
return res
}

// Detector attempts to detect resource information.
// If the detector cannot find resource information, the returned resource is nil but no
// error is returned.
// An error is only returned on unexpected failures.
type Detector func(context.Context) (*Resource, error)

// MultiDetector returns a Detector that calls all input detectors in order and
// merges each result with the previous one. In case a type of label key is already set,
// the first set value is takes precedence.
// It returns on the first error that a sub-detector encounters.
func MultiDetector(detectors ...Detector) Detector {
return func(ctx context.Context) (*Resource, error) {
return detectAll(ctx, detectors...)
}
}

// detectall calls all input detectors sequentially an merges each result with the previous one.
// It returns on the first error that a sub-detector encounters.
func detectAll(ctx context.Context, detectors ...Detector) (*Resource, error) {
var res *Resource
for _, d := range detectors {
r, err := d(ctx)
if err != nil {
return nil, err
}
res = merge(res, r)
}
return res, nil
}
163 changes: 163 additions & 0 deletions resource/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright 2018, OpenCensus 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"
"reflect"
"testing"
)

func TestMerge(t *testing.T) {
cases := []struct {
a, b, want *Resource
}{
{
a: &Resource{
Type: "t1",
Labels: map[string]string{"a": "1", "b": "2"},
},
b: &Resource{
Type: "t2",
Labels: map[string]string{"a": "1", "b": "3", "c": "4"},
},
want: &Resource{
Type: "t1",
Labels: map[string]string{"a": "1", "b": "2", "c": "4"},
},
},
{
a: nil,
b: &Resource{
Type: "t1",
Labels: map[string]string{"a": "1"},
},
want: &Resource{
Type: "t1",
Labels: map[string]string{"a": "1"},
},
},
{
a: &Resource{
Type: "t1",
Labels: map[string]string{"a": "1"},
},
b: nil,
want: &Resource{
Type: "t1",
Labels: map[string]string{"a": "1"},
},
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
res := merge(c.a, c.b)
if !reflect.DeepEqual(res, c.want) {
t.Fatalf("unwanted result: want %+v, got %+v", c.want, res)
}
})
}
}

func TestDecodeLabels(t *testing.T) {
cases := []struct {
encoded string
wantLabels map[string]string
wantFail bool
}{
{
encoded: `example.org/test-1="test $ \"" , Abc="Def"`,
wantLabels: map[string]string{"example.org/test-1": "test $ \"", "Abc": "Def"},
}, {
encoded: `single="key"`,
wantLabels: map[string]string{"single": "key"},
},
{encoded: `invalid-char-ü="test"`, wantFail: true},
{encoded: `invalid-char="ü-test"`, wantFail: true},
{encoded: `missing="trailing-quote`, wantFail: true},
{encoded: `missing=leading-quote"`, wantFail: true},
{encoded: `extra="chars", a`, wantFail: true},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
res, err := DecodeLabels(c.encoded)
if err != nil && !c.wantFail {
t.Fatalf("unwanted error: %s", err)
}
if c.wantFail && err == nil {
t.Fatalf("wanted failure but got none, result: %v", res)
}
if !reflect.DeepEqual(res, c.wantLabels) {
t.Fatalf("wanted result %v, got %v", c.wantLabels, res)
}
})
}
}

func TestEncodeLabels(t *testing.T) {
got := EncodeLabels(map[string]string{
"example.org/test-1": "test ¥ \"",
"un": "quøted",
"Abc": "Def",
})
if want := `Abc="Def",example.org/test-1="test ¥ \"",un="quøted"`; got != want {
t.Fatalf("got %q, want %q", got, want)
}
}

func TestMultiDetector(t *testing.T) {
got, err := MultiDetector(
func(context.Context) (*Resource, error) {
return &Resource{
Type: "t1",
Labels: map[string]string{"a": "1", "b": "2"},
}, nil
},
func(context.Context) (*Resource, error) {
return &Resource{
Type: "t2",
Labels: map[string]string{"a": "11", "c": "3"},
}, nil
},
)(context.Background())
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
want := &Resource{
Type: "t1",
Labels: map[string]string{"a": "1", "b": "2", "c": "3"},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected resource: want %v, got %v", want, got)
}

wantErr := errors.New("err1")
got, err = MultiDetector(
func(context.Context) (*Resource, error) {
return &Resource{
Type: "t1",
Labels: map[string]string{"a": "1", "b": "2"},
}, nil
},
func(context.Context) (*Resource, error) {
return nil, wantErr
},
)(context.Background())
if err != wantErr {
t.Fatalf("unexpected error: want %v, got %v", wantErr, err)
}
}

0 comments on commit 4f7fcb4

Please sign in to comment.