1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-01-16 02:47:20 +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:
Tigran Najaryan 2021-06-08 12:46:42 -04:00 committed by GitHub
parent 0827aa6265
commit 46d9687a35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 421 additions and 140 deletions

View File

@ -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

View File

@ -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

View File

@ -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")},
),

View File

@ -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),

View File

@ -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"),
)),
)

View File

@ -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)

View File

@ -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",

View File

@ -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),
)))...)

View File

@ -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{

View File

@ -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
}

View File

@ -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
}

View File

@ -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",

View File

@ -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}

View File

@ -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",

View File

@ -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",

View File

@ -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),
)))...)

View File

@ -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
}

View File

@ -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",

View File

@ -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")),
}

View File

@ -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{
{

View File

@ -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",

View File

@ -37,7 +37,7 @@ import (
)
func TestModelConversion(t *testing.T) {
resource := resource.NewWithAttributes(
resource := resource.NewSchemaless(
semconv.ServiceNameKey.String("model-test"),
)

View File

@ -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{

View File

@ -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}
}

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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")

View File

@ -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())

View File

@ -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
View 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)
}
})
}
}

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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\"",

View File

@ -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)
}

View File

@ -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
}

View File

@ -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"),
))

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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())
}
})
}
}

View File

@ -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)
}
})
}

View File

@ -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
View 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 = ""