From 556b7b84f583bc4fbb0cd9b8fca24be7c371465a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 4 Mar 2020 11:01:03 -0500 Subject: [PATCH 1/5] Update resources to match spec - Move to SDK - Accept int, float, str, bool - Implement fast create for empty resource --- .../src/opentelemetry/resources/__init__.py | 55 ------------------- .../src/opentelemetry/resources/py.typed | 0 .../opentelemetry/sdk/resources/__init__.py | 28 ++++++---- .../{test_init.py => test_resources.py} | 23 ++++++++ 4 files changed, 41 insertions(+), 65 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/resources/__init__.py delete mode 100644 opentelemetry-api/src/opentelemetry/resources/py.typed rename opentelemetry-sdk/tests/resources/{test_init.py => test_resources.py} (68%) 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/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index b488c0a0c75..5559fde48b8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -12,26 +12,31 @@ # 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.Optional[typing.Dict[str, LabelValue]] -class Resource(resources.Resource): - def __init__(self, labels): + +class Resource: + def __init__(self, labels: Labels): self._labels = labels @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): + def labels(self) -> Labels: return self._labels - def merge(self, other): - if other is None: - return self - if not self._labels: - return other + def merge(self, other: "Resource") -> "Resource": merged_labels = self.labels.copy() for key, value in other.labels.items(): if key not in merged_labels or merged_labels[key] == "": @@ -42,3 +47,6 @@ def __eq__(self, other: object) -> bool: if not isinstance(other, Resource): return False return self.labels == other.labels + + +_EMPTY_RESOURCE = Resource({}) diff --git a/opentelemetry-sdk/tests/resources/test_init.py b/opentelemetry-sdk/tests/resources/test_resources.py similarity index 68% rename from opentelemetry-sdk/tests/resources/test_init.py rename to opentelemetry-sdk/tests/resources/test_resources.py index 2afe17e5633..f3649bf9b56 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"}) From b6d6705099a571428f3b665a070053588bd5fe84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 4 Mar 2020 15:01:05 -0500 Subject: [PATCH 2/5] sdk: pass resource to spans --- .../src/opentelemetry/sdk/trace/__init__.py | 8 ++++++-- opentelemetry-sdk/tests/trace/test_trace.py | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) 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/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 188c019acc1..140e972ebc3 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 = mock.Mock(spec=resources.Resource) + 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): From 36a2fc0a4895e96d813db807a43bc16eebe7f5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 4 Mar 2020 15:30:13 -0500 Subject: [PATCH 3/5] sdk: add resource to meters --- .../src/opentelemetry/sdk/metrics/__init__.py | 13 +++++++++++- .../tests/metrics/test_metrics.py | 21 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) 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/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index ea20cdd5930..2e58611864d 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 = mock.Mock(spec=resources.Resource) + 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__) From 48fddeeadfe3531c45710581bcd9bd65adaf7846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Thu, 5 Mar 2020 15:22:59 -0500 Subject: [PATCH 4/5] update pr - make Resource immutable - use actual Resource object instead of mock --- .../opentelemetry/sdk/resources/__init__.py | 9 ++++--- .../tests/metrics/test_metrics.py | 2 +- .../tests/resources/test_resources.py | 26 ++++++++++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 5559fde48b8..939fc0a069b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -34,11 +34,12 @@ def create_empty() -> "Resource": @property def labels(self) -> Labels: - return self._labels + return self._labels.copy() def merge(self, other: "Resource") -> "Resource": - merged_labels = self.labels.copy() - for key, value in other.labels.items(): + 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) @@ -46,7 +47,7 @@ def merge(self, other: "Resource") -> "Resource": 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/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 2e58611864d..6fcba4de633 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -22,7 +22,7 @@ class TestMeterProvider(unittest.TestCase): def test_resource(self): - resource = mock.Mock(spec=resources.Resource) + resource = resources.Resource.create({}) meter_provider = metrics.MeterProvider(resource=resource) meter = meter_provider.get_meter(__name__) self.assertIs(meter.resource, resource) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index f3649bf9b56..1c73023b996 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -21,12 +21,12 @@ class TestResources(unittest.TestCase): def test_create(self): - labels = ( - {"service": "ui"}, - {"version": 1}, - {"has_bugs", True}, - {"cost", 112.12}, - ) + labels = { + "service": "ui", + "version": 1, + "has_bugs": True, + "cost": 112.12, + } resource = resources.Resource.create(labels) self.assertIsInstance(resource, resources.Resource) @@ -64,3 +64,17 @@ 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, + } + + resource = resources.Resource.create(labels) + self.assertEqual(resource.labels, labels) + + resource.labels["has_bugs"] = False + self.assertEqual(resource.labels, labels) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 140e972ebc3..ea8c9a46d44 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -365,7 +365,7 @@ def test_start_as_current_span_explicit(self): self.assertIsNotNone(child.end_time) def test_explicit_span_resource(self): - resource = mock.Mock(spec=resources.Resource) + resource = resources.Resource.create({}) tracer_provider = trace.TracerProvider(resource=resource) tracer = tracer_provider.get_tracer(__name__) span = tracer.start_span("root") From 6b7c5af527a2d6bec11eb0424fff6711a86b4206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 9 Mar 2020 13:25:32 -0500 Subject: [PATCH 5/5] copy dict on resource constructor too --- .../src/opentelemetry/sdk/resources/__init__.py | 4 ++-- opentelemetry-sdk/tests/resources/test_resources.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 939fc0a069b..05c015de68b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -15,12 +15,12 @@ import typing LabelValue = typing.Union[str, bool, int, float] -Labels = typing.Optional[typing.Dict[str, LabelValue]] +Labels = typing.Dict[str, LabelValue] class Resource: def __init__(self, labels: Labels): - self._labels = labels + self._labels = labels.copy() @staticmethod def create(labels: Labels) -> "Resource": diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 1c73023b996..16cf29057c8 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -73,8 +73,13 @@ def test_immutability(self): "cost": 112.12, } + labels_copy = labels.copy() + resource = resources.Resource.create(labels) - self.assertEqual(resource.labels, labels) + self.assertEqual(resource.labels, labels_copy) resource.labels["has_bugs"] = False - self.assertEqual(resource.labels, labels) + self.assertEqual(resource.labels, labels_copy) + + labels["cost"] = 999.91 + self.assertEqual(resource.labels, labels_copy)