diff --git a/opentelemetry-api/src/opentelemetry/resources/__init__.py b/opentelemetry-api/src/opentelemetry/resources/__init__.py deleted file mode 100644 index d6a6eb64a29..00000000000 --- a/opentelemetry-api/src/opentelemetry/resources/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2019, 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. - -import abc -import typing - - -class Resource(abc.ABC): - """The interface that resources must implement.""" - - @staticmethod - @abc.abstractmethod - def create(labels: typing.Dict[str, str]) -> "Resource": - """Create a new resource. - - Args: - labels: the labels that define the resource - - Returns: - The resource with the labels in question - - """ - - @property - @abc.abstractmethod - def labels(self) -> typing.Dict[str, str]: - """Return the label dictionary associated with this resource. - - Returns: - A dictionary with the labels of the resource - - """ - - @abc.abstractmethod - def merge(self, other: typing.Optional["Resource"]) -> "Resource": - """Return a resource with the union of labels for both resources. - - Labels that exist in the main Resource take precedence unless the - label value is the empty string. - - Args: - other: The resource to merge in - - """ diff --git a/opentelemetry-api/src/opentelemetry/resources/py.typed b/opentelemetry-api/src/opentelemetry/resources/py.typed deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index fc0fe6ae521..5c616b39cf3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -19,6 +19,7 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export.aggregate import Aggregator from opentelemetry.sdk.metrics.export.batcher import Batcher, UngroupedBatcher +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.util import time_ns @@ -289,12 +290,16 @@ class Meter(metrics_api.Meter): """ def __init__( - self, instrumentation_info: "InstrumentationInfo", stateful: bool, + self, + instrumentation_info: "InstrumentationInfo", + stateful: bool, + resource: Resource = Resource.create_empty(), ): self.instrumentation_info = instrumentation_info self.metrics = set() self.observers = set() self.batcher = UngroupedBatcher(stateful) + self.resource = resource def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. @@ -400,6 +405,11 @@ def get_label_set(self, labels: Dict[str, str]): class MeterProvider(metrics_api.MeterProvider): + def __init__( + self, resource: Resource = Resource.create_empty(), + ): + self.resource = resource + def get_meter( self, instrumenting_module_name: str, @@ -413,4 +423,5 @@ def get_meter( instrumenting_module_name, instrumenting_library_version ), stateful=stateful, + resource=self.resource, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index b488c0a0c75..05c015de68b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -12,28 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. -import opentelemetry.resources as resources +import typing +LabelValue = typing.Union[str, bool, int, float] +Labels = typing.Dict[str, LabelValue] -class Resource(resources.Resource): - def __init__(self, labels): - self._labels = labels + +class Resource: + def __init__(self, labels: Labels): + self._labels = labels.copy() @staticmethod - def create(labels): + def create(labels: Labels) -> "Resource": + if not labels: + return _EMPTY_RESOURCE return Resource(labels) + @staticmethod + def create_empty() -> "Resource": + return _EMPTY_RESOURCE + @property - def labels(self): - return self._labels - - def merge(self, other): - if other is None: - return self - if not self._labels: - return other - merged_labels = self.labels.copy() - for key, value in other.labels.items(): + def labels(self) -> Labels: + return self._labels.copy() + + def merge(self, other: "Resource") -> "Resource": + merged_labels = self.labels + # pylint: disable=protected-access + for key, value in other._labels.items(): if key not in merged_labels or merged_labels[key] == "": merged_labels[key] = value return Resource(merged_labels) @@ -41,4 +47,7 @@ def merge(self, other): def __eq__(self, other: object) -> bool: if not isinstance(other, Resource): return False - return self.labels == other.labels + return self._labels == other._labels + + +_EMPTY_RESOURCE = Resource({}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 7f305dcc0d9..aed291b51d0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -25,6 +25,7 @@ from opentelemetry import context as context_api from opentelemetry import trace as trace_api from opentelemetry.sdk import util +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext, sampling @@ -127,7 +128,7 @@ class Span(trace_api.Span): remote, null if this is a root span sampler: The sampler used to create this span trace_config: TODO - resource: TODO + resource: Entity producing telemetry attributes: The span's attributes to be exported events: Timestamped events to be exported links: Links to other spans to be exported @@ -147,7 +148,7 @@ def __init__( parent: trace_api.ParentSpan = None, sampler: Optional[sampling.Sampler] = None, trace_config: None = None, # TODO - resource: None = None, # TODO + resource: None = None, attributes: types.Attributes = None, # TODO events: Sequence[trace_api.Event] = None, # TODO links: Sequence[trace_api.Link] = (), @@ -486,6 +487,7 @@ def start_span( # pylint: disable=too-many-locals context=context, parent=parent, sampler=self.source.sampler, + resource=self.source.resource, attributes=span_attributes, span_processor=self.source._active_span_processor, # pylint:disable=protected-access kind=kind, @@ -535,9 +537,11 @@ class TracerProvider(trace_api.TracerProvider): def __init__( self, sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, + resource: Resource = Resource.create_empty(), shutdown_on_exit: bool = True, ): self._active_span_processor = MultiSpanProcessor() + self.resource = resource self.sampler = sampler self._atexit_handler = None if shutdown_on_exit: diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index ea20cdd5930..6fcba4de633 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -16,10 +16,24 @@ from unittest import mock from opentelemetry import metrics as metrics_api -from opentelemetry.sdk import metrics +from opentelemetry.sdk import metrics, resources from opentelemetry.sdk.metrics import export +class TestMeterProvider(unittest.TestCase): + def test_resource(self): + resource = resources.Resource.create({}) + meter_provider = metrics.MeterProvider(resource=resource) + meter = meter_provider.get_meter(__name__) + self.assertIs(meter.resource, resource) + + def test_resource_empty(self): + meter_provider = metrics.MeterProvider() + meter = meter_provider.get_meter(__name__) + # pylint: disable=protected-access + self.assertIs(meter.resource, resources._EMPTY_RESOURCE) + + class TestMeter(unittest.TestCase): def test_extends_api(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -126,13 +140,16 @@ def test_record_batch_exists(self): self.assertEqual(handle.aggregator.current, 2.0) def test_create_metric(self): - meter = metrics.MeterProvider().get_meter(__name__) + resource = mock.Mock(spec=resources.Resource) + meter_provider = metrics.MeterProvider(resource=resource) + meter = meter_provider.get_meter(__name__) counter = meter.create_metric( "name", "desc", "unit", int, metrics.Counter, () ) self.assertIsInstance(counter, metrics.Counter) self.assertEqual(counter.value_type, int) self.assertEqual(counter.name, "name") + self.assertIs(counter.meter.resource, resource) def test_create_measure(self): meter = metrics.MeterProvider().get_meter(__name__) diff --git a/opentelemetry-sdk/tests/resources/test_init.py b/opentelemetry-sdk/tests/resources/test_resources.py similarity index 55% rename from opentelemetry-sdk/tests/resources/test_init.py rename to opentelemetry-sdk/tests/resources/test_resources.py index 2afe17e5633..16cf29057c8 100644 --- a/opentelemetry-sdk/tests/resources/test_init.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -12,12 +12,35 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=protected-access + import unittest from opentelemetry.sdk import resources class TestResources(unittest.TestCase): + def test_create(self): + labels = { + "service": "ui", + "version": 1, + "has_bugs": True, + "cost": 112.12, + } + + resource = resources.Resource.create(labels) + self.assertIsInstance(resource, resources.Resource) + self.assertEqual(resource.labels, labels) + + resource = resources.Resource.create_empty() + self.assertIs(resource, resources._EMPTY_RESOURCE) + + resource = resources.Resource.create(None) + self.assertIs(resource, resources._EMPTY_RESOURCE) + + resource = resources.Resource.create({}) + self.assertIs(resource, resources._EMPTY_RESOURCE) + def test_resource_merge(self): left = resources.Resource({"service": "ui"}) right = resources.Resource({"host": "service-host"}) @@ -41,3 +64,22 @@ def test_resource_merge_empty_string(self): left.merge(right), resources.Resource({"service": "ui", "host": "service-host"}), ) + + def test_immutability(self): + labels = { + "service": "ui", + "version": 1, + "has_bugs": True, + "cost": 112.12, + } + + labels_copy = labels.copy() + + resource = resources.Resource.create(labels) + self.assertEqual(resource.labels, labels_copy) + + resource.labels["has_bugs"] = False + self.assertEqual(resource.labels, labels_copy) + + labels["cost"] = 999.91 + self.assertEqual(resource.labels, labels_copy) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 188c019acc1..ea8c9a46d44 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -19,7 +19,7 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.sdk import trace +from opentelemetry.sdk import resources, trace from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import sampling from opentelemetry.trace.status import StatusCanonicalCode @@ -364,6 +364,20 @@ def test_start_as_current_span_explicit(self): self.assertIs(tracer.get_current_span(), root) self.assertIsNotNone(child.end_time) + def test_explicit_span_resource(self): + resource = resources.Resource.create({}) + tracer_provider = trace.TracerProvider(resource=resource) + tracer = tracer_provider.get_tracer(__name__) + span = tracer.start_span("root") + self.assertIs(span.resource, resource) + + def test_default_span_resource(self): + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) + span = tracer.start_span("root") + # pylint: disable=protected-access + self.assertIs(span.resource, resources._EMPTY_RESOURCE) + class TestSpan(unittest.TestCase): def setUp(self):