diff --git a/CHANGELOG.md b/CHANGELOG.md index 597f780f08e..42f279256f3 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 Creates package `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc` implementing a gRPC `otlptrace.Client` and offers convenience functions, `NewExportPipeline` and `InstallNewPipeline`, to setup and install a `otlptrace.Exporter` in tracing .(#1922) - The `OTEL_SERVICE_NAME` environment variable is the preferred source for `service.name`, used by the environment resource detector if a service name is present both there and in `OTEL_RESOURCE_ATTRIBUTES`. (#1969) - Creates package `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` implementing a HTTP `otlptrace.Client` and offers convenience functions, `NewExportPipeline` and `InstallNewPipeline`, to setup and install a `otlptrace.Exporter` in tracing. (#1963) +- Changes `go.opentelemetry.io/otel/sdk/resource.NewWithAttributes` to require a schema URL. The old function is still available as `resource.NewSchemaless`. This is a breaking change. (#1938) +- Several builtin resource detectors now correctly populate the schema URL. (#1938) ### Changed diff --git a/bridge/opencensus/exporter.go b/bridge/opencensus/exporter.go index ca12f283a60..a1723f45734 100644 --- a/bridge/opencensus/exporter.go +++ b/bridge/opencensus/exporter.go @@ -126,7 +126,7 @@ func convertResource(res *ocresource.Resource) *resource.Resource { for k, v := range res.Labels { labels = append(labels, attribute.KeyValue{Key: attribute.Key(k), Value: attribute.StringValue(v)}) } - return resource.NewWithAttributes(labels...) + return resource.NewSchemaless(labels...) } // convertDescriptor converts an OpenCensus Descriptor to an OpenTelemetry Descriptor diff --git a/bridge/opencensus/exporter_test.go b/bridge/opencensus/exporter_test.go index 766f8a10140..713d86e3507 100644 --- a/bridge/opencensus/exporter_test.go +++ b/bridge/opencensus/exporter_test.go @@ -155,7 +155,7 @@ func TestExportMetrics(t *testing.T) { export.NewRecord( &basicDesc, attribute.EmptySet(), - resource.NewWithAttributes(), + resource.NewSchemaless(), &ocExactAggregator{ points: []aggregation.Point{ { @@ -187,7 +187,7 @@ func TestExportMetrics(t *testing.T) { export.NewRecord( &basicDesc, attribute.EmptySet(), - resource.NewWithAttributes(), + resource.NewSchemaless(), &ocExactAggregator{ points: []aggregation.Point{ { @@ -222,7 +222,7 @@ func TestExportMetrics(t *testing.T) { export.NewRecord( &basicDesc, attribute.EmptySet(), - resource.NewWithAttributes(), + resource.NewSchemaless(), &ocExactAggregator{ points: []aggregation.Point{ { @@ -349,7 +349,7 @@ func TestConvertResource(t *testing.T) { input: &ocresource.Resource{ Labels: map[string]string{}, }, - expected: resource.NewWithAttributes(), + expected: resource.NewSchemaless(), }, { desc: "resource with labels", @@ -359,7 +359,7 @@ func TestConvertResource(t *testing.T) { "tick": "tock", }, }, - expected: resource.NewWithAttributes( + expected: resource.NewSchemaless( attribute.KeyValue{Key: attribute.Key("foo"), Value: attribute.StringValue("bar")}, attribute.KeyValue{Key: attribute.Key("tick"), Value: attribute.StringValue("tock")}, ), diff --git a/example/jaeger/main.go b/example/jaeger/main.go index de3ea1b5f41..8afb2aa3ec7 100644 --- a/example/jaeger/main.go +++ b/example/jaeger/main.go @@ -51,6 +51,7 @@ func tracerProvider(url string) (*tracesdk.TracerProvider, error) { tracesdk.WithBatcher(exp), // Record information about this application in an Resource. tracesdk.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, semconv.ServiceNameKey.String(service), attribute.String("environment", environment), attribute.Int64("ID", id), diff --git a/example/zipkin/main.go b/example/zipkin/main.go index f299b4bc233..fef6c44aca3 100644 --- a/example/zipkin/main.go +++ b/example/zipkin/main.go @@ -54,6 +54,7 @@ func initTracer(url string) func() { tp := sdktrace.NewTracerProvider( sdktrace.WithSpanProcessor(batcher), sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, semconv.ServiceNameKey.String("zipkin-test"), )), ) diff --git a/exporters/metric/prometheus/prometheus_test.go b/exporters/metric/prometheus/prometheus_test.go index 1c4d895b5e2..95f55f7083d 100644 --- a/exporters/metric/prometheus/prometheus_test.go +++ b/exporters/metric/prometheus/prometheus_test.go @@ -84,7 +84,7 @@ func TestPrometheusExporter(t *testing.T) { DefaultHistogramBoundaries: []float64{-0.5, 1}, }, controller.WithCollectPeriod(0), - controller.WithResource(resource.NewWithAttributes(attribute.String("R", "V"))), + controller.WithResource(resource.NewSchemaless(attribute.String("R", "V"))), ) require.NoError(t, err) diff --git a/exporters/otlp/internal/otlptest/data.go b/exporters/otlp/internal/otlptest/data.go index 08f5dc38d00..9d8c0455707 100644 --- a/exporters/otlp/internal/otlptest/data.go +++ b/exporters/otlp/internal/otlptest/data.go @@ -67,7 +67,7 @@ func (OneRecordCheckpointSet) ForEach(kindSelector exportmetric.ExportKindSelect metric.CounterInstrumentKind, number.Int64Kind, ) - res := resource.NewWithAttributes(attribute.String("a", "b")) + res := resource.NewSchemaless(attribute.String("a", "b")) agg := sum.New(1) if err := agg[0].Update(context.Background(), number.NewInt64Number(42), &desc); err != nil { return err @@ -106,7 +106,7 @@ func SingleReadOnlySpan() []tracesdk.ReadOnlySpan { DroppedEvents: 0, DroppedLinks: 0, ChildSpanCount: 0, - Resource: resource.NewWithAttributes(attribute.String("a", "b")), + Resource: resource.NewSchemaless(attribute.String("a", "b")), InstrumentationLibrary: instrumentation.Library{ Name: "bar", Version: "0.0.0", diff --git a/exporters/otlp/internal/otlptest/otlptest.go b/exporters/otlp/internal/otlptest/otlptest.go index e28e497bd21..bc5d4d801e3 100644 --- a/exporters/otlp/internal/otlptest/otlptest.go +++ b/exporters/otlp/internal/otlptest/otlptest.go @@ -50,13 +50,13 @@ func RunEndToEndTest(ctx context.Context, t *testing.T, exp *otlp.Exporter, mcTr ), } tp1 := sdktrace.NewTracerProvider(append(pOpts, - sdktrace.WithResource(resource.NewWithAttributes( + sdktrace.WithResource(resource.NewSchemaless( attribute.String("rk1", "rv11)"), attribute.Int64("rk2", 5), )))...) tp2 := sdktrace.NewTracerProvider(append(pOpts, - sdktrace.WithResource(resource.NewWithAttributes( + sdktrace.WithResource(resource.NewSchemaless( attribute.String("rk1", "rv12)"), attribute.Float64("rk3", 6.5), )))...) diff --git a/exporters/otlp/internal/transform/metric.go b/exporters/otlp/internal/transform/metric.go index 460fe0279e4..518ae8e6575 100644 --- a/exporters/otlp/internal/transform/metric.go +++ b/exporters/otlp/internal/transform/metric.go @@ -167,6 +167,7 @@ func sink(ctx context.Context, in <-chan result) ([]*metricpb.ResourceMetrics, e Resource *resourcepb.Resource // Group by instrumentation library name and then the MetricDescriptor. InstrumentationLibraryBatches map[instrumentation.Library]map[string]*metricpb.Metric + SchemaURL string } // group by unique Resource string. @@ -184,6 +185,9 @@ func sink(ctx context.Context, in <-chan result) ([]*metricpb.ResourceMetrics, e Resource: Resource(res.Resource), InstrumentationLibraryBatches: make(map[instrumentation.Library]map[string]*metricpb.Metric), } + if res.Resource != nil { + rb.SchemaURL = res.Resource.SchemaURL() + } grouped[rID] = rb } @@ -220,6 +224,7 @@ func sink(ctx context.Context, in <-chan result) ([]*metricpb.ResourceMetrics, e var rms []*metricpb.ResourceMetrics for _, rb := range grouped { + // TODO: populate ResourceMetrics.SchemaURL when the field is added to the Protobuf message. rm := &metricpb.ResourceMetrics{Resource: rb.Resource} for il, mb := range rb.InstrumentationLibraryBatches { ilm := &metricpb.InstrumentationLibraryMetrics{ diff --git a/exporters/otlp/internal/transform/resource_test.go b/exporters/otlp/internal/transform/resource_test.go index f56b56b6a9f..679e4ecb267 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 := []attribute.KeyValue{attribute.Int("one", 1), attribute.Int("two", 2)} - got := Resource(resource.NewWithAttributes(attrs...)).GetAttributes() + got := Resource(resource.NewSchemaless(attrs...)).GetAttributes() if !assert.Len(t, attrs, 2) { return } diff --git a/exporters/otlp/internal/transform/span.go b/exporters/otlp/internal/transform/span.go index 7bcdeeedfdc..1431e350a4c 100644 --- a/exporters/otlp/internal/transform/span.go +++ b/exporters/otlp/internal/transform/span.go @@ -75,6 +75,7 @@ func Spans(sdl []tracesdk.ReadOnlySpan) []*tracepb.ResourceSpans { Resource: Resource(sd.Resource()), InstrumentationLibrarySpans: []*tracepb.InstrumentationLibrarySpans{ils}, } + // TODO: populate ResourceSpans.SchemaURL when the field is added to the Protobuf message. rsm[rKey] = rs continue } diff --git a/exporters/otlp/internal/transform/span_test.go b/exporters/otlp/internal/transform/span_test.go index 74ed36b8990..dd4311785df 100644 --- a/exporters/otlp/internal/transform/span_test.go +++ b/exporters/otlp/internal/transform/span_test.go @@ -261,7 +261,7 @@ func TestSpanData(t *testing.T) { DroppedAttributes: 1, DroppedEvents: 2, DroppedLinks: 3, - Resource: resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)), + Resource: resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)), InstrumentationLibrary: instrumentation.Library{ Name: "go.opentelemetry.io/test/otel", Version: "v0.0.1", diff --git a/exporters/otlp/otlp_metric_test.go b/exporters/otlp/otlp_metric_test.go index b5778f2f1b3..ab5abf985e8 100644 --- a/exporters/otlp/otlp_metric_test.go +++ b/exporters/otlp/otlp_metric_test.go @@ -80,8 +80,8 @@ var ( baseKeyValues = []attribute.KeyValue{attribute.String("host", "test.com")} cpuKey = attribute.Key("CPU") - testInstA = resource.NewWithAttributes(attribute.String("instance", "tester-a")) - testInstB = resource.NewWithAttributes(attribute.String("instance", "tester-b")) + testInstA = resource.NewSchemaless(attribute.String("instance", "tester-a")) + testInstB = resource.NewSchemaless(attribute.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 1d4905180df..5c379fb7cab 100644 --- a/exporters/otlp/otlp_span_test.go +++ b/exporters/otlp/otlp_span_test.go @@ -73,7 +73,7 @@ func TestExportSpans(t *testing.T) { Code: codes.Ok, Description: "Ok", }, - Resource: resource.NewWithAttributes(attribute.String("instance", "tester-a")), + Resource: resource.NewSchemaless(attribute.String("instance", "tester-a")), InstrumentationLibrary: instrumentation.Library{ Name: "lib-a", Version: "v0.1.0", @@ -97,7 +97,7 @@ func TestExportSpans(t *testing.T) { Code: codes.Ok, Description: "Ok", }, - Resource: resource.NewWithAttributes(attribute.String("instance", "tester-a")), + Resource: resource.NewSchemaless(attribute.String("instance", "tester-a")), InstrumentationLibrary: instrumentation.Library{ Name: "lib-b", Version: "v0.1.0", @@ -126,7 +126,7 @@ func TestExportSpans(t *testing.T) { Code: codes.Ok, Description: "Ok", }, - Resource: resource.NewWithAttributes(attribute.String("instance", "tester-a")), + Resource: resource.NewSchemaless(attribute.String("instance", "tester-a")), InstrumentationLibrary: instrumentation.Library{ Name: "lib-a", Version: "v0.1.0", @@ -150,7 +150,7 @@ func TestExportSpans(t *testing.T) { Code: codes.Error, Description: "Unauthenticated", }, - Resource: resource.NewWithAttributes(attribute.String("instance", "tester-b")), + Resource: resource.NewSchemaless(attribute.String("instance", "tester-b")), InstrumentationLibrary: instrumentation.Library{ Name: "lib-a", Version: "v1.1.0", diff --git a/exporters/otlp/otlptrace/internal/otlptracetest/data.go b/exporters/otlp/otlptrace/internal/otlptracetest/data.go index 6ff1544f075..71708cd7393 100644 --- a/exporters/otlp/otlptrace/internal/otlptracetest/data.go +++ b/exporters/otlp/otlptrace/internal/otlptracetest/data.go @@ -53,7 +53,7 @@ func SingleReadOnlySpan() []tracesdk.ReadOnlySpan { DroppedEvents: 0, DroppedLinks: 0, ChildSpanCount: 0, - Resource: resource.NewWithAttributes(attribute.String("a", "b")), + Resource: resource.NewSchemaless(attribute.String("a", "b")), InstrumentationLibrary: instrumentation.Library{ Name: "bar", Version: "0.0.0", diff --git a/exporters/otlp/otlptrace/internal/otlptracetest/otlptest.go b/exporters/otlp/otlptrace/internal/otlptracetest/otlptest.go index d68fe9c1302..28b10db6973 100644 --- a/exporters/otlp/otlptrace/internal/otlptracetest/otlptest.go +++ b/exporters/otlp/otlptrace/internal/otlptracetest/otlptest.go @@ -40,13 +40,13 @@ func RunEndToEndTest(ctx context.Context, t *testing.T, exp *otlptrace.Exporter, ), } tp1 := sdktrace.NewTracerProvider(append(pOpts, - sdktrace.WithResource(resource.NewWithAttributes( + sdktrace.WithResource(resource.NewSchemaless( attribute.String("rk1", "rv11)"), attribute.Int64("rk2", 5), )))...) tp2 := sdktrace.NewTracerProvider(append(pOpts, - sdktrace.WithResource(resource.NewWithAttributes( + sdktrace.WithResource(resource.NewSchemaless( attribute.String("rk1", "rv12)"), attribute.Float64("rk3", 6.5), )))...) diff --git a/exporters/otlp/otlptrace/internal/tracetransform/resource_test.go b/exporters/otlp/otlptrace/internal/tracetransform/resource_test.go index 24eb06a9dc7..57949d0a9b4 100644 --- a/exporters/otlp/otlptrace/internal/tracetransform/resource_test.go +++ b/exporters/otlp/otlptrace/internal/tracetransform/resource_test.go @@ -40,7 +40,7 @@ func TestEmptyResource(t *testing.T) { func TestResourceAttributes(t *testing.T) { attrs := []attribute.KeyValue{attribute.Int("one", 1), attribute.Int("two", 2)} - got := Resource(resource.NewWithAttributes(attrs...)).GetAttributes() + got := Resource(resource.NewSchemaless(attrs...)).GetAttributes() if !assert.Len(t, attrs, 2) { return } diff --git a/exporters/otlp/otlptrace/internal/tracetransform/span_test.go b/exporters/otlp/otlptrace/internal/tracetransform/span_test.go index a19bec22d22..cd0f8feff2f 100644 --- a/exporters/otlp/otlptrace/internal/tracetransform/span_test.go +++ b/exporters/otlp/otlptrace/internal/tracetransform/span_test.go @@ -262,7 +262,7 @@ func TestSpanData(t *testing.T) { DroppedAttributes: 1, DroppedEvents: 2, DroppedLinks: 3, - Resource: resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)), + Resource: resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)), InstrumentationLibrary: instrumentation.Library{ Name: "go.opentelemetry.io/test/otel", Version: "v0.0.1", diff --git a/exporters/stdout/metric_test.go b/exporters/stdout/metric_test.go index af390a73c04..7e5318443ad 100644 --- a/exporters/stdout/metric_test.go +++ b/exporters/stdout/metric_test.go @@ -46,7 +46,7 @@ type testFixture struct { output *bytes.Buffer } -var testResource = resource.NewWithAttributes(attribute.String("R", "V")) +var testResource = resource.NewSchemaless(attribute.String("R", "V")) func newFixture(t *testing.T, opts ...stdout.Option) testFixture { buf := &bytes.Buffer{} @@ -270,11 +270,11 @@ func TestStdoutResource(t *testing.T) { } testCases := []testCase{ newCase("R1=V1,R2=V2,A=B,C=D", - resource.NewWithAttributes(attribute.String("R1", "V1"), attribute.String("R2", "V2")), + resource.NewSchemaless(attribute.String("R1", "V1"), attribute.String("R2", "V2")), attribute.String("A", "B"), attribute.String("C", "D")), newCase("R1=V1,R2=V2", - resource.NewWithAttributes(attribute.String("R1", "V1"), attribute.String("R2", "V2")), + resource.NewSchemaless(attribute.String("R1", "V1"), attribute.String("R2", "V2")), ), newCase("A=B,C=D", nil, @@ -284,7 +284,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.NewWithAttributes(attribute.String("R1", "V1"), attribute.String("R2", "V2")), + resource.NewSchemaless(attribute.String("R1", "V1"), attribute.String("R2", "V2")), attribute.String("R1", "V3"), attribute.String("R2", "V4")), } diff --git a/exporters/stdout/trace_test.go b/exporters/stdout/trace_test.go index fea48d6584c..748bd6213e6 100644 --- a/exporters/stdout/trace_test.go +++ b/exporters/stdout/trace_test.go @@ -49,7 +49,7 @@ func TestExporter_ExportSpan(t *testing.T) { traceState, _ := oteltest.TraceStateFromKeyValues(attribute.String("key", "val")) keyValue := "value" doubleValue := 123.456 - resource := resource.NewWithAttributes(attribute.String("rk1", "rv11")) + resource := resource.NewSchemaless(attribute.String("rk1", "rv11")) ro := tracetest.SpanStubs{ { diff --git a/exporters/trace/jaeger/jaeger_test.go b/exporters/trace/jaeger/jaeger_test.go index 6d0d035c908..3eeb48ad0ee 100644 --- a/exporters/trace/jaeger/jaeger_test.go +++ b/exporters/trace/jaeger/jaeger_test.go @@ -166,7 +166,7 @@ func TestExporterExportSpan(t *testing.T) { require.NoError(t, err) tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp), - sdktrace.WithResource(resource.NewWithAttributes( + sdktrace.WithResource(resource.NewSchemaless( semconv.ServiceNameKey.String(serviceName), attribute.String(tagKey, tagVal), )), @@ -421,7 +421,7 @@ func Test_spanSnapshotToThrift(t *testing.T) { Name: "/foo", StartTime: now, EndTime: now, - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( attribute.String("rk1", rv1), attribute.Int64("rk2", rv2), semconv.ServiceNameKey.String("service name"), @@ -500,7 +500,7 @@ func TestExporterExportSpansHonorsCancel(t *testing.T) { ss := tracetest.SpanStubs{ { Name: "s1", - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( semconv.ServiceNameKey.String("name"), attribute.Key("r1").String("v1"), ), @@ -509,7 +509,7 @@ func TestExporterExportSpansHonorsCancel(t *testing.T) { }, { Name: "s2", - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( semconv.ServiceNameKey.String("name"), attribute.Key("r2").String("v2"), ), @@ -530,7 +530,7 @@ func TestExporterExportSpansHonorsTimeout(t *testing.T) { ss := tracetest.SpanStubs{ { Name: "s1", - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( semconv.ServiceNameKey.String("name"), attribute.Key("r1").String("v1"), ), @@ -539,7 +539,7 @@ func TestExporterExportSpansHonorsTimeout(t *testing.T) { }, { Name: "s2", - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( semconv.ServiceNameKey.String("name"), attribute.Key("r2").String("v2"), ), @@ -577,7 +577,7 @@ func TestJaegerBatchList(t *testing.T) { roSpans: []sdktrace.ReadOnlySpan{ tracetest.SpanStub{ Name: "s1", - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( semconv.ServiceNameKey.String("name"), attribute.Key("r1").String("v1"), ), @@ -611,7 +611,7 @@ func TestJaegerBatchList(t *testing.T) { roSpans: tracetest.SpanStubs{ { Name: "s1", - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( semconv.ServiceNameKey.String("name"), attribute.Key("r1").String("v1"), ), @@ -620,7 +620,7 @@ func TestJaegerBatchList(t *testing.T) { }, { Name: "s2", - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( semconv.ServiceNameKey.String("name"), attribute.Key("r1").String("v1"), ), @@ -629,7 +629,7 @@ func TestJaegerBatchList(t *testing.T) { }, { Name: "s3", - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( semconv.ServiceNameKey.String("name"), attribute.Key("r2").String("v2"), ), @@ -686,7 +686,7 @@ func TestJaegerBatchList(t *testing.T) { roSpans: tracetest.SpanStubs{ { Name: "s1", - Resource: resource.NewWithAttributes( + Resource: resource.NewSchemaless( attribute.Key("r1").String("v1"), ), StartTime: now, @@ -736,7 +736,7 @@ func TestProcess(t *testing.T) { }{ { name: "resources contain service name", - res: resource.NewWithAttributes( + res: resource.NewSchemaless( semconv.ServiceNameKey.String("service name"), attribute.Key("r1").String("v1"), ), @@ -750,7 +750,7 @@ func TestProcess(t *testing.T) { }, { name: "resources don't have service name", - res: resource.NewWithAttributes(attribute.Key("r1").String("v1")), + res: resource.NewSchemaless(attribute.Key("r1").String("v1")), defaultServiceName: "default service name", expectedProcess: &gen.Process{ ServiceName: "default service name", diff --git a/exporters/trace/zipkin/model_test.go b/exporters/trace/zipkin/model_test.go index c4e68f1128f..2d95211fa98 100644 --- a/exporters/trace/zipkin/model_test.go +++ b/exporters/trace/zipkin/model_test.go @@ -37,7 +37,7 @@ import ( ) func TestModelConversion(t *testing.T) { - resource := resource.NewWithAttributes( + resource := resource.NewSchemaless( semconv.ServiceNameKey.String("model-test"), ) diff --git a/exporters/trace/zipkin/zipkin_test.go b/exporters/trace/zipkin/zipkin_test.go index be7cf2ae454..a611a1018a7 100644 --- a/exporters/trace/zipkin/zipkin_test.go +++ b/exporters/trace/zipkin/zipkin_test.go @@ -230,7 +230,7 @@ func logStoreLogger(s *logStore) *log.Logger { } func TestExportSpans(t *testing.T) { - resource := resource.NewWithAttributes( + resource := resource.NewSchemaless( semconv.ServiceNameKey.String("exporter-test"), ) @@ -400,7 +400,7 @@ func TestNewExportPipelineWithOptions(t *testing.T) { tp, err := NewExportPipeline(collector.url, WithSDKOptions( - sdktrace.WithResource(resource.NewWithAttributes( + sdktrace.WithResource(resource.NewSchemaless( semconv.ServiceNameKey.String("zipkin-test"), )), sdktrace.WithSpanLimits(sdktrace.SpanLimits{ diff --git a/sdk/metric/controller/basic/config.go b/sdk/metric/controller/basic/config.go index 4e16afa90cc..77b5b9024e7 100644 --- a/sdk/metric/controller/basic/config.go +++ b/sdk/metric/controller/basic/config.go @@ -17,6 +17,7 @@ package basic // import "go.opentelemetry.io/otel/sdk/metric/controller/basic" import ( "time" + "go.opentelemetry.io/otel" export "go.opentelemetry.io/otel/sdk/export/metric" "go.opentelemetry.io/otel/sdk/resource" ) @@ -67,7 +68,10 @@ type Option interface { // WithResource sets the Resource configuration option of a Config by merging it // with the Resource configuration in the environment. func WithResource(r *resource.Resource) Option { - res := resource.Merge(resource.Environment(), r) + res, err := resource.Merge(resource.Environment(), r) + if err != nil { + otel.Handle(err) + } return resourceOption{res} } diff --git a/sdk/metric/controller/basic/config_test.go b/sdk/metric/controller/basic/config_test.go index 8bbf07de81e..d93cc53a677 100644 --- a/sdk/metric/controller/basic/config_test.go +++ b/sdk/metric/controller/basic/config_test.go @@ -24,7 +24,7 @@ import ( ) func TestWithResource(t *testing.T) { - r := resource.NewWithAttributes(attribute.String("A", "a")) + r := resource.NewSchemaless(attribute.String("A", "a")) c := &config{} WithResource(r).apply(c) diff --git a/sdk/metric/controller/basic/controller_test.go b/sdk/metric/controller/basic/controller_test.go index ae4725546cf..37772e3c294 100644 --- a/sdk/metric/controller/basic/controller_test.go +++ b/sdk/metric/controller/basic/controller_test.go @@ -82,18 +82,18 @@ func TestControllerUsesResource(t *testing.T) { wanted: resource.Default().Encoded(attribute.DefaultEncoder())}, { name: "explicit resource", - options: []controller.Option{controller.WithResource(resource.NewWithAttributes(attribute.String("R", "S")))}, + options: []controller.Option{controller.WithResource(resource.NewSchemaless(attribute.String("R", "S")))}, wanted: "R=S,T=U,key=value"}, { name: "last resource wins", options: []controller.Option{ controller.WithResource(resource.Default()), - controller.WithResource(resource.NewWithAttributes(attribute.String("R", "S"))), + controller.WithResource(resource.NewSchemaless(attribute.String("R", "S"))), }, wanted: "R=S,T=U,key=value"}, { name: "overlapping attributes with environment resource", - options: []controller.Option{controller.WithResource(resource.NewWithAttributes(attribute.String("T", "V")))}, + options: []controller.Option{controller.WithResource(resource.NewSchemaless(attribute.String("T", "V")))}, wanted: "T=V,key=value"}, } for _, c := range cases { diff --git a/sdk/metric/controller/basic/push_test.go b/sdk/metric/controller/basic/push_test.go index 8dd58ded46d..249fe41a8cc 100644 --- a/sdk/metric/controller/basic/push_test.go +++ b/sdk/metric/controller/basic/push_test.go @@ -37,7 +37,7 @@ import ( "go.opentelemetry.io/otel/sdk/resource" ) -var testResource = resource.NewWithAttributes(attribute.String("R", "V")) +var testResource = resource.NewSchemaless(attribute.String("R", "V")) type handler struct { sync.Mutex diff --git a/sdk/metric/correct_test.go b/sdk/metric/correct_test.go index 1b34e8bcdcf..c3355497d3c 100644 --- a/sdk/metric/correct_test.go +++ b/sdk/metric/correct_test.go @@ -35,7 +35,7 @@ import ( ) var Must = metric.Must -var testResource = resource.NewWithAttributes(attribute.String("R", "V")) +var testResource = resource.NewSchemaless(attribute.String("R", "V")) type handler struct { sync.Mutex diff --git a/sdk/metric/processor/basic/basic_test.go b/sdk/metric/processor/basic/basic_test.go index 599f6b8fb38..6b207caa5e5 100644 --- a/sdk/metric/processor/basic/basic_test.go +++ b/sdk/metric/processor/basic/basic_test.go @@ -126,7 +126,7 @@ func testProcessor( // Note: this selector uses the instrument name to dictate // aggregation kind. selector := processorTest.AggregatorSelector() - res := resource.NewWithAttributes(attribute.String("R", "V")) + res := resource.NewSchemaless(attribute.String("R", "V")) labs1 := []attribute.KeyValue{attribute.String("L1", "V")} labs2 := []attribute.KeyValue{attribute.String("L2", "V")} @@ -368,7 +368,7 @@ func TestBasicTimestamps(t *testing.T) { } func TestStatefulNoMemoryCumulative(t *testing.T) { - res := resource.NewWithAttributes(attribute.String("R", "V")) + res := resource.NewSchemaless(attribute.String("R", "V")) ekindSel := export.CumulativeExportKindSelector() desc := metric.NewDescriptor("inst.sum", metric.CounterInstrumentKind, number.Int64Kind) @@ -402,7 +402,7 @@ func TestStatefulNoMemoryCumulative(t *testing.T) { } func TestStatefulNoMemoryDelta(t *testing.T) { - res := resource.NewWithAttributes(attribute.String("R", "V")) + res := resource.NewSchemaless(attribute.String("R", "V")) ekindSel := export.DeltaExportKindSelector() desc := metric.NewDescriptor("inst.sum", metric.SumObserverInstrumentKind, number.Int64Kind) @@ -441,7 +441,7 @@ func TestMultiObserverSum(t *testing.T) { export.DeltaExportKindSelector(), } { - res := resource.NewWithAttributes(attribute.String("R", "V")) + res := resource.NewSchemaless(attribute.String("R", "V")) desc := metric.NewDescriptor("observe.sum", metric.SumObserverInstrumentKind, number.Int64Kind) selector := processorTest.AggregatorSelector() diff --git a/sdk/metric/processor/processortest/test_test.go b/sdk/metric/processor/processortest/test_test.go index 771a3c65d8e..42ec361f393 100644 --- a/sdk/metric/processor/processortest/test_test.go +++ b/sdk/metric/processor/processortest/test_test.go @@ -32,7 +32,7 @@ func generateTestData(proc export.Processor) { ctx := context.Background() accum := metricsdk.NewAccumulator( proc, - resource.NewWithAttributes(attribute.String("R", "V")), + resource.NewSchemaless(attribute.String("R", "V")), ) meter := metric.WrapMeterImpl(accum, "testing") diff --git a/sdk/metric/processor/reducer/reducer_test.go b/sdk/metric/processor/reducer/reducer_test.go index d43fa630395..09912c93da3 100644 --- a/sdk/metric/processor/reducer/reducer_test.go +++ b/sdk/metric/processor/reducer/reducer_test.go @@ -75,7 +75,7 @@ func TestFilterProcessor(t *testing.T) { ) accum := metricsdk.NewAccumulator( reducer.New(testFilter{}, processorTest.Checkpointer(testProc)), - resource.NewWithAttributes(attribute.String("R", "V")), + resource.NewSchemaless(attribute.String("R", "V")), ) generateData(accum) @@ -92,7 +92,7 @@ func TestFilterBasicProcessor(t *testing.T) { basicProc := basic.New(processorTest.AggregatorSelector(), export.CumulativeExportKindSelector()) accum := metricsdk.NewAccumulator( reducer.New(testFilter{}, basicProc), - resource.NewWithAttributes(attribute.String("R", "V")), + resource.NewSchemaless(attribute.String("R", "V")), ) exporter := processorTest.NewExporter(basicProc, attribute.DefaultEncoder()) diff --git a/sdk/resource/auto.go b/sdk/resource/auto.go index c754e221eea..b1c9b302d21 100644 --- a/sdk/resource/auto.go +++ b/sdk/resource/auto.go @@ -53,7 +53,10 @@ func Detect(ctx context.Context, detectors ...Detector) (*Resource, error) { continue } } - autoDetectedRes = Merge(autoDetectedRes, res) + autoDetectedRes, err = Merge(autoDetectedRes, res) + if err != nil { + errInfo = append(errInfo, err.Error()) + } } var aggregatedError error diff --git a/sdk/resource/auto_test.go b/sdk/resource/auto_test.go new file mode 100644 index 00000000000..a12dca40b12 --- /dev/null +++ b/sdk/resource/auto_test.go @@ -0,0 +1,63 @@ +// 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/assert" + + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/semconv" +) + +func TestDetect(t *testing.T) { + + cases := []struct { + name string + schema1, schema2 string + isErr bool + }{ + { + name: "different schema urls", + schema1: "https://opentelemetry.io/schemas/1.3.0", + schema2: "https://opentelemetry.io/schemas/1.4.0", + isErr: true, + }, + { + name: "same schema url", + schema1: "https://opentelemetry.io/schemas/1.4.0", + schema2: "https://opentelemetry.io/schemas/1.4.0", + isErr: false, + }, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("case-%s", c.name), func(t *testing.T) { + d1 := resource.StringDetector(c.schema1, semconv.HostNameKey, os.Hostname) + d2 := resource.StringDetector(c.schema2, semconv.HostNameKey, os.Hostname) + r, err := resource.Detect(context.Background(), d1, d2) + assert.NotNil(t, r) + if c.isErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/sdk/resource/benchmark_test.go b/sdk/resource/benchmark_test.go index e4e762fbe2c..9bde4cb916e 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.NewWithAttributes(l1...), resource.NewWithAttributes(l2...) + return resource.NewSchemaless(l1...), resource.NewSchemaless(l2...) } func benchmarkMergeResource(b *testing.B, size int) { @@ -57,7 +57,7 @@ func benchmarkMergeResource(b *testing.B, size int) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = resource.Merge(r1, r2) + _, _ = resource.Merge(r1, r2) } } diff --git a/sdk/resource/builtin.go b/sdk/resource/builtin.go index 9ad790883f4..06e2a5d4188 100644 --- a/sdk/resource/builtin.go +++ b/sdk/resource/builtin.go @@ -41,8 +41,9 @@ type ( host struct{} stringDetector struct { - K attribute.Key - F func() (string, error) + schemaURL string + K attribute.Key + F func() (string, error) } defaultServiceNameDetector struct{} @@ -58,6 +59,7 @@ var ( // Detect returns a *Resource that describes the OpenTelemetry SDK used. func (telemetrySDK) Detect(context.Context) (*Resource, error) { return NewWithAttributes( + semconv.SchemaURL, semconv.TelemetrySDKNameKey.String("opentelemetry"), semconv.TelemetrySDKLanguageKey.String("go"), semconv.TelemetrySDKVersionKey.String(otel.Version()), @@ -66,13 +68,14 @@ func (telemetrySDK) Detect(context.Context) (*Resource, error) { // 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) + return StringDetector(semconv.SchemaURL, 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 attribute.Key, f func() (string, error)) Detector { - return stringDetector{K: k, F: f} +// containing the string as a value corresponding to k. The resulting Resource +// will have the specified schemaURL. +func StringDetector(schemaURL string, k attribute.Key, f func() (string, error)) Detector { + return stringDetector{schemaURL: schemaURL, K: k, F: f} } // Detect implements Detector. @@ -85,12 +88,13 @@ func (sd stringDetector) Detect(ctx context.Context) (*Resource, error) { if !a.Valid() { return nil, fmt.Errorf("invalid attribute: %q -> %q", a.Key, a.Value.Emit()) } - return NewWithAttributes(sd.K.String(value)), nil + return NewWithAttributes(sd.schemaURL, sd.K.String(value)), nil } // Detect implements Detector func (defaultServiceNameDetector) Detect(ctx context.Context) (*Resource, error) { return StringDetector( + semconv.SchemaURL, semconv.ServiceNameKey, func() (string, error) { executable, err := os.Executable() diff --git a/sdk/resource/builtin_test.go b/sdk/resource/builtin_test.go index 223bd0df800..04c8ee9909e 100644 --- a/sdk/resource/builtin_test.go +++ b/sdk/resource/builtin_test.go @@ -28,7 +28,7 @@ import ( func TestBuiltinStringDetector(t *testing.T) { E := fmt.Errorf("no K") - res, err := resource.StringDetector(attribute.Key("K"), func() (string, error) { + res, err := resource.StringDetector("", attribute.Key("K"), func() (string, error) { return "", E }).Detect(context.Background()) require.True(t, errors.Is(err, E)) @@ -44,14 +44,14 @@ func TestStringDetectorErrors(t *testing.T) { }{ { desc: "explicit error from func should be returned", - s: resource.StringDetector(attribute.Key("K"), func() (string, error) { + s: resource.StringDetector("", attribute.Key("K"), func() (string, error) { return "", fmt.Errorf("K-IS-MISSING") }), errContains: "K-IS-MISSING", }, { desc: "empty key is an invalid", - s: resource.StringDetector(attribute.Key(""), func() (string, error) { + s: resource.StringDetector("", attribute.Key(""), func() (string, error) { return "not-empty", nil }), errContains: "invalid attribute: \"\" -> \"not-empty\"", diff --git a/sdk/resource/config.go b/sdk/resource/config.go index 7324b745d19..2606de7bb79 100644 --- a/sdk/resource/config.go +++ b/sdk/resource/config.go @@ -24,6 +24,8 @@ import ( type config struct { // detectors that will be evaluated. detectors []Detector + // SchemaURL to associate with the Resource. + schemaURL string } // Option is the interface that applies a configuration option. @@ -42,7 +44,7 @@ type detectAttributes struct { } func (d detectAttributes) Detect(context.Context) (*Resource, error) { - return NewWithAttributes(d.attributes...), nil + return NewSchemaless(d.attributes...), nil } // WithDetectors adds detectors to be evaluated for the configured resource. @@ -65,7 +67,7 @@ func WithBuiltinDetectors() Option { fromEnv{}) } -// WithFromEnv adds attributes from environment variables to the configured resource. +// WithFromEnv adds attributes from environment variables to the configured resource. func WithFromEnv() Option { return WithDetectors(fromEnv{}) } @@ -79,3 +81,14 @@ func WithHost() Option { func WithTelemetrySDK() Option { return WithDetectors(telemetrySDK{}) } + +// WithSchemaURL sets the schema URL for the configured resource. +func WithSchemaURL(schemaURL string) Option { + return schemaURLOption(schemaURL) +} + +type schemaURLOption string + +func (o schemaURLOption) apply(cfg *config) { + cfg.schemaURL = string(o) +} diff --git a/sdk/resource/env.go b/sdk/resource/env.go index 4b5fe497a07..2641e903063 100644 --- a/sdk/resource/env.go +++ b/sdk/resource/env.go @@ -57,14 +57,22 @@ func (fromEnv) Detect(context.Context) (*Resource, error) { var res *Resource if svcName != "" { - res = NewWithAttributes(semconv.ServiceNameKey.String(svcName)) + res = NewSchemaless(semconv.ServiceNameKey.String(svcName)) } r2, err := constructOTResources(attrs) // Ensure that the resource with the service name from OTEL_SERVICE_NAME // takes precedence, if it was defined. - return Merge(r2, res), err + res, err2 := Merge(r2, res) + + if err == nil { + err = err2 + } else if err2 != nil { + err = fmt.Errorf("detecting resources: %s", []string{err.Error(), err2.Error()}) + } + + return res, err } func constructOTResources(s string) (*Resource, error) { @@ -84,5 +92,5 @@ func constructOTResources(s string) (*Resource, error) { if len(invalid) > 0 { err = fmt.Errorf("%w: %v", errMissingValue, invalid) } - return NewWithAttributes(attrs...), err + return NewSchemaless(attrs...), err } diff --git a/sdk/resource/env_test.go b/sdk/resource/env_test.go index 6aeca346ab9..1eb1e6f2926 100644 --- a/sdk/resource/env_test.go +++ b/sdk/resource/env_test.go @@ -37,7 +37,7 @@ func TestDetectOnePair(t *testing.T) { detector := &fromEnv{} res, err := detector.Detect(context.Background()) require.NoError(t, err) - assert.Equal(t, NewWithAttributes(attribute.String("key", "value")), res) + assert.Equal(t, NewSchemaless(attribute.String("key", "value")), res) } func TestDetectMultiPairs(t *testing.T) { @@ -51,7 +51,7 @@ func TestDetectMultiPairs(t *testing.T) { detector := &fromEnv{} res, err := detector.Detect(context.Background()) require.NoError(t, err) - assert.Equal(t, res, NewWithAttributes( + assert.Equal(t, res, NewSchemaless( attribute.String("key", "value"), attribute.String("k", "v"), attribute.String("a", "x"), @@ -83,7 +83,7 @@ func TestMissingKeyError(t *testing.T) { res, err := detector.Detect(context.Background()) assert.Error(t, err) assert.Equal(t, err, fmt.Errorf("%w: %v", errMissingValue, "[key]")) - assert.Equal(t, res, NewWithAttributes( + assert.Equal(t, res, NewSchemaless( attribute.String("key", "value"), )) } @@ -99,7 +99,7 @@ func TestDetectServiceNameFromEnv(t *testing.T) { detector := &fromEnv{} res, err := detector.Detect(context.Background()) require.NoError(t, err) - assert.Equal(t, res, NewWithAttributes( + assert.Equal(t, res, NewSchemaless( attribute.String("key", "value"), semconv.ServiceNameKey.String("bar"), )) diff --git a/sdk/resource/os.go b/sdk/resource/os.go index 816d209217a..699b0b58286 100644 --- a/sdk/resource/os.go +++ b/sdk/resource/os.go @@ -29,6 +29,7 @@ func (osTypeDetector) Detect(ctx context.Context) (*Resource, error) { osType := runtimeOS() return NewWithAttributes( + semconv.SchemaURL, semconv.OSTypeKey.String(strings.ToLower(osType)), ), nil } diff --git a/sdk/resource/process.go b/sdk/resource/process.go index f15f97ec5ac..45539d1e2f5 100644 --- a/sdk/resource/process.go +++ b/sdk/resource/process.go @@ -115,14 +115,14 @@ type processRuntimeDescriptionDetector struct{} // Detect returns a *Resource that describes the process identifier (PID) of the // executing process. func (processPIDDetector) Detect(ctx context.Context) (*Resource, error) { - return NewWithAttributes(semconv.ProcessPIDKey.Int(pid())), nil + return NewWithAttributes(semconv.SchemaURL, semconv.ProcessPIDKey.Int(pid())), nil } // Detect returns a *Resource that describes the name of the process executable. func (processExecutableNameDetector) Detect(ctx context.Context) (*Resource, error) { executableName := filepath.Base(commandArgs()[0]) - return NewWithAttributes(semconv.ProcessExecutableNameKey.String(executableName)), nil + return NewWithAttributes(semconv.SchemaURL, semconv.ProcessExecutableNameKey.String(executableName)), nil } // Detect returns a *Resource that describes the full path of the process executable. @@ -132,13 +132,13 @@ func (processExecutablePathDetector) Detect(ctx context.Context) (*Resource, err return nil, err } - return NewWithAttributes(semconv.ProcessExecutablePathKey.String(executablePath)), nil + return NewWithAttributes(semconv.SchemaURL, semconv.ProcessExecutablePathKey.String(executablePath)), nil } // Detect returns a *Resource that describes all the command arguments as received // by the process. func (processCommandArgsDetector) Detect(ctx context.Context) (*Resource, error) { - return NewWithAttributes(semconv.ProcessCommandArgsKey.Array(commandArgs())), nil + return NewWithAttributes(semconv.SchemaURL, semconv.ProcessCommandArgsKey.Array(commandArgs())), nil } // Detect returns a *Resource that describes the username of the user that owns the @@ -149,18 +149,18 @@ func (processOwnerDetector) Detect(ctx context.Context) (*Resource, error) { return nil, err } - return NewWithAttributes(semconv.ProcessOwnerKey.String(owner.Username)), nil + return NewWithAttributes(semconv.SchemaURL, semconv.ProcessOwnerKey.String(owner.Username)), nil } // Detect returns a *Resource that describes the name of the compiler used to compile // this process image. func (processRuntimeNameDetector) Detect(ctx context.Context) (*Resource, error) { - return NewWithAttributes(semconv.ProcessRuntimeNameKey.String(runtimeName())), nil + return NewWithAttributes(semconv.SchemaURL, semconv.ProcessRuntimeNameKey.String(runtimeName())), nil } // Detect returns a *Resource that describes the version of the runtime of this process. func (processRuntimeVersionDetector) Detect(ctx context.Context) (*Resource, error) { - return NewWithAttributes(semconv.ProcessRuntimeVersionKey.String(runtimeVersion())), nil + return NewWithAttributes(semconv.SchemaURL, semconv.ProcessRuntimeVersionKey.String(runtimeVersion())), nil } // Detect returns a *Resource that describes the runtime of this process. @@ -169,6 +169,7 @@ func (processRuntimeDescriptionDetector) Detect(ctx context.Context) (*Resource, "go version %s %s/%s", runtimeVersion(), runtimeOS(), runtimeArch()) return NewWithAttributes( + semconv.SchemaURL, semconv.ProcessRuntimeDescriptionKey.String(runtimeDescription), ), nil } diff --git a/sdk/resource/resource.go b/sdk/resource/resource.go index a2d36ae13e7..4e77834fc8f 100644 --- a/sdk/resource/resource.go +++ b/sdk/resource/resource.go @@ -16,6 +16,8 @@ package resource // import "go.opentelemetry.io/otel/sdk/resource" import ( "context" + "errors" + "fmt" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -29,7 +31,8 @@ import ( // (`*resource.Resource`). The `nil` value is equivalent to an empty // Resource. type Resource struct { - attrs attribute.Set + attrs attribute.Set + schemaURL string } var ( @@ -43,6 +46,10 @@ var ( }(Detect(context.Background(), defaultServiceNameDetector{}, fromEnv{}, telemetrySDK{})) ) +var ( + errMergeConflictSchemaURL = errors.New("cannot merge resource due to conflicting Schema URL") +) + // New returns a Resource combined from the user-provided detectors. func New(ctx context.Context, opts ...Option) (*Resource, error) { cfg := config{} @@ -50,13 +57,34 @@ func New(ctx context.Context, opts ...Option) (*Resource, error) { opt.apply(&cfg) } - return Detect(ctx, cfg.detectors...) + resource, err := Detect(ctx, cfg.detectors...) + + var err2 error + resource, err2 = Merge(resource, &Resource{schemaURL: cfg.schemaURL}) + if err == nil { + err = err2 + } else if err2 != nil { + err = fmt.Errorf("detecting resources: %s", []string{err.Error(), err2.Error()}) + } + + return resource, err +} + +// NewWithAttributes creates a resource from attrs and associates the resource with a +// schema URL. If attrs contains duplicate keys, the last value will be used. If attrs +// contains any invalid items those items will be dropped. The attrs are assumed to be +// in a schema identified by schemaURL. +func NewWithAttributes(schemaURL string, attrs ...attribute.KeyValue) *Resource { + resource := NewSchemaless(attrs...) + resource.schemaURL = schemaURL + return resource } -// NewWithAttributes creates a resource from attrs. If attrs contains -// duplicate keys, the last value will be used. If attrs contains any invalid -// items those items will be dropped. -func NewWithAttributes(attrs ...attribute.KeyValue) *Resource { +// NewSchemaless creates a resource from attrs. If attrs contains duplicate keys, +// the last value will be used. If attrs contains any invalid items those items will +// be dropped. The resource will not be associated with a schema URL. If the schema +// of the attrs is known use NewWithAttributes instead. +func NewSchemaless(attrs ...attribute.KeyValue) *Resource { if len(attrs) == 0 { return &emptyResource } @@ -72,7 +100,7 @@ func NewWithAttributes(attrs ...attribute.KeyValue) *Resource { return &emptyResource } - return &Resource{s} //nolint + return &Resource{attrs: s} //nolint } // String implements the Stringer interface and provides a @@ -96,6 +124,10 @@ func (r *Resource) Attributes() []attribute.KeyValue { return r.attrs.ToSlice() } +func (r *Resource) SchemaURL() string { + return r.schemaURL +} + // Iter returns an interator of the Resource attributes. // This is ideal to use if you do not want a copy of the attributes. func (r *Resource) Iter() attribute.Iterator { @@ -121,15 +153,32 @@ func (r *Resource) Equal(eq *Resource) bool { // If there are common keys between resource a and b, then the value // from resource b will overwrite the value from resource a, even // if resource b's value is empty. -func Merge(a, b *Resource) *Resource { +// +// The SchemaURL of the resources will be merged according to the spec rules: +// https://github.com/open-telemetry/opentelemetry-specification/blob/bad49c714a62da5493f2d1d9bafd7ebe8c8ce7eb/specification/resource/sdk.md#merge +// If the resources have different non-empty schemaURL an empty resource and an error +// will be returned. +func Merge(a, b *Resource) (*Resource, error) { if a == nil && b == nil { - return Empty() + return Empty(), nil } if a == nil { - return b + return b, nil } if b == nil { - return a + return a, nil + } + + // Merge the schema URL. + var schemaURL string + if a.schemaURL == "" { + schemaURL = b.schemaURL + } else if b.schemaURL == "" { + schemaURL = a.schemaURL + } else if a.schemaURL == b.schemaURL { + schemaURL = a.schemaURL + } else { + return Empty(), errMergeConflictSchemaURL } // Note: 'b' attributes will overwrite 'a' with last-value-wins in attribute.Key() @@ -139,7 +188,8 @@ func Merge(a, b *Resource) *Resource { for mi.Next() { combine = append(combine, mi.Label()) } - return NewWithAttributes(combine...) + merged := NewWithAttributes(schemaURL, combine...) + return merged, nil } // 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 c426e56f0da..fb8c838c44d 100644 --- a/sdk/resource/resource_test.go +++ b/sdk/resource/resource_test.go @@ -17,12 +17,14 @@ package resource_test import ( "context" "encoding/json" + "errors" "fmt" "os" "strings" "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" @@ -65,7 +67,7 @@ func TestNewWithAttributes(t *testing.T) { } for _, c := range cases { t.Run(fmt.Sprintf("case-%s", c.name), func(t *testing.T) { - res := resource.NewWithAttributes(c.in...) + res := resource.NewSchemaless(c.in...) if diff := cmp.Diff( res.Attributes(), c.want, @@ -78,86 +80,121 @@ func TestNewWithAttributes(t *testing.T) { func TestMerge(t *testing.T) { cases := []struct { - name string - a, b *resource.Resource - want []attribute.KeyValue + name string + a, b *resource.Resource + want []attribute.KeyValue + isErr bool + schemaURL string }{ + { + name: "Merge 2 nils", + a: nil, + b: nil, + want: nil, + }, { name: "Merge with no overlap, no nil", - a: resource.NewWithAttributes(kv11, kv31), - b: resource.NewWithAttributes(kv21, kv41), + a: resource.NewSchemaless(kv11, kv31), + b: resource.NewSchemaless(kv21, kv41), want: []attribute.KeyValue{kv11, kv21, kv31, kv41}, }, { name: "Merge with no overlap, no nil, not interleaved", - a: resource.NewWithAttributes(kv11, kv21), - b: resource.NewWithAttributes(kv31, kv41), + a: resource.NewSchemaless(kv11, kv21), + b: resource.NewSchemaless(kv31, kv41), want: []attribute.KeyValue{kv11, kv21, kv31, kv41}, }, { name: "Merge with common key order1", - a: resource.NewWithAttributes(kv11), - b: resource.NewWithAttributes(kv12, kv21), + a: resource.NewSchemaless(kv11), + b: resource.NewSchemaless(kv12, kv21), want: []attribute.KeyValue{kv12, kv21}, }, { name: "Merge with common key order2", - a: resource.NewWithAttributes(kv12, kv21), - b: resource.NewWithAttributes(kv11), + a: resource.NewSchemaless(kv12, kv21), + b: resource.NewSchemaless(kv11), want: []attribute.KeyValue{kv11, kv21}, }, { name: "Merge with common key order4", - a: resource.NewWithAttributes(kv11, kv21, kv41), - b: resource.NewWithAttributes(kv31, kv41), + a: resource.NewSchemaless(kv11, kv21, kv41), + b: resource.NewSchemaless(kv31, kv41), want: []attribute.KeyValue{kv11, kv21, kv31, kv41}, }, { name: "Merge with no keys", - a: resource.NewWithAttributes(), - b: resource.NewWithAttributes(), + a: resource.NewSchemaless(), + b: resource.NewSchemaless(), want: nil, }, { name: "Merge with first resource no keys", - a: resource.NewWithAttributes(), - b: resource.NewWithAttributes(kv21), + a: resource.NewSchemaless(), + b: resource.NewSchemaless(kv21), want: []attribute.KeyValue{kv21}, }, { name: "Merge with second resource no keys", - a: resource.NewWithAttributes(kv11), - b: resource.NewWithAttributes(), + a: resource.NewSchemaless(kv11), + b: resource.NewSchemaless(), want: []attribute.KeyValue{kv11}, }, { name: "Merge with first resource nil", a: nil, - b: resource.NewWithAttributes(kv21), + b: resource.NewSchemaless(kv21), want: []attribute.KeyValue{kv21}, }, { name: "Merge with second resource nil", - a: resource.NewWithAttributes(kv11), + a: resource.NewSchemaless(kv11), b: nil, want: []attribute.KeyValue{kv11}, }, { name: "Merge with first resource value empty string", - a: resource.NewWithAttributes(kv42), - b: resource.NewWithAttributes(kv41), + a: resource.NewSchemaless(kv42), + b: resource.NewSchemaless(kv41), want: []attribute.KeyValue{kv41}, }, { name: "Merge with second resource value empty string", - a: resource.NewWithAttributes(kv41), - b: resource.NewWithAttributes(kv42), + a: resource.NewSchemaless(kv41), + b: resource.NewSchemaless(kv42), want: []attribute.KeyValue{kv42}, }, + { + name: "Merge with first resource with schema", + a: resource.NewWithAttributes("https://opentelemetry.io/schemas/1.4.0", kv41), + b: resource.NewSchemaless(kv42), + want: []attribute.KeyValue{kv42}, + schemaURL: "https://opentelemetry.io/schemas/1.4.0", + }, + { + name: "Merge with second resource with schema", + a: resource.NewSchemaless(kv41), + b: resource.NewWithAttributes("https://opentelemetry.io/schemas/1.4.0", kv42), + want: []attribute.KeyValue{kv42}, + schemaURL: "https://opentelemetry.io/schemas/1.4.0", + }, + { + name: "Merge with different schemas", + a: resource.NewWithAttributes("https://opentelemetry.io/schemas/1.4.0", kv41), + b: resource.NewWithAttributes("https://opentelemetry.io/schemas/1.3.0", kv42), + want: nil, + isErr: true, + }, } for _, c := range cases { t.Run(fmt.Sprintf("case-%s", c.name), func(t *testing.T) { - res := resource.Merge(c.a, c.b) + res, err := resource.Merge(c.a, c.b) + if c.isErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.EqualValues(t, c.schemaURL, res.SchemaURL()) if diff := cmp.Diff( res.Attributes(), c.want, @@ -249,7 +286,7 @@ func TestString(t *testing.T) { want: "B=b", }, } { - if got := resource.NewWithAttributes(test.kvs...).String(); got != test.want { + if got := resource.NewSchemaless(test.kvs...).String(); got != test.want { t.Errorf("Resource(%v).String() = %q, want %q", test.kvs, got, test.want) } } @@ -258,7 +295,7 @@ func TestString(t *testing.T) { const envVar = "OTEL_RESOURCE_ATTRIBUTES" func TestMarshalJSON(t *testing.T) { - r := resource.NewWithAttributes(attribute.Int64("A", 1), attribute.String("C", "D")) + r := resource.NewSchemaless(attribute.Int64("A", 1), attribute.String("C", "D")) data, err := json.Marshal(r) require.NoError(t, err) require.Equal(t, @@ -274,9 +311,11 @@ func TestNew(t *testing.T) { options []resource.Option resourceValues map[string]string + schemaURL string + isErr bool }{ { - name: "No Options returns empty resrouce", + name: "No Options returns empty resource", envars: "key=value,other=attr", options: nil, resourceValues: map[string]string{}, @@ -298,6 +337,7 @@ func TestNew(t *testing.T) { resourceValues: map[string]string{ "host.name": hostname(), }, + schemaURL: semconv.SchemaURL, }, { name: "Only Env", @@ -321,6 +361,7 @@ func TestNew(t *testing.T) { "telemetry.sdk.language": "go", "telemetry.sdk.version": otel.Version(), }, + schemaURL: semconv.SchemaURL, }, { name: "WithAttributes", @@ -346,6 +387,46 @@ func TestNew(t *testing.T) { "key": "value", "other": "attr", }, + schemaURL: semconv.SchemaURL, + }, + { + name: "With schema url", + envars: "", + options: []resource.Option{ + resource.WithAttributes(attribute.String("A", "B")), + resource.WithSchemaURL("https://opentelemetry.io/schemas/1.0.0"), + }, + resourceValues: map[string]string{ + "A": "B", + }, + schemaURL: "https://opentelemetry.io/schemas/1.0.0", + }, + { + name: "With conflicting schema urls", + envars: "", + options: []resource.Option{ + resource.WithDetectors( + resource.StringDetector("https://opentelemetry.io/schemas/1.0.0", semconv.HostNameKey, os.Hostname), + ), + resource.WithSchemaURL("https://opentelemetry.io/schemas/1.1.0"), + }, + resourceValues: map[string]string{}, + schemaURL: "", + isErr: true, + }, + { + name: "With conflicting detector schema urls", + envars: "", + options: []resource.Option{ + resource.WithDetectors( + resource.StringDetector("https://opentelemetry.io/schemas/1.0.0", semconv.HostNameKey, os.Hostname), + resource.StringDetector("https://opentelemetry.io/schemas/1.1.0", semconv.HostNameKey, func() (string, error) { return "", errors.New("fail") }), + ), + resource.WithSchemaURL("https://opentelemetry.io/schemas/1.2.0"), + }, + resourceValues: map[string]string{}, + schemaURL: "", + isErr: true, }, } for _, tt := range tc { @@ -359,8 +440,19 @@ func TestNew(t *testing.T) { ctx := context.Background() res, err := resource.New(ctx, tt.options...) - require.NoError(t, err) + if tt.isErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.EqualValues(t, tt.resourceValues, toMap(res)) + + // TODO: do we need to ensure that resource is never nil and eliminate the + // following if? + if res != nil { + assert.EqualValues(t, tt.schemaURL, res.SchemaURL()) + } }) } } diff --git a/sdk/trace/provider.go b/sdk/trace/provider.go index 57edf78972a..3a32445bc75 100644 --- a/sdk/trace/provider.go +++ b/sdk/trace/provider.go @@ -279,7 +279,11 @@ func WithSpanProcessor(sp SpanProcessor) TracerProviderOption { // resource.Default() Resource by default. func WithResource(r *resource.Resource) TracerProviderOption { return traceProviderOptionFunc(func(cfg *tracerProviderConfig) { - cfg.resource = resource.Merge(resource.Environment(), r) + var err error + cfg.resource, err = resource.Merge(resource.Environment(), r) + if err != nil { + otel.Handle(err) + } }) } diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index cf055c5740a..9eb8f239c1d 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -1210,6 +1210,12 @@ func TestWithSpanKind(t *testing.T) { } } +func mergeResource(t *testing.T, r1, r2 *resource.Resource) *resource.Resource { + r, err := resource.Merge(r1, r2) + assert.NoError(t, err) + return r +} + func TestWithResource(t *testing.T) { store, err := ottest.SetEnvVariables(map[string]string{ envVar: "key=value,rk5=7", @@ -1235,20 +1241,20 @@ func TestWithResource(t *testing.T) { }, { name: "explicit resource", - options: []TracerProviderOption{WithResource(resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)))}, - want: resource.Merge(resource.Environment(), resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5))), + options: []TracerProviderOption{WithResource(resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)))}, + want: mergeResource(t, resource.Environment(), resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5))), }, { name: "last resource wins", options: []TracerProviderOption{ - WithResource(resource.NewWithAttributes(attribute.String("rk1", "vk1"), attribute.Int64("rk2", 5))), - WithResource(resource.NewWithAttributes(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10)))}, - want: resource.Merge(resource.Environment(), resource.NewWithAttributes(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10))), + WithResource(resource.NewSchemaless(attribute.String("rk1", "vk1"), attribute.Int64("rk2", 5))), + WithResource(resource.NewSchemaless(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10)))}, + want: mergeResource(t, resource.Environment(), resource.NewSchemaless(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10))), }, { name: "overlapping attributes with environment resource", - options: []TracerProviderOption{WithResource(resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10)))}, - want: resource.Merge(resource.Environment(), resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10))), + options: []TracerProviderOption{WithResource(resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10)))}, + want: mergeResource(t, resource.Environment(), resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10))), }, } for _, tc := range cases { @@ -1345,7 +1351,7 @@ func TestSpanCapturesPanic(t *testing.T) { func TestReadOnlySpan(t *testing.T) { kv := attribute.String("foo", "bar") - tp := NewTracerProvider(WithResource(resource.NewWithAttributes(kv))) + tp := NewTracerProvider(WithResource(resource.NewSchemaless(kv))) tr := tp.Tracer("ReadOnlySpan", trace.WithInstrumentationVersion("3")) // Initialize parent context. diff --git a/semconv/schema.go b/semconv/schema.go new file mode 100644 index 00000000000..32d8511b382 --- /dev/null +++ b/semconv/schema.go @@ -0,0 +1,22 @@ +// 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 semconv + +// SchemaURL is the schema URL that matches the version of the semantic conventions +// that this package defines. This package defines semantic conventions for spec +// v1.3.0 which was released before the concept of schemas was introduce, thus the +// schema URL is empty. Semconv packages starting from v1.4.0 must declare non-empty +// schema URL in the form https://opentelemetry.io/schemas/ +const SchemaURL = ""