diff --git a/CHANGELOG.md b/CHANGELOG.md index efcd947d725..59f3e712c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/otel-collector/main.go b/example/otel-collector/main.go index 39aa5ba6860..acbe3373107 100644 --- a/example/otel-collector/main.go +++ b/example/otel-collector/main.go @@ -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 diff --git a/exporters/metric/prometheus/example_test.go b/exporters/metric/prometheus/example_test.go index 93d3093ebd3..ac0c85a74f8 100644 --- a/exporters/metric/prometheus/example_test.go +++ b/exporters/metric/prometheus/example_test.go @@ -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) diff --git a/exporters/metric/prometheus/prometheus_test.go b/exporters/metric/prometheus/prometheus_test.go index e6c238d6aae..6ec49eeeb76 100644 --- a/exporters/metric/prometheus/prometheus_test.go +++ b/exporters/metric/prometheus/prometheus_test.go @@ -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) diff --git a/exporters/otlp/internal/transform/metric_test.go b/exporters/otlp/internal/transform/metric_test.go index bdc848a646a..1d1bdb5b978 100644 --- a/exporters/otlp/internal/transform/metric_test.go +++ b/exporters/otlp/internal/transform/metric_test.go @@ -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)) } diff --git a/exporters/otlp/internal/transform/resource_test.go b/exporters/otlp/internal/transform/resource_test.go index 3d73150d52e..f1e35c9cc9b 100644 --- a/exporters/otlp/internal/transform/resource_test.go +++ b/exporters/otlp/internal/transform/resource_test.go @@ -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 } diff --git a/exporters/otlp/internal/transform/span_test.go b/exporters/otlp/internal/transform/span_test.go index 9612345e568..f8bd5e5e145 100644 --- a/exporters/otlp/internal/transform/span_test.go +++ b/exporters/otlp/internal/transform/span_test.go @@ -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", diff --git a/exporters/otlp/otlp_integration_test.go b/exporters/otlp/otlp_integration_test.go index 7cbe7d64999..84a0f3e8d87 100644 --- a/exporters/otlp/otlp_integration_test.go +++ b/exporters/otlp/otlp_integration_test.go @@ -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), )))...) diff --git a/exporters/otlp/otlp_metric_test.go b/exporters/otlp/otlp_metric_test.go index b288c605a55..c82b605edf0 100644 --- a/exporters/otlp/otlp_metric_test.go +++ b/exporters/otlp/otlp_metric_test.go @@ -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} diff --git a/exporters/otlp/otlp_span_test.go b/exporters/otlp/otlp_span_test.go index eb6ee72b467..27b75055454 100644 --- a/exporters/otlp/otlp_span_test.go +++ b/exporters/otlp/otlp_span_test.go @@ -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", diff --git a/exporters/stdout/metric_test.go b/exporters/stdout/metric_test.go index 456af2e7f06..33c2c02e58a 100644 --- a/exporters/stdout/metric_test.go +++ b/exporters/stdout/metric_test.go @@ -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")), } diff --git a/exporters/stdout/trace_test.go b/exporters/stdout/trace_test.go index 7ee977b85d9..2a1665545a2 100644 --- a/exporters/stdout/trace_test.go +++ b/exporters/stdout/trace_test.go @@ -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{ diff --git a/exporters/trace/jaeger/jaeger_test.go b/exporters/trace/jaeger/jaeger_test.go index 5489788bdee..2eaef9d045d 100644 --- a/exporters/trace/jaeger/jaeger_test.go +++ b/exporters/trace/jaeger/jaeger_test.go @@ -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, diff --git a/sdk/metric/benchmark_test.go b/sdk/metric/benchmark_test.go index 8c5d761a903..9b562c4fed7 100644 --- a/sdk/metric/benchmark_test.go +++ b/sdk/metric/benchmark_test.go @@ -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 } diff --git a/sdk/metric/config.go b/sdk/metric/config.go deleted file mode 100644 index 02773ca75e4..00000000000 --- a/sdk/metric/config.go +++ /dev/null @@ -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 -} diff --git a/sdk/metric/controller/pull/pull.go b/sdk/metric/controller/pull/pull.go index 3a4a388508d..8412075ff0e 100644 --- a/sdk/metric/controller/pull/pull.go +++ b/sdk/metric/controller/pull/pull.go @@ -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, diff --git a/sdk/metric/controller/push/config_test.go b/sdk/metric/controller/push/config_test.go index d7079511f0c..899d5ade41b 100644 --- a/sdk/metric/controller/push/config_test.go +++ b/sdk/metric/controller/push/config_test.go @@ -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) diff --git a/sdk/metric/controller/push/push.go b/sdk/metric/controller/push/push.go index 1261f0a0e33..ccba41faf20 100644 --- a/sdk/metric/controller/push/push.go +++ b/sdk/metric/controller/push/push.go @@ -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), diff --git a/sdk/metric/controller/push/push_test.go b/sdk/metric/controller/push/push_test.go index 22096214deb..ddb325a7bf3 100644 --- a/sdk/metric/controller/push/push_test.go +++ b/sdk/metric/controller/push/push_test.go @@ -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 diff --git a/sdk/metric/correct_test.go b/sdk/metric/correct_test.go index 1d1f113f17d..4f3aea6ca67 100644 --- a/sdk/metric/correct_test.go +++ b/sdk/metric/correct_test.go @@ -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 diff --git a/sdk/metric/processor/basic/basic_test.go b/sdk/metric/processor/basic/basic_test.go index a786ed79396..71e9d3870d4 100644 --- a/sdk/metric/processor/basic/basic_test.go +++ b/sdk/metric/processor/basic/basic_test.go @@ -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() diff --git a/sdk/metric/processor/processortest/test_test.go b/sdk/metric/processor/processortest/test_test.go index 8bd5a2f6e81..d7ab2eee872 100644 --- a/sdk/metric/processor/processortest/test_test.go +++ b/sdk/metric/processor/processortest/test_test.go @@ -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") diff --git a/sdk/metric/processor/reducer/reducer_test.go b/sdk/metric/processor/reducer/reducer_test.go index 24720910607..6c0a87301d2 100644 --- a/sdk/metric/processor/reducer/reducer_test.go +++ b/sdk/metric/processor/reducer/reducer_test.go @@ -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()) diff --git a/sdk/metric/sdk.go b/sdk/metric/sdk.go index dc7e1a8f23f..b21ad5f5f54 100644 --- a/sdk/metric/sdk.go +++ b/sdk/metric/sdk.go @@ -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, } } diff --git a/sdk/metric/stress_test.go b/sdk/metric/stress_test.go index 710ce43bc56..c43e6f528f4 100644 --- a/sdk/metric/stress_test.go +++ b/sdk/metric/stress_test.go @@ -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) diff --git a/sdk/resource/auto.go b/sdk/resource/auto.go index 1e572e0a3f8..4f8d366d43b 100644 --- a/sdk/resource/auto.go +++ b/sdk/resource/auto.go @@ -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()) diff --git a/sdk/resource/benchmark_test.go b/sdk/resource/benchmark_test.go index bc6b4427dc7..ddb64bbe731 100644 --- a/sdk/resource/benchmark_test.go +++ b/sdk/resource/benchmark_test.go @@ -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) { diff --git a/sdk/resource/builtin.go b/sdk/resource/builtin.go new file mode 100644 index 00000000000..9af78cf1d03 --- /dev/null +++ b/sdk/resource/builtin.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 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 +} diff --git a/sdk/resource/builtin_test.go b/sdk/resource/builtin_test.go new file mode 100644 index 00000000000..dad4b656b85 --- /dev/null +++ b/sdk/resource/builtin_test.go @@ -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) +} diff --git a/sdk/resource/config.go b/sdk/resource/config.go new file mode 100644 index 00000000000..2949ba06f33 --- /dev/null +++ b/sdk/resource/config.go @@ -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...) +} diff --git a/sdk/resource/config_test.go b/sdk/resource/config_test.go new file mode 100644 index 00000000000..869564f70b0 --- /dev/null +++ b/sdk/resource/config_test.go @@ -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 +} diff --git a/sdk/resource/env.go b/sdk/resource/env.go index e0fb72516c7..383b7b1df2c 100644 --- a/sdk/resource/env.go +++ b/sdk/resource/env.go @@ -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 } diff --git a/sdk/resource/env_test.go b/sdk/resource/env_test.go index 66c1a123548..622fb147838 100644 --- a/sdk/resource/env_test.go +++ b/sdk/resource/env_test.go @@ -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"), )) } diff --git a/sdk/resource/resource.go b/sdk/resource/resource.go index b03d2b4072f..61956c27346 100644 --- a/sdk/resource/resource.go +++ b/sdk/resource/resource.go @@ -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 diff --git a/sdk/resource/resource_test.go b/sdk/resource/resource_test.go index e3e36482efe..74f97e236b5 100644 --- a/sdk/resource/resource_test.go +++ b/sdk/resource/resource_test.go @@ -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, diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index 03f797a8325..f12ab6db3f8 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -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 != "" {