blob: 09b91e1e1b0f591b92b3c40929c7867086544f67 [file] [log] [blame]
Abhay Kumar40252eb2025-10-13 13:25:53 +00001// Copyright The OpenTelemetry Authors
2// SPDX-License-Identifier: Apache-2.0
3
4package resource // import "go.opentelemetry.io/otel/sdk/resource"
5
6import (
7 "context"
8 "errors"
9 "fmt"
10 "sync"
11
12 "go.opentelemetry.io/otel"
13 "go.opentelemetry.io/otel/attribute"
14 "go.opentelemetry.io/otel/sdk/internal/x"
15)
16
17// Resource describes an entity about which identifying information
18// and metadata is exposed. Resource is an immutable object,
19// equivalent to a map from key to unique value.
20//
21// Resources should be passed and stored as pointers
22// (`*resource.Resource`). The `nil` value is equivalent to an empty
23// Resource.
24//
25// Note that the Go == operator compares not just the resource attributes but
26// also all other internals of the Resource type. Therefore, Resource values
27// should not be used as map or database keys. In general, the [Resource.Equal]
28// method should be used instead of direct comparison with ==, since that
29// method ensures the correct comparison of resource attributes, and the
30// [attribute.Distinct] returned from [Resource.Equivalent] should be used for
31// map and database keys instead.
32type Resource struct {
33 attrs attribute.Set
34 schemaURL string
35}
36
37// Compile-time check that the Resource remains comparable.
38var _ map[Resource]struct{} = nil
39
40var (
41 defaultResource *Resource
42 defaultResourceOnce sync.Once
43)
44
45// ErrSchemaURLConflict is an error returned when two Resources are merged
46// together that contain different, non-empty, schema URLs.
47var ErrSchemaURLConflict = errors.New("conflicting Schema URL")
48
49// New returns a [Resource] built using opts.
50//
51// This may return a partial Resource along with an error containing
52// [ErrPartialResource] if options that provide a [Detector] are used and that
53// error is returned from one or more of the Detectors. It may also return a
54// merge-conflict Resource along with an error containing
55// [ErrSchemaURLConflict] if merging Resources from the opts results in a
56// schema URL conflict (see [Resource.Merge] for more information). It is up to
57// the caller to determine if this returned Resource should be used or not
58// based on these errors.
59func New(ctx context.Context, opts ...Option) (*Resource, error) {
60 cfg := config{}
61 for _, opt := range opts {
62 cfg = opt.apply(cfg)
63 }
64
65 r := &Resource{schemaURL: cfg.schemaURL}
66 return r, detect(ctx, r, cfg.detectors)
67}
68
69// NewWithAttributes creates a resource from attrs and associates the resource with a
70// schema URL. If attrs contains duplicate keys, the last value will be used. If attrs
71// contains any invalid items those items will be dropped. The attrs are assumed to be
72// in a schema identified by schemaURL.
73func NewWithAttributes(schemaURL string, attrs ...attribute.KeyValue) *Resource {
74 resource := NewSchemaless(attrs...)
75 resource.schemaURL = schemaURL
76 return resource
77}
78
79// NewSchemaless creates a resource from attrs. If attrs contains duplicate keys,
80// the last value will be used. If attrs contains any invalid items those items will
81// be dropped. The resource will not be associated with a schema URL. If the schema
82// of the attrs is known use NewWithAttributes instead.
83func NewSchemaless(attrs ...attribute.KeyValue) *Resource {
84 if len(attrs) == 0 {
85 return &Resource{}
86 }
87
88 // Ensure attributes comply with the specification:
89 // https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/common/README.md#attribute
90 s, _ := attribute.NewSetWithFiltered(attrs, func(kv attribute.KeyValue) bool {
91 return kv.Valid()
92 })
93
94 // If attrs only contains invalid entries do not allocate a new resource.
95 if s.Len() == 0 {
96 return &Resource{}
97 }
98
99 return &Resource{attrs: s} //nolint
100}
101
102// String implements the Stringer interface and provides a
103// human-readable form of the resource.
104//
105// Avoid using this representation as the key in a map of resources,
106// use Equivalent() as the key instead.
107func (r *Resource) String() string {
108 if r == nil {
109 return ""
110 }
111 return r.attrs.Encoded(attribute.DefaultEncoder())
112}
113
114// MarshalLog is the marshaling function used by the logging system to represent this Resource.
115func (r *Resource) MarshalLog() interface{} {
116 return struct {
117 Attributes attribute.Set
118 SchemaURL string
119 }{
120 Attributes: r.attrs,
121 SchemaURL: r.schemaURL,
122 }
123}
124
125// Attributes returns a copy of attributes from the resource in a sorted order.
126// To avoid allocating a new slice, use an iterator.
127func (r *Resource) Attributes() []attribute.KeyValue {
128 if r == nil {
129 r = Empty()
130 }
131 return r.attrs.ToSlice()
132}
133
134// SchemaURL returns the schema URL associated with Resource r.
135func (r *Resource) SchemaURL() string {
136 if r == nil {
137 return ""
138 }
139 return r.schemaURL
140}
141
142// Iter returns an iterator of the Resource attributes.
143// This is ideal to use if you do not want a copy of the attributes.
144func (r *Resource) Iter() attribute.Iterator {
145 if r == nil {
146 r = Empty()
147 }
148 return r.attrs.Iter()
149}
150
151// Equal returns whether r and o represent the same resource. Two resources can
152// be equal even if they have different schema URLs.
153//
154// See the documentation on the [Resource] type for the pitfalls of using ==
155// with Resource values; most code should use Equal instead.
156func (r *Resource) Equal(o *Resource) bool {
157 if r == nil {
158 r = Empty()
159 }
160 if o == nil {
161 o = Empty()
162 }
163 return r.Equivalent() == o.Equivalent()
164}
165
166// Merge creates a new [Resource] by merging a and b.
167//
168// If there are common keys between a and b, then the value from b will
169// overwrite the value from a, even if b's value is empty.
170//
171// The SchemaURL of the resources will be merged according to the
172// [OpenTelemetry specification rules]:
173//
174// - If a's schema URL is empty then the returned Resource's schema URL will
175// be set to the schema URL of b,
176// - Else if b's schema URL is empty then the returned Resource's schema URL
177// will be set to the schema URL of a,
178// - Else if the schema URLs of a and b are the same then that will be the
179// schema URL of the returned Resource,
180// - Else this is a merging error. If the resources have different,
181// non-empty, schema URLs an error containing [ErrSchemaURLConflict] will
182// be returned with the merged Resource. The merged Resource will have an
183// empty schema URL. It may be the case that some unintended attributes
184// have been overwritten or old semantic conventions persisted in the
185// returned Resource. It is up to the caller to determine if this returned
186// Resource should be used or not.
187//
188// [OpenTelemetry specification rules]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/resource/sdk.md#merge
189func Merge(a, b *Resource) (*Resource, error) {
190 if a == nil && b == nil {
191 return Empty(), nil
192 }
193 if a == nil {
194 return b, nil
195 }
196 if b == nil {
197 return a, nil
198 }
199
200 // Note: 'b' attributes will overwrite 'a' with last-value-wins in attribute.Key()
201 // Meaning this is equivalent to: append(a.Attributes(), b.Attributes()...)
202 mi := attribute.NewMergeIterator(b.Set(), a.Set())
203 combine := make([]attribute.KeyValue, 0, a.Len()+b.Len())
204 for mi.Next() {
205 combine = append(combine, mi.Attribute())
206 }
207
208 switch {
209 case a.schemaURL == "":
210 return NewWithAttributes(b.schemaURL, combine...), nil
211 case b.schemaURL == "":
212 return NewWithAttributes(a.schemaURL, combine...), nil
213 case a.schemaURL == b.schemaURL:
214 return NewWithAttributes(a.schemaURL, combine...), nil
215 }
216 // Return the merged resource with an appropriate error. It is up to
217 // the user to decide if the returned resource can be used or not.
218 return NewSchemaless(combine...), fmt.Errorf(
219 "%w: %s and %s",
220 ErrSchemaURLConflict,
221 a.schemaURL,
222 b.schemaURL,
223 )
224}
225
226// Empty returns an instance of Resource with no attributes. It is
227// equivalent to a `nil` Resource.
228func Empty() *Resource {
229 return &Resource{}
230}
231
232// Default returns an instance of Resource with a default
233// "service.name" and OpenTelemetrySDK attributes.
234func Default() *Resource {
235 defaultResourceOnce.Do(func() {
236 var err error
237 defaultDetectors := []Detector{
238 defaultServiceNameDetector{},
239 fromEnv{},
240 telemetrySDK{},
241 }
242 if x.Resource.Enabled() {
243 defaultDetectors = append([]Detector{defaultServiceInstanceIDDetector{}}, defaultDetectors...)
244 }
245 defaultResource, err = Detect(
246 context.Background(),
247 defaultDetectors...,
248 )
249 if err != nil {
250 otel.Handle(err)
251 }
252 // If Detect did not return a valid resource, fall back to emptyResource.
253 if defaultResource == nil {
254 defaultResource = &Resource{}
255 }
256 })
257 return defaultResource
258}
259
260// Environment returns an instance of Resource with attributes
261// extracted from the OTEL_RESOURCE_ATTRIBUTES environment variable.
262func Environment() *Resource {
263 detector := &fromEnv{}
264 resource, err := detector.Detect(context.Background())
265 if err != nil {
266 otel.Handle(err)
267 }
268 return resource
269}
270
271// Equivalent returns an object that can be compared for equality
272// between two resources. This value is suitable for use as a key in
273// a map.
274func (r *Resource) Equivalent() attribute.Distinct {
275 return r.Set().Equivalent()
276}
277
278// Set returns the equivalent *attribute.Set of this resource's attributes.
279func (r *Resource) Set() *attribute.Set {
280 if r == nil {
281 r = Empty()
282 }
283 return &r.attrs
284}
285
286// MarshalJSON encodes the resource attributes as a JSON list of { "Key":
287// "...", "Value": ... } pairs in order sorted by key.
288func (r *Resource) MarshalJSON() ([]byte, error) {
289 if r == nil {
290 r = Empty()
291 }
292 return r.attrs.MarshalJSON()
293}
294
295// Len returns the number of unique key-values in this Resource.
296func (r *Resource) Len() int {
297 if r == nil {
298 return 0
299 }
300 return r.attrs.Len()
301}
302
303// Encoded returns an encoded representation of the resource.
304func (r *Resource) Encoded(enc attribute.Encoder) string {
305 if r == nil {
306 return ""
307 }
308 return r.attrs.Encoded(enc)
309}