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
Browse files Browse the repository at this point in the history
This package adds resources as a core concept to OpenCensus.

Signed-off-by: Fabian Reinartz <[email protected]>
  • Loading branch information
Fabian Reinartz committed Aug 22, 2018
1 parent 1789eaf commit e71bc5a
Show file tree
Hide file tree
Showing 2 changed files with 274 additions and 0 deletions.
158 changes: 158 additions & 0 deletions resource/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// 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 defines the resource type and provides helpers to derive them as well
// as the generic population through environment variables.
package resource

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

const (
envVarType = "OC_RESOURCE_TYPE"
envVarTags = "OC_RESOURCE_TAGS"
)

// Resource describes an entity about which data is exposed.
type Resource struct {
Type string
Tags map[string]string
}

// EncodeTags encodes a tags to a string as provided via the OC_RESOURCE_TAGS environment variable.
func EncodeTags(tags map[string]string) (s string) {
i := 0
for k, v := range tags {
if i > 0 {
s += ","
}
s += k + "=" + strconv.Quote(v)
i++
}
return s
}

// We accept domain names and paths as tag keys. Values may be quoted or unquoted in general.
// If a value contains whitespaces, =, or " characters, it must always be quoted.
var tagRegex = regexp.MustCompile(`\s*([a-zA-Z0-9-_./]+)=(?:(".*?")|([^\s="]+))\s*,`)

func DecodeTags(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 := tagRegex.FindStringSubmatch(s)
if len(match) == 0 {
return nil, fmt.Errorf("invalid tag 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 tag formatting, remainder: %s, err: %s", s, err)
}
}
m[match[1]] = v

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

// FromEnvVars loads resource information from the OC_TYPE and OC_RESOURCE_TAGS environment variables.
func FromEnvVars(context.Context) (*Resource, error) {
res := &Resource{
Type: strings.TrimSpace(os.Getenv(envVarType)),
}
tags := strings.TrimSpace(os.Getenv(envVarTags))
if tags == "" {
return res, nil
}
var err error
if res.Tags, err = DecodeTags(tags); err != nil {
return nil, err
}
return res, nil
}

// 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,
Tags: map[string]string{},
}
for k, v := range a.Tags {
res.Tags[k] = v
}
if res.Type == "" {
res.Type = b.Type
}
for k, v := range b.Tags {
if _, ok := res.Tags[k]; !ok {
res.Tags[k] = v
}
}
return res
}

// Detector attempts to detect resource information.
// If the detector cannot find specific information, the respective Resource fields should
// be left empty but no error should be returned.
// An error should only be returned if unexpected errors occur during lookup.
type Detector func(context.Context) (*Resource, error)

// NewDetectorFromResource returns a detector that will always return resource r.
func NewDetectorFromResource(r *Resource) Detector {
return func(context.Context) (*Resource, error) {
return r, nil
}
}

// ChainedDetector returns a Detector that 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 ChainedDetector(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
}
116 changes: 116 additions & 0 deletions resource/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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 (
"reflect"
"testing"
)

func TestMerge(t *testing.T) {
cases := []struct {
a, b, expect *Resource
}{
{
a: &Resource{
Type: "t1",
Tags: map[string]string{"a": "1", "b": "2"},
},
b: &Resource{
Type: "t2",
Tags: map[string]string{"a": "1", "b": "3", "c": "4"},
},
expect: &Resource{
Type: "t1",
Tags: map[string]string{"a": "1", "b": "2", "c": "4"},
},
},
{
a: nil,
b: &Resource{
Type: "t1",
Tags: map[string]string{"a": "1"},
},
expect: &Resource{
Type: "t1",
Tags: map[string]string{"a": "1"},
},
},
{
a: &Resource{
Type: "t1",
Tags: map[string]string{"a": "1"},
},
b: nil,
expect: &Resource{
Type: "t1",
Tags: map[string]string{"a": "1"},
},
},
}
for _, c := range cases {
res := Merge(c.a, c.b)
if !reflect.DeepEqual(res, c.expect) {
t.Fatalf("unexpected result: want %+v, got %+v", c.expect, res)
}
}
}

func TestDecodeTags(t *testing.T) {
cases := []struct {
s string
expect map[string]string
fail bool
}{
{
s: `example.org/test-1="test ¥ \"" ,un=quøted, Abc=Def`,
expect: map[string]string{"example.org/test-1": "test ¥ \"", "un": "quøted", "Abc": "Def"},
}, {
s: `single=key`,
expect: map[string]string{"single": "key"},
},
{s: `invalid-char-ü=test`, fail: true},
{s: `missing="trailing-quote`, fail: true},
{s: `missing=leading-quote"`, fail: true},
{s: `extra=chars, a`, fail: true},
{s: `a, extra=chars`, fail: true},
{s: `a, extra=chars`, fail: true},
}
for i, c := range cases {
t.Logf("test %d: %s", i, c.s)

res, err := DecodeTags(c.s)
if err != nil && !c.fail {
t.Fatalf("unexpected error: %s", err)
}
if c.fail && err == nil {
t.Fatalf("expected failure but got none, result: %v", res)
}
if !reflect.DeepEqual(res, c.expect) {
t.Fatalf("expected result %v, got %v", c.expect, res)
}
}
}

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

0 comments on commit e71bc5a

Please sign in to comment.