1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-02-05 13:15:41 +02:00

Change resource.New() to use functional options; add builtin attributes for (host.*, telemetry.sdk.*) (#1235)

* Add a resource.Configure() with functional options

* Add a changelog

* Add tests for builtin resources

* Rename to WithoutBuiltin

* Add new test; restore environment after tests

* Apply feedback

* Apply suggestions from code review

❤️

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>

* Comment edits

* Rename New, former method NewFromAttributes

* NewFromAttributes->NewWithAttributes

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
This commit is contained in:
Joshua MacDonald 2020-10-31 11:16:55 -07:00 committed by GitHub
parent 034195692a
commit 187adeb251
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 553 additions and 140 deletions

View File

@ -35,6 +35,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Move the `go.opentelemetry.io/otel/api/global` package to `go.opentelemetry.io/otel/global`. (#1262)
- Rename correlation context header from `"otcorrelations"` to `"baggage"` to match the OpenTelemetry specification. (#1267)
- Fix `Code.UnmarshalJSON` to work with valid json only. (#1276)
- The `resource.New()` method changes signature to support builtin attributes and functional options, including `telemetry.sdk.*` and
`host.name` semantic conventions; the former method is renamed `resource.NewWithAttributes`. (#1235)
### Removed

View File

@ -41,6 +41,7 @@ import (
// Initializes an OTLP exporter, and configures the corresponding trace and
// metric providers.
func initProvider() func() {
ctx := context.Background()
// If the OpenTelemetry Collector is running on a local cluster (minikube or
// microk8s), it should be accessible through the NodePort service at the
@ -54,13 +55,18 @@ func initProvider() func() {
)
handleErr(err, "failed to create exporter")
res, err := resource.New(ctx,
resource.WithAttributes(
// the service name used to display traces in backends
semconv.ServiceNameKey.String("test-service"),
),
)
handleErr(err, "failed to create resource")
bsp := sdktrace.NewBatchSpanProcessor(exp)
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}),
sdktrace.WithResource(resource.New(
// the service name used to display traces in backends
semconv.ServiceNameKey.String("test-service"),
)),
sdktrace.WithResource(res),
sdktrace.WithSpanProcessor(bsp),
)
@ -80,7 +86,6 @@ func initProvider() func() {
pusher.Start()
return func() {
ctx := context.Background()
handleErr(tracerProvider.Shutdown(ctx), "failed to shutdown provider")
handleErr(exp.Shutdown(ctx), "failed to stop exporter")
pusher.Stop() // pushes any last exports to the receiver

View File

@ -37,10 +37,20 @@ import (
// TODO: Address this issue.
func ExampleNewExportPipeline() {
// Create a resource, with builtin attributes plus R=V.
res, err := resource.New(
context.Background(),
resource.WithoutBuiltin(), // Test-only!
resource.WithAttributes(label.String("R", "V")),
)
if err != nil {
panic(err)
}
// Create a meter
exporter, err := prometheus.NewExportPipeline(
prometheus.Config{},
pull.WithResource(resource.New(label.String("R", "V"))),
pull.WithResource(res),
)
if err != nil {
panic(err)

View File

@ -39,7 +39,7 @@ func TestPrometheusExporter(t *testing.T) {
DefaultHistogramBoundaries: []float64{-0.5, 1},
},
pull.WithCachePeriod(0),
pull.WithResource(resource.New(label.String("R", "V"))),
pull.WithResource(resource.NewWithAttributes(label.String("R", "V"))),
)
require.NoError(t, err)

View File

@ -320,7 +320,7 @@ func TestRecordAggregatorIncompatibleErrors(t *testing.T) {
makeMpb := func(kind aggregation.Kind, agg aggregation.Aggregation) (*metricpb.Metric, error) {
desc := otel.NewDescriptor("things", otel.CounterInstrumentKind, otel.Int64NumberKind)
labels := label.NewSet()
res := resource.New()
res := resource.Empty()
test := &testAgg{
kind: kind,
agg: agg,
@ -357,7 +357,7 @@ func TestRecordAggregatorUnexpectedErrors(t *testing.T) {
makeMpb := func(kind aggregation.Kind, agg aggregation.Aggregation) (*metricpb.Metric, error) {
desc := otel.NewDescriptor("things", otel.CounterInstrumentKind, otel.Int64NumberKind)
labels := label.NewSet()
res := resource.New()
res := resource.Empty()
return Record(export.NewRecord(&desc, &labels, res, agg, intervalStart, intervalEnd))
}

View File

@ -40,7 +40,7 @@ func TestEmptyResource(t *testing.T) {
func TestResourceAttributes(t *testing.T) {
attrs := []label.KeyValue{label.Int("one", 1), label.Int("two", 2)}
got := Resource(resource.New(attrs...)).GetAttributes()
got := Resource(resource.NewWithAttributes(attrs...)).GetAttributes()
if !assert.Len(t, attrs, 2) {
return
}

View File

@ -252,7 +252,7 @@ func TestSpanData(t *testing.T) {
DroppedAttributeCount: 1,
DroppedMessageEventCount: 2,
DroppedLinkCount: 3,
Resource: resource.New(label.String("rk1", "rv1"), label.Int64("rk2", 5)),
Resource: resource.NewWithAttributes(label.String("rk1", "rv1"), label.Int64("rk2", 5)),
InstrumentationLibrary: instrumentation.Library{
Name: "go.opentelemetry.io/test/otel",
Version: "v0.0.1",

View File

@ -94,13 +94,13 @@ func newExporterEndToEndTest(t *testing.T, additionalOpts []otlp.ExporterOption)
),
}
tp1 := sdktrace.NewTracerProvider(append(pOpts,
sdktrace.WithResource(resource.New(
sdktrace.WithResource(resource.NewWithAttributes(
label.String("rk1", "rv11)"),
label.Int64("rk2", 5),
)))...)
tp2 := sdktrace.NewTracerProvider(append(pOpts,
sdktrace.WithResource(resource.New(
sdktrace.WithResource(resource.NewWithAttributes(
label.String("rk1", "rv12)"),
label.Float32("rk3", 6.5),
)))...)

View File

@ -104,8 +104,8 @@ var (
baseKeyValues = []label.KeyValue{label.String("host", "test.com")}
cpuKey = label.Key("CPU")
testInstA = resource.New(label.String("instance", "tester-a"))
testInstB = resource.New(label.String("instance", "tester-b"))
testInstA = resource.NewWithAttributes(label.String("instance", "tester-a"))
testInstB = resource.NewWithAttributes(label.String("instance", "tester-b"))
testHistogramBoundaries = []float64{2.0, 4.0, 8.0}

View File

@ -97,7 +97,7 @@ func TestExportSpans(t *testing.T) {
},
StatusCode: codes.Ok,
StatusMessage: "Ok",
Resource: resource.New(label.String("instance", "tester-a")),
Resource: resource.NewWithAttributes(label.String("instance", "tester-a")),
InstrumentationLibrary: instrumentation.Library{
Name: "lib-a",
Version: "v0.1.0",
@ -119,7 +119,7 @@ func TestExportSpans(t *testing.T) {
},
StatusCode: codes.Ok,
StatusMessage: "Ok",
Resource: resource.New(label.String("instance", "tester-a")),
Resource: resource.NewWithAttributes(label.String("instance", "tester-a")),
InstrumentationLibrary: instrumentation.Library{
Name: "lib-b",
Version: "v0.1.0",
@ -142,7 +142,7 @@ func TestExportSpans(t *testing.T) {
},
StatusCode: codes.Ok,
StatusMessage: "Ok",
Resource: resource.New(label.String("instance", "tester-a")),
Resource: resource.NewWithAttributes(label.String("instance", "tester-a")),
InstrumentationLibrary: instrumentation.Library{
Name: "lib-a",
Version: "v0.1.0",
@ -164,7 +164,7 @@ func TestExportSpans(t *testing.T) {
},
StatusCode: codes.Error,
StatusMessage: "Unauthenticated",
Resource: resource.New(label.String("instance", "tester-b")),
Resource: resource.NewWithAttributes(label.String("instance", "tester-b")),
InstrumentationLibrary: instrumentation.Library{
Name: "lib-a",
Version: "v1.1.0",

View File

@ -48,7 +48,7 @@ type testFixture struct {
output *bytes.Buffer
}
var testResource = resource.New(label.String("R", "V"))
var testResource = resource.NewWithAttributes(label.String("R", "V"))
func newFixture(t *testing.T, opts ...stdout.Option) testFixture {
buf := &bytes.Buffer{}
@ -290,11 +290,11 @@ func TestStdoutResource(t *testing.T) {
}
testCases := []testCase{
newCase("R1=V1,R2=V2,A=B,C=D",
resource.New(label.String("R1", "V1"), label.String("R2", "V2")),
resource.NewWithAttributes(label.String("R1", "V1"), label.String("R2", "V2")),
label.String("A", "B"),
label.String("C", "D")),
newCase("R1=V1,R2=V2",
resource.New(label.String("R1", "V1"), label.String("R2", "V2")),
resource.NewWithAttributes(label.String("R1", "V1"), label.String("R2", "V2")),
),
newCase("A=B,C=D",
nil,
@ -304,7 +304,7 @@ func TestStdoutResource(t *testing.T) {
// We explicitly do not de-duplicate between resources
// and metric labels in this exporter.
newCase("R1=V1,R2=V2,R1=V3,R2=V4",
resource.New(label.String("R1", "V1"), label.String("R2", "V2")),
resource.NewWithAttributes(label.String("R1", "V1"), label.String("R2", "V2")),
label.String("R1", "V3"),
label.String("R2", "V4")),
}

View File

@ -44,7 +44,7 @@ func TestExporter_ExportSpan(t *testing.T) {
spanID, _ := otel.SpanIDFromHex("0102030405060708")
keyValue := "value"
doubleValue := 123.456
resource := resource.New(label.String("rk1", "rv11"))
resource := resource.NewWithAttributes(label.String("rk1", "rv11"))
testSpan := &export.SpanData{
SpanContext: otel.SpanContext{

View File

@ -409,7 +409,7 @@ func Test_spanDataToThrift(t *testing.T) {
StatusCode: codes.Error,
StatusMessage: statusMessage,
SpanKind: otel.SpanKindClient,
Resource: resource.New(label.String("rk1", rv1), label.Int64("rk2", rv2)),
Resource: resource.NewWithAttributes(label.String("rk1", rv1), label.Int64("rk2", rv2)),
InstrumentationLibrary: instrumentation.Library{
Name: instrLibName,
Version: instrLibVersion,

View File

@ -42,7 +42,7 @@ func newFixture(b *testing.B) *benchFixture {
AggregatorSelector: processortest.AggregatorSelector(),
}
bf.accumulator = sdk.NewAccumulator(bf)
bf.accumulator = sdk.NewAccumulator(bf, nil)
bf.meter = otel.WrapMeterImpl(bf.accumulator, "benchmarks")
return bf
}

View File

@ -1,45 +0,0 @@
// 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 metric
import (
"go.opentelemetry.io/otel/sdk/resource"
)
// Config contains configuration for an SDK.
type Config struct {
// Resource describes all the metric records processed by the
// Accumulator.
Resource *resource.Resource
}
// Option is the interface that applies the value to a configuration option.
type Option interface {
// Apply sets the Option value of a Config.
Apply(*Config)
}
// WithResource sets the Resource configuration option of a Config.
func WithResource(res *resource.Resource) Option {
return resourceOption{res}
}
type resourceOption struct {
*resource.Resource
}
func (o resourceOption) Apply(config *Config) {
config.Resource = o.Resource
}

View File

@ -60,7 +60,7 @@ func New(checkpointer export.Checkpointer, options ...Option) *Controller {
}
accum := sdk.NewAccumulator(
checkpointer,
sdk.WithResource(config.Resource),
config.Resource,
)
return &Controller{
accumulator: accum,

View File

@ -24,7 +24,7 @@ import (
)
func TestWithResource(t *testing.T) {
r := resource.New(label.String("A", "a"))
r := resource.NewWithAttributes(label.String("A", "a"))
c := &Config{}
WithResource(r).Apply(c)

View File

@ -61,7 +61,7 @@ func New(checkpointer export.Checkpointer, exporter export.Exporter, opts ...Opt
impl := sdk.NewAccumulator(
checkpointer,
sdk.WithResource(c.Resource),
c.Resource,
)
return &Controller{
provider: registry.NewMeterProvider(impl),

View File

@ -36,7 +36,7 @@ import (
"go.opentelemetry.io/otel/sdk/resource"
)
var testResource = resource.New(label.String("R", "V"))
var testResource = resource.NewWithAttributes(label.String("R", "V"))
type handler struct {
sync.Mutex

View File

@ -34,7 +34,7 @@ import (
)
var Must = otel.Must
var testResource = resource.New(label.String("R", "V"))
var testResource = resource.NewWithAttributes(label.String("R", "V"))
type handler struct {
sync.Mutex
@ -96,7 +96,7 @@ func newSDK(t *testing.T) (otel.Meter, *metricsdk.Accumulator, *correctnessProce
}
accum := metricsdk.NewAccumulator(
processor,
metricsdk.WithResource(testResource),
testResource,
)
meter := otel.WrapMeterImpl(accum, "test")
return meter, accum, processor

View File

@ -121,7 +121,7 @@ func testProcessor(
// Note: this selector uses the instrument name to dictate
// aggregation kind.
selector := processorTest.AggregatorSelector()
res := resource.New(label.String("R", "V"))
res := resource.NewWithAttributes(label.String("R", "V"))
labs1 := []label.KeyValue{label.String("L1", "V")}
labs2 := []label.KeyValue{label.String("L2", "V")}
@ -361,7 +361,7 @@ func TestBasicTimestamps(t *testing.T) {
}
func TestStatefulNoMemoryCumulative(t *testing.T) {
res := resource.New(label.String("R", "V"))
res := resource.NewWithAttributes(label.String("R", "V"))
ekind := export.CumulativeExporter
desc := otel.NewDescriptor("inst.sum", otel.CounterInstrumentKind, otel.Int64NumberKind)
@ -395,7 +395,7 @@ func TestStatefulNoMemoryCumulative(t *testing.T) {
}
func TestStatefulNoMemoryDelta(t *testing.T) {
res := resource.New(label.String("R", "V"))
res := resource.NewWithAttributes(label.String("R", "V"))
ekind := export.DeltaExporter
desc := otel.NewDescriptor("inst.sum", otel.SumObserverInstrumentKind, otel.Int64NumberKind)
@ -435,7 +435,7 @@ func TestMultiObserverSum(t *testing.T) {
export.DeltaExporter,
} {
res := resource.New(label.String("R", "V"))
res := resource.NewWithAttributes(label.String("R", "V"))
desc := otel.NewDescriptor("observe.sum", otel.SumObserverInstrumentKind, otel.Int64NumberKind)
selector := processorTest.AggregatorSelector()

View File

@ -32,9 +32,7 @@ func generateTestData(proc export.Processor) {
ctx := context.Background()
accum := metricsdk.NewAccumulator(
proc,
metricsdk.WithResource(
resource.New(label.String("R", "V")),
),
resource.NewWithAttributes(label.String("R", "V")),
)
meter := otel.WrapMeterImpl(accum, "testing")

View File

@ -75,9 +75,7 @@ func TestFilterProcessor(t *testing.T) {
)
accum := metricsdk.NewAccumulator(
reducer.New(testFilter{}, processorTest.Checkpointer(testProc)),
metricsdk.WithResource(
resource.New(label.String("R", "V")),
),
resource.NewWithAttributes(label.String("R", "V")),
)
generateData(accum)
@ -94,9 +92,7 @@ func TestFilterBasicProcessor(t *testing.T) {
basicProc := basic.New(processorTest.AggregatorSelector(), export.CumulativeExporter)
accum := metricsdk.NewAccumulator(
reducer.New(testFilter{}, basicProc),
metricsdk.WithResource(
resource.New(label.String("R", "V")),
),
resource.NewWithAttributes(label.String("R", "V")),
)
exporter := processorTest.NewExporter(basicProc, label.DefaultEncoder())

View File

@ -305,16 +305,11 @@ func (s *syncInstrument) RecordOne(ctx context.Context, number api.Number, kvs [
// processor will call Collect() when it receives a request to scrape
// current metric values. A push-based processor should configure its
// own periodic collection.
func NewAccumulator(processor export.Processor, opts ...Option) *Accumulator {
c := &Config{}
for _, opt := range opts {
opt.Apply(c)
}
func NewAccumulator(processor export.Processor, resource *resource.Resource) *Accumulator {
return &Accumulator{
processor: processor,
asyncInstruments: internal.NewAsyncInstrumentState(),
resource: c.Resource,
resource: resource,
}
}

View File

@ -292,7 +292,8 @@ func stressTest(t *testing.T, impl testImpl) {
AggregatorSelector: processortest.AggregatorSelector(),
}
cc := concurrency()
sdk := NewAccumulator(fixture)
sdk := NewAccumulator(fixture, nil)
meter := otel.WrapMeterImpl(sdk, "stress_test")
fixture.wg.Add(cc + 1)

View File

@ -43,6 +43,9 @@ func Detect(ctx context.Context, detectors ...Detector) (*Resource, error) {
var autoDetectedRes *Resource
var errInfo []string
for _, detector := range detectors {
if detector == nil {
continue
}
res, err := detector.Detect(ctx)
if err != nil {
errInfo = append(errInfo, err.Error())

View File

@ -47,7 +47,7 @@ func makeLabels(n int) (_, _ *resource.Resource) {
}
}
return resource.New(l1...), resource.New(l2...)
return resource.NewWithAttributes(l1...), resource.NewWithAttributes(l2...)
}
func benchmarkMergeResource(b *testing.B, size int) {

81
sdk/resource/builtin.go Normal file
View File

@ -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 resource
import (
"context"
"fmt"
"os"
"go.opentelemetry.io/otel/label"
opentelemetry "go.opentelemetry.io/otel/sdk"
"go.opentelemetry.io/otel/semconv"
)
type (
// TelemetrySDK is a Detector that provides information about
// the OpenTelemetry SDK used. This Detector is included as a
// builtin. If these resource attributes are not wanted, use
// the WithTelemetrySDK(nil) or WithoutBuiltin() options to
// explicitly disable them.
TelemetrySDK struct{}
// Host is a Detector that provides information about the host
// being run on. This Detector is included as a builtin. If
// these resource attributes are not wanted, use the
// WithHost(nil) or WithoutBuiltin() options to explicitly
// disable them.
Host struct{}
stringDetector struct {
K label.Key
F func() (string, error)
}
)
var (
_ Detector = TelemetrySDK{}
_ Detector = Host{}
_ Detector = stringDetector{}
)
// Detect returns a *Resource that describes the OpenTelemetry SDK used.
func (TelemetrySDK) Detect(context.Context) (*Resource, error) {
return NewWithAttributes(
semconv.TelemetrySDKNameKey.String("opentelemetry-go"),
semconv.TelemetrySDKLanguageKey.String("go"),
semconv.TelemetrySDKVersionKey.String(opentelemetry.Version()),
), nil
}
// Detect returns a *Resource that describes the host being run on.
func (Host) Detect(ctx context.Context) (*Resource, error) {
return StringDetector(semconv.HostNameKey, os.Hostname).Detect(ctx)
}
// StringDetector returns a Detector that will produce a *Resource
// containing the string as a value corresponding to k.
func StringDetector(k label.Key, f func() (string, error)) Detector {
return stringDetector{K: k, F: f}
}
// Detect implements Detector.
func (sd stringDetector) Detect(ctx context.Context) (*Resource, error) {
value, err := sd.F()
if err != nil {
return nil, fmt.Errorf("%s: %w", string(sd.K), err)
}
return NewWithAttributes(sd.K.String(value)), nil
}

View File

@ -0,0 +1,59 @@
// 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_test
import (
"context"
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/label"
"go.opentelemetry.io/otel/sdk/resource"
)
func TestBuiltinStringDetector(t *testing.T) {
E := fmt.Errorf("no K")
res, err := resource.StringDetector(label.Key("K"), func() (string, error) {
return "", E
}).Detect(context.Background())
require.True(t, errors.Is(err, E))
require.NotEqual(t, E, err)
require.Nil(t, res)
}
func TestBuiltinStringConfig(t *testing.T) {
res, err := resource.New(
context.Background(),
resource.WithoutBuiltin(),
resource.WithAttributes(label.String("A", "B")),
resource.WithDetectors(resource.StringDetector(label.Key("K"), func() (string, error) {
return "", fmt.Errorf("K-IS-MISSING")
})),
)
require.Error(t, err)
require.Contains(t, err.Error(), "K-IS-MISSING")
require.NotNil(t, res)
m := map[string]string{}
for _, kv := range res.Attributes() {
m[string(kv.Key)] = kv.Value.Emit()
}
require.EqualValues(t, map[string]string{
"A": "B",
}, m)
}

150
sdk/resource/config.go Normal file
View File

@ -0,0 +1,150 @@
// 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"
"go.opentelemetry.io/otel/label"
)
// config contains configuration for Resource creation.
type config struct {
// detectors that will be evaluated.
detectors []Detector
// telemetrySDK is used to specify non-default
// `telemetry.sdk.*` attributes.
telemetrySDK Detector
// HostResource is used to specify non-default `host.*`
// attributes.
host Detector
// FromEnv is used to specify non-default OTEL_RESOURCE_ATTRIBUTES
// attributes.
fromEnv Detector
}
// Option is the interface that applies a configuration option.
type Option interface {
// Apply sets the Option value of a config.
Apply(*config)
}
// WithAttributes adds attributes to the configured Resource.
func WithAttributes(attributes ...label.KeyValue) Option {
return WithDetectors(detectAttributes{attributes})
}
type detectAttributes struct {
attributes []label.KeyValue
}
func (d detectAttributes) Detect(context.Context) (*Resource, error) {
return NewWithAttributes(d.attributes...), nil
}
// WithDetectors adds detectors to be evaluated for the configured resource.
func WithDetectors(detectors ...Detector) Option {
return detectorsOption{detectors}
}
type detectorsOption struct {
detectors []Detector
}
// Apply implements Option.
func (o detectorsOption) Apply(cfg *config) {
cfg.detectors = append(cfg.detectors, o.detectors...)
}
// WithTelemetrySDK overrides the builtin `telemetry.sdk.*`
// attributes. Use nil to disable these attributes entirely.
func WithTelemetrySDK(d Detector) Option {
return telemetrySDKOption{d}
}
type telemetrySDKOption struct {
Detector
}
// Apply implements Option.
func (o telemetrySDKOption) Apply(cfg *config) {
cfg.telemetrySDK = o.Detector
}
// WithHost overrides the builtin `host.*` attributes. Use nil to
// disable these attributes entirely.
func WithHost(d Detector) Option {
return hostOption{d}
}
type hostOption struct {
Detector
}
// Apply implements Option.
func (o hostOption) Apply(cfg *config) {
cfg.host = o.Detector
}
// WithFromEnv overrides the builtin detector for
// OTEL_RESOURCE_ATTRIBUTES. Use nil to disable environment checking.
func WithFromEnv(d Detector) Option {
return fromEnvOption{d}
}
type fromEnvOption struct {
Detector
}
// Apply implements Option.
func (o fromEnvOption) Apply(cfg *config) {
cfg.fromEnv = o.Detector
}
// WithoutBuiltin disables all the builtin detectors, including the
// telemetry.sdk.*, host.*, and the environment detector.
func WithoutBuiltin() Option {
return noBuiltinOption{}
}
type noBuiltinOption struct{}
// Apply implements Option.
func (o noBuiltinOption) Apply(cfg *config) {
cfg.host = nil
cfg.telemetrySDK = nil
cfg.fromEnv = nil
}
// New returns a Resource combined from the provided attributes,
// user-provided detectors and builtin detectors.
func New(ctx context.Context, opts ...Option) (*Resource, error) {
cfg := config{
telemetrySDK: TelemetrySDK{},
host: Host{},
fromEnv: FromEnv{},
}
for _, opt := range opts {
opt.Apply(&cfg)
}
detectors := append(
[]Detector{cfg.telemetrySDK, cfg.host, cfg.fromEnv},
cfg.detectors...,
)
return Detect(ctx, detectors...)
}

139
sdk/resource/config_test.go Normal file
View File

@ -0,0 +1,139 @@
// 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_test
import (
"context"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/require"
ottest "go.opentelemetry.io/otel/internal/testing"
"go.opentelemetry.io/otel/label"
opentelemetry "go.opentelemetry.io/otel/sdk"
"go.opentelemetry.io/otel/sdk/resource"
)
const envVar = "OTEL_RESOURCE_ATTRIBUTES"
func TestDefaultConfig(t *testing.T) {
store, err := ottest.SetEnvVariables(map[string]string{
envVar: "",
})
require.NoError(t, err)
defer func() { require.NoError(t, store.Restore()) }()
ctx := context.Background()
res, err := resource.New(ctx)
require.NoError(t, err)
require.EqualValues(t, map[string]string{
"host.name": hostname(),
"telemetry.sdk.name": "opentelemetry-go",
"telemetry.sdk.language": "go",
"telemetry.sdk.version": opentelemetry.Version(),
}, toMap(res))
}
func TestDefaultConfigNoHost(t *testing.T) {
store, err := ottest.SetEnvVariables(map[string]string{
envVar: "",
})
require.NoError(t, err)
defer func() { require.NoError(t, store.Restore()) }()
ctx := context.Background()
res, err := resource.New(ctx, resource.WithHost(nil))
require.NoError(t, err)
require.EqualValues(t, map[string]string{
"telemetry.sdk.name": "opentelemetry-go",
"telemetry.sdk.language": "go",
"telemetry.sdk.version": opentelemetry.Version(),
}, toMap(res))
}
func TestDefaultConfigNoEnv(t *testing.T) {
store, err := ottest.SetEnvVariables(map[string]string{
envVar: "from=here",
})
require.NoError(t, err)
defer func() { require.NoError(t, store.Restore()) }()
ctx := context.Background()
res, err := resource.New(ctx, resource.WithFromEnv(nil))
require.NoError(t, err)
require.EqualValues(t, map[string]string{
"host.name": hostname(),
"telemetry.sdk.name": "opentelemetry-go",
"telemetry.sdk.language": "go",
"telemetry.sdk.version": opentelemetry.Version(),
}, toMap(res))
}
func TestDefaultConfigWithEnv(t *testing.T) {
store, err := ottest.SetEnvVariables(map[string]string{
envVar: "key=value,other=attr",
})
require.NoError(t, err)
defer func() { require.NoError(t, store.Restore()) }()
ctx := context.Background()
res, err := resource.New(ctx)
require.NoError(t, err)
require.EqualValues(t, map[string]string{
"key": "value",
"other": "attr",
"host.name": hostname(),
"telemetry.sdk.name": "opentelemetry-go",
"telemetry.sdk.language": "go",
"telemetry.sdk.version": opentelemetry.Version(),
}, toMap(res))
}
func TestWithoutBuiltin(t *testing.T) {
store, err := ottest.SetEnvVariables(map[string]string{
envVar: "key=value,other=attr",
})
require.NoError(t, err)
defer func() { require.NoError(t, store.Restore()) }()
ctx := context.Background()
res, err := resource.New(
ctx,
resource.WithoutBuiltin(),
resource.WithAttributes(label.String("hello", "collector")),
)
require.NoError(t, err)
require.EqualValues(t, map[string]string{
"hello": "collector",
}, toMap(res))
}
func toMap(res *resource.Resource) map[string]string {
m := map[string]string{}
for _, attr := range res.Attributes() {
m[string(attr.Key)] = attr.Value.Emit()
}
return m
}
func hostname() string {
hn, err := os.Hostname()
if err != nil {
return fmt.Sprintf("hostname(%s)", err)
}
return hn
}

View File

@ -27,19 +27,22 @@ import (
const envVar = "OTEL_RESOURCE_ATTRIBUTES"
var (
//errMissingValue is returned when a resource value is missing.
// 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
// FromEnv is a Detector that implements the Detector and collects
// resources from environment. This Detector is included as a
// builtin. If these resource attributes are not wanted, use the
// WithFromEnv(nil) or WithoutBuiltin() options to explicitly disable
// them.
type FromEnv struct{}
// compile time assertion that FromEnv implements Detector interface
var _ Detector = (*FromEnv)(nil)
var _ Detector = FromEnv{}
// Detect collects resources from environment
func (d *FromEnv) Detect(context.Context) (*Resource, error) {
func (FromEnv) Detect(context.Context) (*Resource, error) {
labels := strings.TrimSpace(os.Getenv(envVar))
if labels == "" {
@ -65,5 +68,5 @@ func constructOTResources(s string) (*Resource, error) {
if len(invalid) > 0 {
err = fmt.Errorf("%w: %v", errMissingValue, invalid)
}
return New(labels...), err
return NewWithAttributes(labels...), err
}

View File

@ -17,32 +17,40 @@ package resource
import (
"context"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ottest "go.opentelemetry.io/otel/internal/testing"
"go.opentelemetry.io/otel/label"
)
func TestDetectOnePair(t *testing.T) {
os.Setenv(envVar, "key=value")
store, err := ottest.SetEnvVariables(map[string]string{
envVar: "key=value",
})
require.NoError(t, err)
defer func() { require.NoError(t, store.Restore()) }()
detector := &FromEnv{}
res, err := detector.Detect(context.Background())
require.NoError(t, err)
assert.Equal(t, New(label.String("key", "value")), res)
assert.Equal(t, NewWithAttributes(label.String("key", "value")), res)
}
func TestDetectMultiPairs(t *testing.T) {
os.Setenv("x", "1")
os.Setenv(envVar, "key=value, k = v , a= x, a=z")
store, err := ottest.SetEnvVariables(map[string]string{
"x": "1",
envVar: "key=value, k = v , a= x, a=z",
})
require.NoError(t, err)
defer func() { require.NoError(t, store.Restore()) }()
detector := &FromEnv{}
res, err := detector.Detect(context.Background())
require.NoError(t, err)
assert.Equal(t, res, New(
assert.Equal(t, res, NewWithAttributes(
label.String("key", "value"),
label.String("k", "v"),
label.String("a", "x"),
@ -51,7 +59,11 @@ func TestDetectMultiPairs(t *testing.T) {
}
func TestEmpty(t *testing.T) {
os.Setenv(envVar, " ")
store, err := ottest.SetEnvVariables(map[string]string{
envVar: " ",
})
require.NoError(t, err)
defer func() { require.NoError(t, store.Restore()) }()
detector := &FromEnv{}
res, err := detector.Detect(context.Background())
@ -60,13 +72,17 @@ func TestEmpty(t *testing.T) {
}
func TestMissingKeyError(t *testing.T) {
os.Setenv(envVar, "key=value,key")
store, err := ottest.SetEnvVariables(map[string]string{
envVar: "key=value,key",
})
require.NoError(t, err)
defer func() { require.NoError(t, store.Restore()) }()
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(
assert.Equal(t, res, NewWithAttributes(
label.String("key", "value"),
))
}

View File

@ -31,10 +31,10 @@ type Resource struct {
var emptyResource Resource
// New creates a resource from a set of attributes. If there are
// NewWithAttributes 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 ...label.KeyValue) *Resource {
func NewWithAttributes(kvs ...label.KeyValue) *Resource {
return &Resource{
labels: label.NewSet(kvs...),
}
@ -103,7 +103,7 @@ func Merge(a, b *Resource) *Resource {
for mi.Next() {
combine = append(combine, mi.Label())
}
return New(combine...)
return NewWithAttributes(combine...)
}
// Empty returns an instance of Resource with no attributes. It is

View File

@ -58,7 +58,7 @@ func TestNew(t *testing.T) {
}
for _, c := range cases {
t.Run(fmt.Sprintf("case-%s", c.name), func(t *testing.T) {
res := resource.New(c.in...)
res := resource.NewWithAttributes(c.in...)
if diff := cmp.Diff(
res.Attributes(),
c.want,
@ -77,61 +77,61 @@ func TestMerge(t *testing.T) {
}{
{
name: "Merge with no overlap, no nil",
a: resource.New(kv11, kv31),
b: resource.New(kv21, kv41),
a: resource.NewWithAttributes(kv11, kv31),
b: resource.NewWithAttributes(kv21, kv41),
want: []label.KeyValue{kv11, kv21, kv31, kv41},
},
{
name: "Merge with no overlap, no nil, not interleaved",
a: resource.New(kv11, kv21),
b: resource.New(kv31, kv41),
a: resource.NewWithAttributes(kv11, kv21),
b: resource.NewWithAttributes(kv31, kv41),
want: []label.KeyValue{kv11, kv21, kv31, kv41},
},
{
name: "Merge with common key order1",
a: resource.New(kv11),
b: resource.New(kv12, kv21),
a: resource.NewWithAttributes(kv11),
b: resource.NewWithAttributes(kv12, kv21),
want: []label.KeyValue{kv11, kv21},
},
{
name: "Merge with common key order2",
a: resource.New(kv12, kv21),
b: resource.New(kv11),
a: resource.NewWithAttributes(kv12, kv21),
b: resource.NewWithAttributes(kv11),
want: []label.KeyValue{kv12, kv21},
},
{
name: "Merge with common key order4",
a: resource.New(kv11, kv21, kv41),
b: resource.New(kv31, kv41),
a: resource.NewWithAttributes(kv11, kv21, kv41),
b: resource.NewWithAttributes(kv31, kv41),
want: []label.KeyValue{kv11, kv21, kv31, kv41},
},
{
name: "Merge with no keys",
a: resource.New(),
b: resource.New(),
a: resource.NewWithAttributes(),
b: resource.NewWithAttributes(),
want: nil,
},
{
name: "Merge with first resource no keys",
a: resource.New(),
b: resource.New(kv21),
a: resource.NewWithAttributes(),
b: resource.NewWithAttributes(kv21),
want: []label.KeyValue{kv21},
},
{
name: "Merge with second resource no keys",
a: resource.New(kv11),
b: resource.New(),
a: resource.NewWithAttributes(kv11),
b: resource.NewWithAttributes(),
want: []label.KeyValue{kv11},
},
{
name: "Merge with first resource nil",
a: nil,
b: resource.New(kv21),
b: resource.NewWithAttributes(kv21),
want: []label.KeyValue{kv21},
},
{
name: "Merge with second resource nil",
a: resource.New(kv11),
a: resource.NewWithAttributes(kv11),
b: nil,
want: []label.KeyValue{kv11},
},
@ -207,14 +207,14 @@ func TestString(t *testing.T) {
want: `A\=a\\\,B=b`,
},
} {
if got := resource.New(test.kvs...).String(); got != test.want {
if got := resource.NewWithAttributes(test.kvs...).String(); got != test.want {
t.Errorf("Resource(%v).String() = %q, want %q", test.kvs, got, test.want)
}
}
}
func TestMarshalJSON(t *testing.T) {
r := resource.New(label.Int64("A", 1), label.String("C", "D"))
r := resource.NewWithAttributes(label.Int64("A", 1), label.String("C", "D"))
data, err := json.Marshal(r)
require.NoError(t, err)
require.Equal(t,

View File

@ -1169,7 +1169,7 @@ func TestWithResource(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te),
WithConfig(Config{DefaultSampler: AlwaysSample()}),
WithResource(resource.New(label.String("rk1", "rv1"), label.Int64("rk2", 5))))
WithResource(resource.NewWithAttributes(label.String("rk1", "rv1"), label.Int64("rk2", 5))))
span := startSpan(tp, "WithResource")
span.SetAttributes(label.String("key1", "value1"))
got, err := endSpan(te, span)
@ -1189,7 +1189,7 @@ func TestWithResource(t *testing.T) {
},
SpanKind: otel.SpanKindInternal,
HasRemoteParent: true,
Resource: resource.New(label.String("rk1", "rv1"), label.Int64("rk2", 5)),
Resource: resource.NewWithAttributes(label.String("rk1", "rv1"), label.Int64("rk2", 5)),
InstrumentationLibrary: instrumentation.Library{Name: "WithResource"},
}
if diff := cmpDiff(got, want); diff != "" {