55
55
above example.
56
56
"""
57
57
58
- import abc
59
58
import concurrent .futures
60
59
import logging
61
60
import os
62
61
import platform
63
62
import sys
64
- import typing
63
+ from abc import ABC , abstractmethod
65
64
from json import dumps
66
65
from os import environ
67
66
from types import ModuleType
68
- from typing import List , MutableMapping , Optional , cast
67
+ from typing import List , Mapping , MutableMapping , Optional , cast
69
68
from urllib import parse
69
+ from warnings import warn
70
70
71
71
from opentelemetry .attributes import BoundedAttributes
72
72
from opentelemetry .sdk .environment_variables import (
88
88
pass
89
89
90
90
LabelValue = AttributeValue
91
- Attributes = typing . Mapping [str , LabelValue ]
91
+ Attributes = Mapping [str , LabelValue ]
92
92
logger = logging .getLogger (__name__ )
93
93
94
94
CLOUD_PROVIDER = ResourceAttributes .CLOUD_PROVIDER
152
152
_OPENTELEMETRY_SDK_VERSION : str = version ("opentelemetry-sdk" )
153
153
154
154
155
+ class Entity :
156
+
157
+ def __init__ (
158
+ self ,
159
+ type_ : str ,
160
+ id_ : Mapping [str , str ],
161
+ attributes : Optional [Attributes ] = None ,
162
+ schema_url : Optional [str ] = None ,
163
+ ):
164
+
165
+ if not type_ :
166
+ raise Exception ("Entity type must not be empty" )
167
+
168
+ if attributes is None :
169
+ attributes = {}
170
+
171
+ self ._type = type_
172
+
173
+ # These are attributes that identify the entity and must not change
174
+ # during the lifetime of the entity. id_ must contain at least one
175
+ # attribute.
176
+ if not id_ :
177
+ raise Exception ("Entity id must not be empty" )
178
+
179
+ self ._id = id_
180
+
181
+ # These are attributes that do not identify the entity and may change
182
+ # during the lifetime of the entity.
183
+ self ._attributes = attributes
184
+
185
+ if schema_url is None :
186
+ schema_url = ""
187
+
188
+ self ._schema_url = schema_url
189
+
190
+ @property
191
+ def type (self ):
192
+ return self ._type
193
+
194
+ @property
195
+ def id (self ):
196
+ # FIXME we need a checker here that makes sure that the id attributes
197
+ # are compliant with the spec. Not implementing it here since this
198
+ # seems like a thing that should be available to other components as
199
+ # well.
200
+ return self ._id
201
+
202
+ @property
203
+ def attributes (self ):
204
+ return self ._attributes
205
+
206
+ @property
207
+ def schema_url (self ):
208
+ return self ._schema_url
209
+
210
+
155
211
class Resource :
156
212
"""A Resource is an immutable representation of the entity producing telemetry as Attributes."""
157
213
158
214
_attributes : BoundedAttributes
159
215
_schema_url : str
160
216
161
217
def __init__ (
162
- self , attributes : Attributes , schema_url : typing .Optional [str ] = None
218
+ self ,
219
+ attributes : Attributes ,
220
+ schema_url : Optional [str ] = None ,
221
+ * entities ,
163
222
):
164
223
self ._attributes = BoundedAttributes (attributes = attributes )
165
224
if schema_url is None :
166
225
schema_url = ""
226
+ else :
227
+ warn ("Resource.schema_url is deprecated" , DeprecationWarning )
228
+
167
229
self ._schema_url = schema_url
168
230
231
+ # FIXME the spec draft says the Resource will have an EntityRef proto
232
+ # figure out what that is supposed to be.
233
+ self ._entities = entities
234
+
169
235
@staticmethod
170
236
def create (
171
- attributes : typing . Optional [Attributes ] = None ,
172
- schema_url : typing . Optional [str ] = None ,
237
+ attributes : Optional [Attributes ] = None ,
238
+ schema_url : Optional [str ] = None ,
173
239
) -> "Resource" :
174
240
"""Creates a new `Resource` from attributes.
175
241
@@ -181,7 +247,7 @@ def create(
181
247
The newly-created Resource.
182
248
"""
183
249
184
- if not attributes :
250
+ if attributes is None :
185
251
attributes = {}
186
252
187
253
otel_experimental_resource_detectors = {"otel" }.union (
@@ -225,6 +291,51 @@ def create(
225
291
)
226
292
return resource
227
293
294
+ @staticmethod
295
+ def create_using_entities (
296
+ attributes : Optional [Attributes ] = None ,
297
+ schema_url : Optional [str ] = None ,
298
+ ) -> "Resource" :
299
+ # This method is added here with the intention of not disturbing the
300
+ # previous API create method for backwards compatibility reasons.
301
+
302
+ if attributes is None :
303
+ attributes = {}
304
+
305
+ if schema_url is None :
306
+ schema_url = ""
307
+
308
+ selected_entities = _select_entities (
309
+ [
310
+ entity_detector .detect ()
311
+ for entity_detector in _get_entity_detectors ()
312
+ ]
313
+ )
314
+
315
+ resource_attributes = {}
316
+
317
+ for selected_entity in selected_entities :
318
+ for key , value in selected_entity ._id .items ():
319
+ resource_attributes [key ] = value
320
+
321
+ for key , value in selected_entity ._attributes .items ():
322
+ resource_attributes [key ] = value
323
+
324
+ entity_schema_url = selected_entities [0 ].schema_url
325
+
326
+ for selected_entity in selected_entities :
327
+ if selected_entity .schema_url != entity_schema_url :
328
+ entity_schema_url = None
329
+ break
330
+
331
+ resource_attributes .update (attributes )
332
+
333
+ resource = Resource .create (
334
+ attributes = resource_attributes , schema_url = entity_schema_url
335
+ )
336
+
337
+ return resource
338
+
228
339
@staticmethod
229
340
def get_empty () -> "Resource" :
230
341
return _EMPTY_RESOURCE
@@ -298,6 +409,72 @@ def to_json(self, indent: int = 4) -> str:
298
409
)
299
410
300
411
412
+ def _get_entity_detectors ():
413
+
414
+ entity_detectors : List [ResourceDetector ] = []
415
+
416
+ for entity_detector in entry_points (
417
+ group = "opentelemetry_entity_detector" ,
418
+ ):
419
+
420
+ entity_detectors .append (entity_detector .load ()())
421
+
422
+ # This checker is added here but it could live in the configuration
423
+ # mechanism, so that it detects a possible error when 2 entity
424
+ # detectors have the same priority even earlier.
425
+ if len (entity_detectors ) > 1 :
426
+
427
+ sorted_entity_detectors = sorted (
428
+ entity_detectors , key = lambda x : x .priority
429
+ )
430
+
431
+ priorities = set ()
432
+
433
+ for entity_detector in sorted_entity_detectors :
434
+
435
+ if entity_detector .priority in priorities :
436
+ raise ValueError (
437
+ f"Duplicate priority { entity_detector .priority } "
438
+ f"for entity detector of type { type (entity_detector )} "
439
+ )
440
+
441
+ priorities .add (entity_detector .priority )
442
+
443
+ return entity_detectors
444
+
445
+
446
+ def _select_entities (unselected_entities ):
447
+
448
+ selected_entities = [unselected_entities .pop (0 )]
449
+
450
+ for unselected_entity in unselected_entities :
451
+
452
+ for selected_entity in selected_entities :
453
+
454
+ if selected_entity .type == unselected_entity .type :
455
+ if (
456
+ selected_entity .id == unselected_entity .id
457
+ and selected_entity .schema_url
458
+ == (unselected_entity .schema_url )
459
+ ):
460
+ for key , value in unselected_entity .attributes .items ():
461
+ if key not in selected_entity .attributes .keys ():
462
+ selected_entity ._attributes [key ] = value
463
+ break
464
+ elif (
465
+ selected_entity .id == unselected_entity .id
466
+ and selected_entity .schema_url
467
+ != (unselected_entity .schema_url )
468
+ ):
469
+ break
470
+ elif selected_entity .id != unselected_entity .id :
471
+ break
472
+ else :
473
+ selected_entities .append (unselected_entity )
474
+
475
+ return selected_entities
476
+
477
+
301
478
_EMPTY_RESOURCE = Resource ({})
302
479
_DEFAULT_RESOURCE = Resource (
303
480
{
@@ -308,19 +485,89 @@ def to_json(self, indent: int = 4) -> str:
308
485
)
309
486
310
487
311
- class ResourceDetector ( abc . ABC ):
488
+ class Detector ( ABC ):
312
489
def __init__ (self , raise_on_error : bool = False ) -> None :
313
490
self .raise_on_error = raise_on_error
314
491
315
- @abc .abstractmethod
492
+ @abstractmethod
493
+ def detect (self ):
494
+ raise NotImplementedError ()
495
+
496
+
497
+ class ResourceDetector (Detector ):
498
+ @abstractmethod
316
499
def detect (self ) -> "Resource" :
317
500
raise NotImplementedError ()
318
501
319
502
503
+ class EntityDetector (Detector ):
504
+ def __init__ (self , raise_on_error : bool = False ) -> None :
505
+ self .raise_on_error = raise_on_error
506
+
507
+ @abstractmethod
508
+ def detect (self ) -> "Entity" :
509
+ raise NotImplementedError ()
510
+
511
+ @property
512
+ @abstractmethod
513
+ def priority (self ):
514
+ raise NotImplementedError ()
515
+
516
+
517
+ class Type0EntityDetector (EntityDetector ):
518
+
519
+ _entity = None
520
+
521
+ def detect (self ) -> Entity :
522
+
523
+ if self ._entity is None :
524
+ # The OTEP says an entity detector must not provide two entities of
525
+ # the same type. It seems to me that this means it will only
526
+ # provide one entity at all since the entity detector is associated
527
+ # to a particular "type" (process, OS, etc)
528
+ self ._entity = Entity (
529
+ "type0" , id_ = {"a" : "b" }, attributes = {"c" : "d" }
530
+ )
531
+
532
+ return self ._entity
533
+
534
+ @property
535
+ def priority (self ):
536
+ # This probably needs a configuration mechanism so that it can get its
537
+ # priority from some configuration file or something else.
538
+ return 0
539
+
540
+
541
+ class Type1EntityDetector (EntityDetector ):
542
+
543
+ _entity = None
544
+
545
+ def detect (self ) -> Entity :
546
+
547
+ if self ._entity is None :
548
+ # The OTEP says an entity detector must not provide two entities of
549
+ # the same type. It seems to me that this means it will only
550
+ # provide one entity at all since the entity detector is associated
551
+ # to a particular "type" (process, OS, etc)
552
+
553
+ self ._entity = Entity (
554
+ "type1" , id_ = {"a" : "b" }, attributes = {"c" : "d" }
555
+ )
556
+
557
+ return self ._entity
558
+
559
+ @property
560
+ def priority (self ):
561
+ # This probably needs a configuration mechanism so that it can get its
562
+ # priority from some configuration file or something else.
563
+ return 1
564
+
565
+
320
566
class OTELResourceDetector (ResourceDetector ):
321
567
# pylint: disable=no-self-use
322
568
def detect (self ) -> "Resource" :
323
569
570
+ # An example of OTEL_RESOURCE_ATTRIBUTES is "key0=value0,key1=value1"
324
571
env_resources_items = environ .get (OTEL_RESOURCE_ATTRIBUTES )
325
572
env_resource_map = {}
326
573
@@ -472,8 +719,8 @@ def detect(self) -> "Resource":
472
719
473
720
474
721
def get_aggregated_resources (
475
- detectors : typing . List ["ResourceDetector" ],
476
- initial_resource : typing . Optional [Resource ] = None ,
722
+ detectors : List ["ResourceDetector" ],
723
+ initial_resource : Optional [Resource ] = None ,
477
724
timeout : int = 5 ,
478
725
) -> "Resource" :
479
726
"""Retrieves resources from detectors in the order that they were passed
0 commit comments