You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +02:00 
			
		
		
		
	Add Schema URL support to Resource (#1938)
This implements specification requirement: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#resource-creation - Changes `resource.NewWithAttributes` to require a schema URL. The old function is still available as `resource.NewSchemaless`. This is a breaking change. We want to encourage using schema URL and make it a conscious choice to have a resource without schema. - Merge schema URLs acccording to the spec in resource.Merge. - Several builtin resource detectors now correctly populate the schema URL. Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
This commit is contained in:
		| @@ -38,6 +38,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm | ||||
|   These functions replace the `Set`, `Value`, `ContextWithValue`, `ContextWithoutValue`, and `ContextWithEmpty` functions from that package and directly work with the new `Baggage` type. (#1967) | ||||
| - 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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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")}, | ||||
| 			), | ||||
|   | ||||
| @@ -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), | ||||
|   | ||||
| @@ -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"), | ||||
| 		)), | ||||
| 	) | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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), | ||||
| 		)))...) | ||||
|   | ||||
| @@ -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{ | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
|   | ||||
| @@ -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 | ||||
| 		} | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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} | ||||
|  | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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), | ||||
| 		)))...) | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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")), | ||||
| 	} | ||||
|   | ||||
| @@ -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{ | ||||
| 		{ | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -37,7 +37,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| func TestModelConversion(t *testing.T) { | ||||
| 	resource := resource.NewWithAttributes( | ||||
| 	resource := resource.NewSchemaless( | ||||
| 		semconv.ServiceNameKey.String("model-test"), | ||||
| 	) | ||||
|  | ||||
|   | ||||
| @@ -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{ | ||||
|   | ||||
| @@ -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} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() | ||||
|  | ||||
|   | ||||
| @@ -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") | ||||
|  | ||||
|   | ||||
| @@ -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()) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										63
									
								
								sdk/resource/auto_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								sdk/resource/auto_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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\"", | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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"), | ||||
| 	)) | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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. 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 { | ||||
| // 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 | ||||
| } | ||||
|  | ||||
| // 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 | ||||
|   | ||||
| @@ -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()) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
							
								
								
									
										22
									
								
								semconv/schema.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								semconv/schema.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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/<version> | ||||
| const SchemaURL = "" | ||||
		Reference in New Issue
	
	Block a user