Skip to content

Commit

Permalink
Add entities prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
ocelotl committed Sep 22, 2024
1 parent f15821f commit 479d175
Show file tree
Hide file tree
Showing 3 changed files with 304 additions and 4 deletions.
4 changes: 4 additions & 0 deletions opentelemetry-sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ otel = "opentelemetry.sdk.resources:OTELResourceDetector"
process = "opentelemetry.sdk.resources:ProcessResourceDetector"
os = "opentelemetry.sdk.resources:OsResourceDetector"

[project.entry-points.opentelemetry_entity_detector]
type0= "opentelemetry.sdk.resources:Type0EntityDetector"
type1= "opentelemetry.sdk.resources:Type1EntityDetector"

[project.urls]
Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk"

Expand Down
210 changes: 206 additions & 4 deletions opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,19 @@
above example.
"""

import abc
import concurrent.futures
import logging
import os
import platform
import sys
import typing
from abc import ABC, abstractmethod
from json import dumps
from os import environ
from types import ModuleType
from typing import List, MutableMapping, Optional, cast
from urllib import parse
from warnings import warn

from opentelemetry.attributes import BoundedAttributes
from opentelemetry.sdk.environment_variables import (
Expand Down Expand Up @@ -152,20 +153,72 @@
_OPENTELEMETRY_SDK_VERSION: str = version("opentelemetry-sdk")


class Entity:

def __init__(self, type_: str, id_, attributes, schema_url: str):

if not type_:
raise Exception("Entity type must not be empty")

self._type = type_

# These are attributes that identify the entity and must not change
# during the lifetime of the entity. id_ must contain at least one
# attribute.
if not id_:
raise Exception("Entity id must not be empty")

self._id = id_

# These are attributes that do not identify the entity and may change
# during the lifetime of the entity.
self._attributes = attributes

self._schema_url = schema_url

@property
def type(self):
return self._type

@property
def id(self):
# FIXME we need a checker here that makes sure that the id attributes
# are compliant with the spec. Not implementing it here since this
# seems like a thing that should be available to other components as
# well.
return self._id

@property
def attributes(self):
return self._attributes

@property
def schema_url(self):
return self._schema_url


class Resource:
"""A Resource is an immutable representation of the entity producing telemetry as Attributes."""

_attributes: BoundedAttributes
_schema_url: str

def __init__(
self, attributes: Attributes, schema_url: typing.Optional[str] = None
self,
attributes: Attributes,
schema_url: typing.Optional[str] = None,
*entities,
):
self._attributes = BoundedAttributes(attributes=attributes)
if schema_url is None:
schema_url = ""
else:
warn("Resource.schema_url is deprecated", DeprecationWarning)

self._schema_url = schema_url

self._entities = entities

@staticmethod
def create(
attributes: typing.Optional[Attributes] = None,
Expand Down Expand Up @@ -225,6 +278,58 @@ def create(
)
return resource

@staticmethod
def create_using_entities(
attributes: typing.Optional[Attributes] = None,
schema_url: typing.Optional[str] = None,
) -> "Resource":

entity_detectors: List[ResourceDetector] = []

for entity_detector in entry_points(
group="opentelemetry_entity_detector",
):

entity_detectors.append(entity_detector.load()())

# This checker is added here but it could live in the configuration
# mechanism, so that it detects a possible error when 2 entity
# detectors have the same priority even earlier.
if len(entity_detectors) > 1:

sorted_entity_detectors = sorted(
entity_detectors, key=lambda x: x.priority
)

priorities = set()

for entity_detector in sorted_entity_detectors:

if entity_detector.priority in priorities:
raise ValueError(
f"Duplicate priority {entity_detector.priority}"
f"for entity detector of type {type(entity_detector)}"
)

priorities.add(entity_detector)

selected_entities = _select_entities(
entity_detector.detect() for entity in entity_detectors
)

if len(selected_entities) > 1:

entity_schema_url = selected_entities[0].schema_url

for selected_entity in selected_entities[1]:
if selected_entity.schema_url != entity_schema_url:
break

else:
return Resource.create(schema_url=entity_schema_url)

return Resource.create()

@staticmethod
def get_empty() -> "Resource":
return _EMPTY_RESOURCE
Expand Down Expand Up @@ -298,6 +403,38 @@ def to_json(self, indent: int = 4) -> str:
)


def _select_entities(unselected_entities):

selected_entities = [unselected_entities.pop(0)]

for unselected_entity in unselected_entities:

for selected_entity in selected_entities:

if selected_entity.type == unselected_entity.type:
if (
selected_entity.id == unselected_entity.id
and selected_entity.schema_url
== (unselected_entity.schema_url)
):
for key, value in unselected_entity.attributes.items():
if key not in selected_entity.attributes.keys():
selected_entity._attributes[key] = value
break
elif (
selected_entity.id == unselected_entity.id
and selected_entity.schema_url
!= (unselected_entity.schema_url)
):
break
elif selected_entity.id != unselected_entity.id:
break
else:
selected_entities.append(unselected_entity)

return selected_entities


_EMPTY_RESOURCE = Resource({})
_DEFAULT_RESOURCE = Resource(
{
Expand All @@ -308,19 +445,84 @@ def to_json(self, indent: int = 4) -> str:
)


class ResourceDetector(abc.ABC):
class Detector(ABC):
def __init__(self, raise_on_error: bool = False) -> None:
self.raise_on_error = raise_on_error

@abc.abstractmethod
@abstractmethod
def detect(self):
raise NotImplementedError()


class ResourceDetector(Detector):
@abstractmethod
def detect(self) -> "Resource":
raise NotImplementedError()


class EntityDetector(Detector):
def __init__(self, raise_on_error: bool = False) -> None:
self.raise_on_error = raise_on_error

@abstractmethod
def detect(self) -> "Entity":
raise NotImplementedError()

@property
@abstractmethod
def priority(self):
raise NotImplementedError()


class Type0EntityDetector(EntityDetector):

_entity = None

def detect(self) -> Entity:

if self._entity is not None:
# The OTEP says an entity detector must not provide two entities of
# the same type. It seems to me that this means it will only
# provide one entity at all since the entity detector is associated
# to a particular "type" (process, OS, etc)
return self._entity

return Entity("type0", id_={"a": "b"}, attributes={"c": "d"})

@property
def priority(self):
# This probably needs a configuration mechanism so that it can get its
# priority from some configuration file or something else.
return 0


class Type1EntityDetector(EntityDetector):

_entity = None

def detect(self) -> Entity:

if self._entity is not None:
# The OTEP says an entity detector must not provide two entities of
# the same type. It seems to me that this means it will only
# provide one entity at all since the entity detector is associated
# to a particular "type" (process, OS, etc)
return self._entity

self._entity = Entity("type1", id_={"a": "b"}, attributes={"c": "d"})

@property
def priority(self):
# This probably needs a configuration mechanism so that it can get its
# priority from some configuration file or something else.
return 1


class OTELResourceDetector(ResourceDetector):
# pylint: disable=no-self-use
def detect(self) -> "Resource":

# An example of OTEL_RESOURCE_ATTRIBUTES is "key0=value0,key1=value1"
env_resources_items = environ.get(OTEL_RESOURCE_ATTRIBUTES)
env_resource_map = {}

Expand Down
94 changes: 94 additions & 0 deletions opentelemetry-sdk/tests/resources/test_entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 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.

from unittest import TestCase

from opentelemetry.sdk.resources import (
Entity,
Resource,
Type0EntityDetector,
Type1EntityDetector,
_select_entities,
)


class TestEntities(TestCase):

def test_sorted(self):

entity_detectors = Resource.create_using_entities()

self.assertEqual(len(entity_detectors), 2)

self.assertIsInstance(entity_detectors[0], Type0EntityDetector)
self.assertIsInstance(entity_detectors[1], Type1EntityDetector)

def test_select_entities(self):

unselected_entities = [
Entity("type_0", {"a": "a"}, {"b": "b"}, "a"),
Entity("type_0", {"a": "a"}, {"c": "c"}, "a"),
]

selected_entities = _select_entities(unselected_entities)

self.assertEqual(len(selected_entities), 1)
self.assertEqual(selected_entities[0].type, "type_0")
self.assertEqual(selected_entities[0].id, {"a": "a"})
self.assertEqual(selected_entities[0].attributes, {"b": "b", "c": "c"})
self.assertEqual(selected_entities[0].schema_url, "a")

unselected_entities = [
Entity("type_0", {"a": "a"}, {"b": "b"}, "a"),
Entity("type_0", {"a": "a"}, {"c": "c"}, "b"),
]

selected_entities = _select_entities(unselected_entities)

self.assertEqual(len(selected_entities), 1)
self.assertEqual(selected_entities[0].type, "type_0")
self.assertEqual(selected_entities[0].id, {"a": "a"})
self.assertEqual(selected_entities[0].attributes, {"b": "b"})
self.assertEqual(selected_entities[0].schema_url, "a")

unselected_entities = [
Entity("type_0", {"a": "a"}, {"b": "b"}, "a"),
Entity("type_0", {"a": "b"}, {"c": "c"}, "a"),
]

selected_entities = _select_entities(unselected_entities)

self.assertEqual(len(selected_entities), 1)
self.assertEqual(selected_entities[0].type, "type_0")
self.assertEqual(selected_entities[0].id, {"a": "a"})
self.assertEqual(selected_entities[0].attributes, {"b": "b"})
self.assertEqual(selected_entities[0].schema_url, "a")

unselected_entities = [
Entity("type_0", {"a": "a"}, {"b": "b"}, "a"),
Entity("type_1", {"a": "b"}, {"c": "c"}, "a"),
]

selected_entities = _select_entities(unselected_entities)

self.assertEqual(len(selected_entities), 2)
self.assertEqual(selected_entities[0].type, "type_0")
self.assertEqual(selected_entities[0].id, {"a": "a"})
self.assertEqual(selected_entities[0].attributes, {"b": "b"})
self.assertEqual(selected_entities[0].schema_url, "a")

self.assertEqual(selected_entities[1].type, "type_1")
self.assertEqual(selected_entities[1].id, {"a": "b"})
self.assertEqual(selected_entities[1].attributes, {"c": "c"})
self.assertEqual(selected_entities[1].schema_url, "a")

0 comments on commit 479d175

Please sign in to comment.