1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package resource_test
16
17 import (
18 "context"
19 "encoding/json"
20 "errors"
21 "fmt"
22 "os"
23 "strings"
24 "sync"
25 "testing"
26
27 "github.com/google/go-cmp/cmp"
28 "github.com/stretchr/testify/assert"
29 "github.com/stretchr/testify/require"
30
31 "go.opentelemetry.io/otel/attribute"
32 "go.opentelemetry.io/otel/sdk"
33 ottest "go.opentelemetry.io/otel/sdk/internal/internaltest"
34 "go.opentelemetry.io/otel/sdk/resource"
35 semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
36 )
37
38 var (
39 kv11 = attribute.String("k1", "v11")
40 kv12 = attribute.String("k1", "v12")
41 kv21 = attribute.String("k2", "v21")
42 kv31 = attribute.String("k3", "v31")
43 kv41 = attribute.String("k4", "v41")
44 kv42 = attribute.String("k4", "")
45 )
46
47 func TestNewWithAttributes(t *testing.T) {
48 cases := []struct {
49 name string
50 in []attribute.KeyValue
51 want []attribute.KeyValue
52 }{
53 {
54 name: "Key with common key order1",
55 in: []attribute.KeyValue{kv12, kv11, kv21},
56 want: []attribute.KeyValue{kv11, kv21},
57 },
58 {
59 name: "Key with common key order2",
60 in: []attribute.KeyValue{kv11, kv12, kv21},
61 want: []attribute.KeyValue{kv12, kv21},
62 },
63 {
64 name: "Key with nil",
65 in: nil,
66 want: nil,
67 },
68 }
69 for _, c := range cases {
70 t.Run(fmt.Sprintf("case-%s", c.name), func(t *testing.T) {
71 res := resource.NewSchemaless(c.in...)
72 if diff := cmp.Diff(
73 res.Attributes(),
74 c.want,
75 cmp.AllowUnexported(attribute.Value{})); diff != "" {
76 t.Fatalf("unwanted result: diff %+v,", diff)
77 }
78 })
79 }
80 }
81
82 func TestMerge(t *testing.T) {
83 cases := []struct {
84 name string
85 a, b *resource.Resource
86 want []attribute.KeyValue
87 isErr bool
88 schemaURL string
89 }{
90 {
91 name: "Merge 2 nils",
92 a: nil,
93 b: nil,
94 want: nil,
95 },
96 {
97 name: "Merge with no overlap, no nil",
98 a: resource.NewSchemaless(kv11, kv31),
99 b: resource.NewSchemaless(kv21, kv41),
100 want: []attribute.KeyValue{kv11, kv21, kv31, kv41},
101 },
102 {
103 name: "Merge with no overlap, no nil, not interleaved",
104 a: resource.NewSchemaless(kv11, kv21),
105 b: resource.NewSchemaless(kv31, kv41),
106 want: []attribute.KeyValue{kv11, kv21, kv31, kv41},
107 },
108 {
109 name: "Merge with common key order1",
110 a: resource.NewSchemaless(kv11),
111 b: resource.NewSchemaless(kv12, kv21),
112 want: []attribute.KeyValue{kv12, kv21},
113 },
114 {
115 name: "Merge with common key order2",
116 a: resource.NewSchemaless(kv12, kv21),
117 b: resource.NewSchemaless(kv11),
118 want: []attribute.KeyValue{kv11, kv21},
119 },
120 {
121 name: "Merge with common key order4",
122 a: resource.NewSchemaless(kv11, kv21, kv41),
123 b: resource.NewSchemaless(kv31, kv41),
124 want: []attribute.KeyValue{kv11, kv21, kv31, kv41},
125 },
126 {
127 name: "Merge with no keys",
128 a: resource.NewSchemaless(),
129 b: resource.NewSchemaless(),
130 want: nil,
131 },
132 {
133 name: "Merge with first resource no keys",
134 a: resource.NewSchemaless(),
135 b: resource.NewSchemaless(kv21),
136 want: []attribute.KeyValue{kv21},
137 },
138 {
139 name: "Merge with second resource no keys",
140 a: resource.NewSchemaless(kv11),
141 b: resource.NewSchemaless(),
142 want: []attribute.KeyValue{kv11},
143 },
144 {
145 name: "Merge with first resource nil",
146 a: nil,
147 b: resource.NewSchemaless(kv21),
148 want: []attribute.KeyValue{kv21},
149 },
150 {
151 name: "Merge with second resource nil",
152 a: resource.NewSchemaless(kv11),
153 b: nil,
154 want: []attribute.KeyValue{kv11},
155 },
156 {
157 name: "Merge with first resource value empty string",
158 a: resource.NewSchemaless(kv42),
159 b: resource.NewSchemaless(kv41),
160 want: []attribute.KeyValue{kv41},
161 },
162 {
163 name: "Merge with second resource value empty string",
164 a: resource.NewSchemaless(kv41),
165 b: resource.NewSchemaless(kv42),
166 want: []attribute.KeyValue{kv42},
167 },
168 {
169 name: "Merge with first resource with schema",
170 a: resource.NewWithAttributes("https://opentelemetry.io/schemas/1.4.0", kv41),
171 b: resource.NewSchemaless(kv42),
172 want: []attribute.KeyValue{kv42},
173 schemaURL: "https://opentelemetry.io/schemas/1.4.0",
174 },
175 {
176 name: "Merge with second resource with schema",
177 a: resource.NewSchemaless(kv41),
178 b: resource.NewWithAttributes("https://opentelemetry.io/schemas/1.4.0", kv42),
179 want: []attribute.KeyValue{kv42},
180 schemaURL: "https://opentelemetry.io/schemas/1.4.0",
181 },
182 {
183 name: "Merge with different schemas",
184 a: resource.NewWithAttributes("https://opentelemetry.io/schemas/1.4.0", kv41),
185 b: resource.NewWithAttributes("https://opentelemetry.io/schemas/1.3.0", kv42),
186 want: nil,
187 isErr: true,
188 },
189 }
190 for _, c := range cases {
191 t.Run(fmt.Sprintf("case-%s", c.name), func(t *testing.T) {
192 res, err := resource.Merge(c.a, c.b)
193 if c.isErr {
194 assert.Error(t, err)
195 } else {
196 assert.NoError(t, err)
197 }
198 assert.EqualValues(t, c.schemaURL, res.SchemaURL())
199 if diff := cmp.Diff(
200 res.Attributes(),
201 c.want,
202 cmp.AllowUnexported(attribute.Value{})); diff != "" {
203 t.Fatalf("unwanted result: diff %+v,", diff)
204 }
205 })
206 }
207 }
208
209 func TestEmpty(t *testing.T) {
210 var res *resource.Resource
211 assert.Equal(t, "", res.SchemaURL())
212 assert.Equal(t, "", res.String())
213 assert.Equal(t, []attribute.KeyValue(nil), res.Attributes())
214
215 it := res.Iter()
216 assert.Equal(t, 0, it.Len())
217 assert.True(t, res.Equal(res))
218 }
219
220 func TestDefault(t *testing.T) {
221 res := resource.Default()
222 require.False(t, res.Equal(resource.Empty()))
223 require.True(t, res.Set().HasValue(semconv.ServiceNameKey))
224
225 serviceName, _ := res.Set().Value(semconv.ServiceNameKey)
226 require.True(t, strings.HasPrefix(serviceName.AsString(), "unknown_service:"))
227 require.Greaterf(t, len(serviceName.AsString()), len("unknown_service:"),
228 "default service.name should include executable name")
229
230 require.Contains(t, res.Attributes(), semconv.TelemetrySDKLanguageGo)
231 require.Contains(t, res.Attributes(), semconv.TelemetrySDKVersion(sdk.Version()))
232 require.Contains(t, res.Attributes(), semconv.TelemetrySDKName("opentelemetry"))
233 }
234
235 func TestString(t *testing.T) {
236 for _, test := range []struct {
237 kvs []attribute.KeyValue
238 want string
239 }{
240 {
241 kvs: nil,
242 want: "",
243 },
244 {
245 kvs: []attribute.KeyValue{},
246 want: "",
247 },
248 {
249 kvs: []attribute.KeyValue{kv11},
250 want: "k1=v11",
251 },
252 {
253 kvs: []attribute.KeyValue{kv11, kv12},
254 want: "k1=v12",
255 },
256 {
257 kvs: []attribute.KeyValue{kv11, kv21},
258 want: "k1=v11,k2=v21",
259 },
260 {
261 kvs: []attribute.KeyValue{kv21, kv11},
262 want: "k1=v11,k2=v21",
263 },
264 {
265 kvs: []attribute.KeyValue{kv11, kv21, kv31},
266 want: "k1=v11,k2=v21,k3=v31",
267 },
268 {
269 kvs: []attribute.KeyValue{kv31, kv11, kv21},
270 want: "k1=v11,k2=v21,k3=v31",
271 },
272 {
273 kvs: []attribute.KeyValue{attribute.String("A", "a"), attribute.String("B", "b")},
274 want: "A=a,B=b",
275 },
276 {
277 kvs: []attribute.KeyValue{attribute.String("A", "a,B=b")},
278 want: `A=a\,B\=b`,
279 },
280 {
281 kvs: []attribute.KeyValue{attribute.String("A", `a,B\=b`)},
282 want: `A=a\,B\\\=b`,
283 },
284 {
285 kvs: []attribute.KeyValue{attribute.String("A=a,B", `b`)},
286 want: `A\=a\,B=b`,
287 },
288 {
289 kvs: []attribute.KeyValue{attribute.String(`A=a\,B`, `b`)},
290 want: `A\=a\\\,B=b`,
291 },
292 {
293 kvs: []attribute.KeyValue{attribute.String("", "invalid")},
294 want: "",
295 },
296 {
297 kvs: []attribute.KeyValue{attribute.String("", "invalid"), attribute.String("B", "b")},
298 want: "B=b",
299 },
300 } {
301 if got := resource.NewSchemaless(test.kvs...).String(); got != test.want {
302 t.Errorf("Resource(%v).String() = %q, want %q", test.kvs, got, test.want)
303 }
304 }
305 }
306
307 const envVar = "OTEL_RESOURCE_ATTRIBUTES"
308
309 func TestMarshalJSON(t *testing.T) {
310 r := resource.NewSchemaless(attribute.Int64("A", 1), attribute.String("C", "D"))
311 data, err := json.Marshal(r)
312 require.NoError(t, err)
313 require.Equal(t,
314 `[{"Key":"A","Value":{"Type":"INT64","Value":1}},{"Key":"C","Value":{"Type":"STRING","Value":"D"}}]`,
315 string(data))
316 }
317
318 func TestNew(t *testing.T) {
319 tc := []struct {
320 name string
321 envars string
322 detectors []resource.Detector
323 options []resource.Option
324
325 resourceValues map[string]string
326 schemaURL string
327 isErr bool
328 }{
329 {
330 name: "No Options returns empty resource",
331 envars: "key=value,other=attr",
332 options: nil,
333 resourceValues: map[string]string{},
334 },
335 {
336 name: "Nil Detectors works",
337 envars: "key=value,other=attr",
338 options: []resource.Option{
339 resource.WithDetectors(),
340 },
341 resourceValues: map[string]string{},
342 },
343 {
344 name: "Only Host",
345 envars: "from=here",
346 options: []resource.Option{
347 resource.WithHost(),
348 },
349 resourceValues: map[string]string{
350 "host.name": hostname(),
351 },
352 schemaURL: semconv.SchemaURL,
353 },
354 {
355 name: "Only Env",
356 envars: "key=value,other=attr",
357 options: []resource.Option{
358 resource.WithFromEnv(),
359 },
360 resourceValues: map[string]string{
361 "key": "value",
362 "other": "attr",
363 },
364 },
365 {
366 name: "Only TelemetrySDK",
367 envars: "",
368 options: []resource.Option{
369 resource.WithTelemetrySDK(),
370 },
371 resourceValues: map[string]string{
372 "telemetry.sdk.name": "opentelemetry",
373 "telemetry.sdk.language": "go",
374 "telemetry.sdk.version": sdk.Version(),
375 },
376 schemaURL: semconv.SchemaURL,
377 },
378 {
379 name: "WithAttributes",
380 envars: "key=value,other=attr",
381 options: []resource.Option{
382 resource.WithAttributes(attribute.String("A", "B")),
383 },
384 resourceValues: map[string]string{
385 "A": "B",
386 },
387 },
388 {
389 name: "With schema url",
390 envars: "",
391 options: []resource.Option{
392 resource.WithAttributes(attribute.String("A", "B")),
393 resource.WithSchemaURL("https://opentelemetry.io/schemas/1.0.0"),
394 },
395 resourceValues: map[string]string{
396 "A": "B",
397 },
398 schemaURL: "https://opentelemetry.io/schemas/1.0.0",
399 },
400 {
401 name: "With conflicting schema urls",
402 envars: "",
403 options: []resource.Option{
404 resource.WithDetectors(
405 resource.StringDetector("https://opentelemetry.io/schemas/1.0.0", semconv.HostNameKey, os.Hostname),
406 ),
407 resource.WithSchemaURL("https://opentelemetry.io/schemas/1.1.0"),
408 },
409 resourceValues: map[string]string{},
410 schemaURL: "",
411 isErr: true,
412 },
413 {
414 name: "With conflicting detector schema urls",
415 envars: "",
416 options: []resource.Option{
417 resource.WithDetectors(
418 resource.StringDetector("https://opentelemetry.io/schemas/1.0.0", semconv.HostNameKey, os.Hostname),
419 resource.StringDetector("https://opentelemetry.io/schemas/1.1.0", semconv.HostNameKey, func() (string, error) { return "", errors.New("fail") }),
420 ),
421 resource.WithSchemaURL("https://opentelemetry.io/schemas/1.2.0"),
422 },
423 resourceValues: map[string]string{},
424 schemaURL: "",
425 isErr: true,
426 },
427 }
428 for _, tt := range tc {
429 t.Run(tt.name, func(t *testing.T) {
430 store, err := ottest.SetEnvVariables(map[string]string{
431 envVar: tt.envars,
432 })
433 require.NoError(t, err)
434 defer func() { require.NoError(t, store.Restore()) }()
435
436 ctx := context.Background()
437 res, err := resource.New(ctx, tt.options...)
438
439 if tt.isErr {
440 require.Error(t, err)
441 } else {
442 require.NoError(t, err)
443 }
444
445 require.EqualValues(t, tt.resourceValues, toMap(res))
446
447
448
449 if res != nil {
450 assert.EqualValues(t, tt.schemaURL, res.SchemaURL())
451 }
452 })
453 }
454 }
455
456 func TestNewWrapedError(t *testing.T) {
457 localErr := errors.New("local error")
458 _, err := resource.New(
459 context.Background(),
460 resource.WithDetectors(
461 resource.StringDetector("", "", func() (string, error) {
462 return "", localErr
463 }),
464 resource.StringDetector("", "", func() (string, error) {
465 return "", assert.AnError
466 }),
467 ),
468 )
469
470 assert.ErrorIs(t, err, localErr)
471 assert.ErrorIs(t, err, assert.AnError)
472 assert.NotErrorIs(t, err, errors.New("false positive error"))
473 }
474
475 func TestWithHostID(t *testing.T) {
476 mockHostIDProvider()
477 t.Cleanup(restoreHostIDProvider)
478
479 ctx := context.Background()
480
481 res, err := resource.New(ctx,
482 resource.WithHostID(),
483 )
484
485 require.NoError(t, err)
486 require.EqualValues(t, map[string]string{
487 "host.id": "f2c668b579780554f70f72a063dc0864",
488 }, toMap(res))
489 }
490
491 func TestWithHostIDError(t *testing.T) {
492 mockHostIDProviderWithError()
493 t.Cleanup(restoreHostIDProvider)
494
495 ctx := context.Background()
496
497 res, err := resource.New(ctx,
498 resource.WithHostID(),
499 )
500
501 assert.ErrorIs(t, err, assert.AnError)
502 require.EqualValues(t, map[string]string{}, toMap(res))
503 }
504
505 func TestWithOSType(t *testing.T) {
506 mockRuntimeProviders()
507 t.Cleanup(restoreAttributesProviders)
508
509 ctx := context.Background()
510
511 res, err := resource.New(ctx,
512 resource.WithOSType(),
513 )
514
515 require.NoError(t, err)
516 require.EqualValues(t, map[string]string{
517 "os.type": "linux",
518 }, toMap(res))
519 }
520
521 func TestWithOSDescription(t *testing.T) {
522 mockRuntimeProviders()
523 t.Cleanup(restoreAttributesProviders)
524
525 ctx := context.Background()
526
527 res, err := resource.New(ctx,
528 resource.WithOSDescription(),
529 )
530
531 require.NoError(t, err)
532 require.EqualValues(t, map[string]string{
533 "os.description": "Test",
534 }, toMap(res))
535 }
536
537 func TestWithOS(t *testing.T) {
538 mockRuntimeProviders()
539 t.Cleanup(restoreAttributesProviders)
540
541 ctx := context.Background()
542
543 res, err := resource.New(ctx,
544 resource.WithOS(),
545 )
546
547 require.NoError(t, err)
548 require.EqualValues(t, map[string]string{
549 "os.type": "linux",
550 "os.description": "Test",
551 }, toMap(res))
552 }
553
554 func TestWithProcessPID(t *testing.T) {
555 mockProcessAttributesProvidersWithErrors()
556 ctx := context.Background()
557
558 res, err := resource.New(ctx,
559 resource.WithProcessPID(),
560 )
561
562 require.NoError(t, err)
563 require.EqualValues(t, map[string]string{
564 "process.pid": fmt.Sprint(fakePID),
565 }, toMap(res))
566 }
567
568 func TestWithProcessExecutableName(t *testing.T) {
569 mockProcessAttributesProvidersWithErrors()
570 ctx := context.Background()
571
572 res, err := resource.New(ctx,
573 resource.WithProcessExecutableName(),
574 )
575
576 require.NoError(t, err)
577 require.EqualValues(t, map[string]string{
578 "process.executable.name": fakeExecutableName,
579 }, toMap(res))
580 }
581
582 func TestWithProcessExecutablePath(t *testing.T) {
583 mockProcessAttributesProviders()
584 ctx := context.Background()
585
586 res, err := resource.New(ctx,
587 resource.WithProcessExecutablePath(),
588 )
589
590 require.NoError(t, err)
591 require.EqualValues(t, map[string]string{
592 "process.executable.path": fakeExecutablePath,
593 }, toMap(res))
594 }
595
596 func TestWithProcessCommandArgs(t *testing.T) {
597 mockProcessAttributesProvidersWithErrors()
598 ctx := context.Background()
599
600 res, err := resource.New(ctx,
601 resource.WithProcessCommandArgs(),
602 )
603
604 require.NoError(t, err)
605 require.EqualValues(t, map[string]string{
606 "process.command_args": fmt.Sprint(fakeCommandArgs),
607 }, toMap(res))
608 }
609
610 func TestWithProcessOwner(t *testing.T) {
611 mockProcessAttributesProviders()
612 ctx := context.Background()
613
614 res, err := resource.New(ctx,
615 resource.WithProcessOwner(),
616 )
617
618 require.NoError(t, err)
619 require.EqualValues(t, map[string]string{
620 "process.owner": fakeOwner,
621 }, toMap(res))
622 }
623
624 func TestWithProcessRuntimeName(t *testing.T) {
625 mockProcessAttributesProvidersWithErrors()
626 ctx := context.Background()
627
628 res, err := resource.New(ctx,
629 resource.WithProcessRuntimeName(),
630 )
631
632 require.NoError(t, err)
633 require.EqualValues(t, map[string]string{
634 "process.runtime.name": fakeRuntimeName,
635 }, toMap(res))
636 }
637
638 func TestWithProcessRuntimeVersion(t *testing.T) {
639 mockProcessAttributesProvidersWithErrors()
640 ctx := context.Background()
641
642 res, err := resource.New(ctx,
643 resource.WithProcessRuntimeVersion(),
644 )
645
646 require.NoError(t, err)
647 require.EqualValues(t, map[string]string{
648 "process.runtime.version": fakeRuntimeVersion,
649 }, toMap(res))
650 }
651
652 func TestWithProcessRuntimeDescription(t *testing.T) {
653 mockProcessAttributesProvidersWithErrors()
654 ctx := context.Background()
655
656 res, err := resource.New(ctx,
657 resource.WithProcessRuntimeDescription(),
658 )
659
660 require.NoError(t, err)
661 require.EqualValues(t, map[string]string{
662 "process.runtime.description": fakeRuntimeDescription,
663 }, toMap(res))
664 }
665
666 func TestWithProcess(t *testing.T) {
667 mockProcessAttributesProviders()
668 ctx := context.Background()
669
670 res, err := resource.New(ctx,
671 resource.WithProcess(),
672 )
673
674 require.NoError(t, err)
675 require.EqualValues(t, map[string]string{
676 "process.pid": fmt.Sprint(fakePID),
677 "process.executable.name": fakeExecutableName,
678 "process.executable.path": fakeExecutablePath,
679 "process.command_args": fmt.Sprint(fakeCommandArgs),
680 "process.owner": fakeOwner,
681 "process.runtime.name": fakeRuntimeName,
682 "process.runtime.version": fakeRuntimeVersion,
683 "process.runtime.description": fakeRuntimeDescription,
684 }, toMap(res))
685 }
686
687 func toMap(res *resource.Resource) map[string]string {
688 m := map[string]string{}
689 for _, attr := range res.Attributes() {
690 m[string(attr.Key)] = attr.Value.Emit()
691 }
692 return m
693 }
694
695 func hostname() string {
696 hn, err := os.Hostname()
697 if err != nil {
698 return fmt.Sprintf("hostname(%s)", err)
699 }
700 return hn
701 }
702
703 func TestWithContainerID(t *testing.T) {
704 t.Cleanup(restoreAttributesProviders)
705
706 fakeContainerID := "fake-container-id"
707
708 testCases := []struct {
709 name string
710 containerIDProvider func() (string, error)
711 expectedResource map[string]string
712 expectedErr bool
713 }{
714 {
715 name: "get container id",
716 containerIDProvider: func() (string, error) {
717 return fakeContainerID, nil
718 },
719 expectedResource: map[string]string{
720 string(semconv.ContainerIDKey): fakeContainerID,
721 },
722 },
723 {
724 name: "no container id found",
725 containerIDProvider: func() (string, error) {
726 return "", nil
727 },
728 expectedResource: map[string]string{},
729 },
730 {
731 name: "error",
732 containerIDProvider: func() (string, error) {
733 return "", fmt.Errorf("unable to get container id")
734 },
735 expectedResource: map[string]string{},
736 expectedErr: true,
737 },
738 }
739
740 for _, tc := range testCases {
741 t.Run(tc.name, func(t *testing.T) {
742 resource.SetContainerProviders(tc.containerIDProvider)
743
744 res, err := resource.New(context.Background(),
745 resource.WithContainerID(),
746 )
747
748 if tc.expectedErr {
749 assert.Error(t, err)
750 }
751 assert.Equal(t, tc.expectedResource, toMap(res))
752 })
753 }
754 }
755
756 func TestWithContainer(t *testing.T) {
757 t.Cleanup(restoreAttributesProviders)
758
759 fakeContainerID := "fake-container-id"
760 resource.SetContainerProviders(func() (string, error) {
761 return fakeContainerID, nil
762 })
763
764 res, err := resource.New(context.Background(),
765 resource.WithContainer(),
766 )
767
768 assert.NoError(t, err)
769 assert.Equal(t, map[string]string{
770 string(semconv.ContainerIDKey): fakeContainerID,
771 }, toMap(res))
772 }
773
774 func TestResourceConcurrentSafe(t *testing.T) {
775
776
777 var wg sync.WaitGroup
778 for i := 0; i < 2; i++ {
779 wg.Add(1)
780 go func() {
781 defer wg.Done()
782 d := &fakeDetector{}
783 _, err := resource.Detect(context.Background(), d)
784 assert.NoError(t, err)
785 }()
786 }
787 wg.Wait()
788 }
789
790 type fakeDetector struct{}
791
792 func (f fakeDetector) Detect(_ context.Context) (*resource.Resource, error) {
793
794
795 return resource.NewWithAttributes("https://opentelemetry.io/schemas/1.3.0"), nil
796 }
797
798 var _ resource.Detector = &fakeDetector{}
799
View as plain text