Repository: open-telemetry/opentelemetry-go
Pull request #1938 (2021-05-21 17:59:39)

Add Schema URL support to Resource
This implements specification requirement:
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#resource-creation

- Change `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.

- resource.Merge merges schema URLs according to the spec.

- Several builtin resource detectors now correctly populate the schema URL.


tigrannajaryan: @MrAlias @Aneurysm9 Please have a look.

This PR implements the Resource portion of schema URL. It imports the current semconv package. Once we agree on https://github.com/open-telemetry/opentelemetry-go/pull/1933 this will need to be changed to import a specific semconv package version.
codecov[bot]: # [Codecov](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) Report
> Merging [#1938](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) (d38ec6d) into [main](https://codecov.io/gh/open-telemetry/opentelemetry-go/commit/d3b1280863f421bba9656543296b71c133f1ba13?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) (d3b1280) will **decrease** coverage by `0.0%`.
> The diff coverage is `76.6%`.

[![Impacted file tree graph](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/graphs/tree.svg?width=650&height=150&src=pr&token=8efTmh4kvf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)

```diff
@@           Coverage Diff           @@
##            main   #1938     +/-   ##
=======================================
- Coverage   79.5%   79.4%   -0.1%     
=======================================
  Files        139     139             
  Lines       7463    7500     +37     
=======================================
+ Hits        5937    5961     +24     
- Misses      1278    1286      +8     
- Partials     248     253      +5     
```


| [Impacted Files](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) | Coverage Δ | |
|---|---|---|
| [exporters/otlp/internal/transform/span.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-ZXhwb3J0ZXJzL290bHAvaW50ZXJuYWwvdHJhbnNmb3JtL3NwYW4uZ28=) | `98.1% <ø> (ø)` | |
| [sdk/resource/config.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-c2RrL3Jlc291cmNlL2NvbmZpZy5nbw==) | `83.3% <0.0%> (-16.7%)` | :arrow_down: |
| [sdk/metric/controller/basic/config.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-c2RrL21ldHJpYy9jb250cm9sbGVyL2Jhc2ljL2NvbmZpZy5nbw==) | `84.6% <33.3%> (-15.4%)` | :arrow_down: |
| [sdk/resource/auto.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-c2RrL3Jlc291cmNlL2F1dG8uZ28=) | `75.0% <33.3%> (-10.8%)` | :arrow_down: |
| [sdk/trace/provider.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-c2RrL3RyYWNlL3Byb3ZpZGVyLmdv) | `82.8% <50.0%> (-1.6%)` | :arrow_down: |
| [sdk/resource/resource.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-c2RrL3Jlc291cmNlL3Jlc291cmNlLmdv) | `69.1% <78.5%> (+3.0%)` | :arrow_up: |
| [exporters/otlp/internal/transform/metric.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-ZXhwb3J0ZXJzL290bHAvaW50ZXJuYWwvdHJhbnNmb3JtL21ldHJpYy5nbw==) | `84.7% <100.0%> (+0.1%)` | :arrow_up: |
| [sdk/resource/builtin.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-c2RrL3Jlc291cmNlL2J1aWx0aW4uZ28=) | `90.9% <100.0%> (+0.9%)` | :arrow_up: |
| [sdk/resource/os.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-c2RrL3Jlc291cmNlL29zLmdv) | `100.0% <100.0%> (ø)` | |
| [sdk/resource/process.go](https://codecov.io/gh/open-telemetry/opentelemetry-go/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-c2RrL3Jlc291cmNlL3Byb2Nlc3MuZ28=) | `100.0% <100.0%> (ø)` | |

tigrannajaryan: @MrAlias @Aneurysm9 Assuming we are OK with `NewWithAttributes` function renaming this is ready for review.
tigrannajaryan: Tests are unstable and keep failing. I do not think it is related to my changes in any way (e.g. TestNewBatchSpanProcessorWithOptions failed).
tigrannajaryan: > Is the end goal to require all resource be linked to a schema? If so, I'm hesitant to release a way to create a resource without one (`NewSchemaless`).

I think we can't force that yet. The schema should be optional. The SDK will create resources with the right schema out of the box, but I don't think we are ready to declare schemaless resources invalid.

MrAlias: >  I don't think we are ready to declare schemaless resources invalid.

Is this something we would declare in v2?
tigrannajaryan: > > I don't think we are ready to declare schemaless resources invalid.
> 
> Is this something we would declare in v2?

I am not sure, probably not. I guess there will always be the cases when people want to "just send some resource" without putting much thought into what schema it follows.
Aneurysm9: > > > I don't think we are ready to declare schemaless resources invalid.
> > 
> > 
> > Is this something we would declare in v2?
> 
> I am not sure, probably not. I guess there will always be the cases when people want to "just send some resource" without putting much thought into what schema it follows.

Agreed.  Users may put arbitrary keys/values into resources that have no relation to any defined schema and I'm not sure we should stop them.
tigrannajaryan: `test-coverage` failed for reasons unrelated to my change: https://github.com/open-telemetry/opentelemetry-go/runs/2767758746?check_suite_focus=true
I don't understand what is wrong from the job output.

I will rerun for now.
tigrannajaryan: @Aneurysm9 @MrAlias can we merge or you want to have another look?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6895ab2..ae0eccd1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
 
diff --git a/bridge/opencensus/exporter.go b/bridge/opencensus/exporter.go
index ca12f283..a1723f45 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/example/jaeger/main.go b/example/jaeger/main.go
index de3ea1b5..8afb2aa3 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 f299b4bc..fef6c44a 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/otlp/internal/transform/metric.go b/exporters/otlp/internal/transform/metric.go
index 460fe027..518ae8e6 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/span.go b/exporters/otlp/internal/transform/span.go
index 7bcdeeed..1431e350 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/sdk/metric/controller/basic/config.go b/sdk/metric/controller/basic/config.go
index 4e16afa9..77b5b902 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/resource/auto.go b/sdk/resource/auto.go
index c754e221..b1c9b302 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/builtin.go b/sdk/resource/builtin.go
index 9ad79088..06e2a5d4 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/config.go b/sdk/resource/config.go
index 7324b745..2606de7b 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 4b5fe497..2641e903 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/os.go b/sdk/resource/os.go
index 816d2092..699b0b58 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 f15f97ec..45539d1e 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 a2d36ae1..4e77834f 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/trace/provider.go b/sdk/trace/provider.go
index 57edf789..3a32445b 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/semconv/schema.go b/semconv/schema.go
new file mode 100644
index 00000000..32d8511b
--- /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/<version>
+const SchemaURL = ""


Test patch:
diff --git a/bridge/opencensus/exporter_test.go b/bridge/opencensus/exporter_test.go
index 766f8a10..713d86e3 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/exporters/metric/prometheus/prometheus_test.go b/exporters/metric/prometheus/prometheus_test.go
index 1c4d895b..95f55f70 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 08f5dc38..9d8c0455 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 e28e497b..bc5d4d80 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/resource_test.go b/exporters/otlp/internal/transform/resource_test.go
index f56b56b6..679e4ecb 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_test.go b/exporters/otlp/internal/transform/span_test.go
index 74ed36b8..dd431178 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 b5778f2f..ab5abf98 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 1d490518..5c379fb7 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 6ff1544f..71708cd7 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 d68fe9c1..28b10db6 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 24eb06a9..57949d0a 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 a19bec22..cd0f8fef 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 af390a73..7e531844 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 fea48d65..748bd621 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 6d0d035c..3eeb48ad 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 c4e68f11..2d95211f 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 be7cf2ae..a611a101 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_test.go b/sdk/metric/controller/basic/config_test.go
index 8bbf07de..d93cc53a 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 ae472554..37772e3c 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 8dd58ded..249fe41a 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 1b34e8bc..c3355497 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 599f6b8f..6b207caa 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 771a3c65..42ec361f 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 d43fa630..09912c93 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_test.go b/sdk/resource/auto_test.go
new file mode 100644
index 00000000..a12dca40
--- /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 e4e762fb..9bde4cb9 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_test.go b/sdk/resource/builtin_test.go
index 223bd0df..04c8ee99 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/env_test.go b/sdk/resource/env_test.go
index 6aeca346..1eb1e6f2 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/resource_test.go b/sdk/resource/resource_test.go
index c426e56f..fb8c838c 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/trace_test.go b/sdk/trace/trace_test.go
index cf055c57..9eb8f239 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.