| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame^] | 1 | // Copyright The OpenTelemetry Authors |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| 4 | package resource // import "go.opentelemetry.io/otel/sdk/resource" |
| 5 | |
| 6 | import ( |
| 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. |
| 32 | type Resource struct { |
| 33 | attrs attribute.Set |
| 34 | schemaURL string |
| 35 | } |
| 36 | |
| 37 | // Compile-time check that the Resource remains comparable. |
| 38 | var _ map[Resource]struct{} = nil |
| 39 | |
| 40 | var ( |
| 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. |
| 47 | var 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. |
| 59 | func 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. |
| 73 | func 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. |
| 83 | func 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. |
| 107 | func (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. |
| 115 | func (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. |
| 127 | func (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. |
| 135 | func (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. |
| 144 | func (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. |
| 156 | func (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 |
| 189 | func 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. |
| 228 | func Empty() *Resource { |
| 229 | return &Resource{} |
| 230 | } |
| 231 | |
| 232 | // Default returns an instance of Resource with a default |
| 233 | // "service.name" and OpenTelemetrySDK attributes. |
| 234 | func 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. |
| 262 | func 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. |
| 274 | func (r *Resource) Equivalent() attribute.Distinct { |
| 275 | return r.Set().Equivalent() |
| 276 | } |
| 277 | |
| 278 | // Set returns the equivalent *attribute.Set of this resource's attributes. |
| 279 | func (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. |
| 288 | func (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. |
| 296 | func (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. |
| 304 | func (r *Resource) Encoded(enc attribute.Encoder) string { |
| 305 | if r == nil { |
| 306 | return "" |
| 307 | } |
| 308 | return r.attrs.Encoded(enc) |
| 309 | } |