From 96c5cefc430f91ce6ff08b1fcf89c011a16d89f3 Mon Sep 17 00:00:00 2001 From: Seth Maxwell Date: Wed, 3 Jun 2020 21:12:08 +0000 Subject: [PATCH 01/12] Add fix for byte type attributes --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 5b74b4a618..4e916e4286 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -422,6 +422,8 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: # Freeze mutable sequences defensively if isinstance(value, MutableSequence): value = tuple(value) + if isinstance(value, bytes): + value = value.decode() with self._lock: self.attributes[key] = value From 7c0c928186e11cdb48779821073ec7e8eedbba6d Mon Sep 17 00:00:00 2001 From: Seth Maxwell Date: Thu, 4 Jun 2020 20:00:34 +0000 Subject: [PATCH 02/12] Add try except around byte attribute decoding --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 4e916e4286..b6275f9047 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -423,7 +423,11 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: if isinstance(value, MutableSequence): value = tuple(value) if isinstance(value, bytes): - value = value.decode() + try: + value = value.decode() + except ValueError: + logger.warning("Byte attribute could not be decoded.") + return with self._lock: self.attributes[key] = value From 91ef41b7c9c0dc5286c49d6b32fb53f88876b6d8 Mon Sep 17 00:00:00 2001 From: Seth Maxwell Date: Wed, 3 Jun 2020 21:12:08 +0000 Subject: [PATCH 03/12] Add fix for byte type attributes --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 20c50a849f..8fa093bc56 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -424,6 +424,8 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: # Freeze mutable sequences defensively if isinstance(value, MutableSequence): value = tuple(value) + if isinstance(value, bytes): + value = value.decode() with self._lock: self.attributes[key] = value From e6cecc39968323b7efaf5d62ae0989016f845ae9 Mon Sep 17 00:00:00 2001 From: Seth Maxwell Date: Thu, 4 Jun 2020 20:00:34 +0000 Subject: [PATCH 04/12] Add try except around byte attribute decoding --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 8fa093bc56..980caed37d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -425,7 +425,11 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: if isinstance(value, MutableSequence): value = tuple(value) if isinstance(value, bytes): - value = value.decode() + try: + value = value.decode() + except ValueError: + logger.warning("Byte attribute could not be decoded.") + return with self._lock: self.attributes[key] = value From 5fcb426cb739b07f76774c9b55adb1ceb4ca5f5c Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Mon, 1 Jun 2020 13:04:15 -0700 Subject: [PATCH 05/12] docs: Fix broken link (#763) --- docs/examples/basic_meter/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/basic_meter/README.rst b/docs/examples/basic_meter/README.rst index a48e5bb612..d77fbb7c4b 100644 --- a/docs/examples/basic_meter/README.rst +++ b/docs/examples/basic_meter/README.rst @@ -13,7 +13,7 @@ There are three different examples: * observer: Shows how to use the observer instrument. -The source files of these examples are available :scm_web:`here `. +The source files of these examples are available :scm_web:`here `. Installation ------------ From 7018a78ce68b7c94fccb485adfb6b7d68e324e35 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 2 Jun 2020 09:49:58 -0700 Subject: [PATCH 06/12] Rename Measure to ValueRecorder (#761) --- docs/examples/basic_meter/basic_metrics.py | 4 +- .../basic_meter/calling_conventions.py | 4 +- .../test_otcollector_metrics_exporter.py | 10 +-- .../opentelemetry/ext/prometheus/__init__.py | 4 +- opentelemetry-api/CHANGELOG.md | 3 + .../src/opentelemetry/metrics/__init__.py | 55 +++++------- .../tests/metrics/test_metrics.py | 20 ++--- opentelemetry-sdk/CHANGELOG.md | 3 + .../src/opentelemetry/sdk/metrics/__init__.py | 12 +-- .../sdk/metrics/export/aggregate.py | 2 +- .../sdk/metrics/export/batcher.py | 4 +- .../tests/metrics/test_metrics.py | 84 ++++++++++--------- 12 files changed, 103 insertions(+), 102 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index 5d137bf01e..6e8a5c040f 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -24,7 +24,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -80,7 +80,7 @@ def usage(argv): description="size of requests", unit="1", value_type=int, - metric_type=Measure, + metric_type=ValueRecorder, label_keys=("environment",), ) diff --git a/docs/examples/basic_meter/calling_conventions.py b/docs/examples/basic_meter/calling_conventions.py index 15b57fdafc..f8cc3dddbb 100644 --- a/docs/examples/basic_meter/calling_conventions.py +++ b/docs/examples/basic_meter/calling_conventions.py @@ -19,7 +19,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -43,7 +43,7 @@ description="size of requests", unit="1", value_type=int, - metric_type=Measure, + metric_type=ValueRecorder, label_keys=("environment",), ) diff --git a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py index 63ea28cd93..18b4a32806 100644 --- a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py @@ -23,8 +23,8 @@ from opentelemetry.ext.opencensusexporter import metrics_exporter from opentelemetry.sdk.metrics import ( Counter, - Measure, MeterProvider, + ValueRecorder, get_labels_as_key, ) from opentelemetry.sdk.metrics.export import ( @@ -76,7 +76,7 @@ def test_get_collector_metric_type(self): ) self.assertIs(result, metrics_pb2.MetricDescriptor.CUMULATIVE_DOUBLE) result = metrics_exporter.get_collector_metric_type( - Measure("testName", "testDescription", "unit", None, None) + ValueRecorder("testName", "testDescription", "unit", None, None) ) self.assertIs(result, metrics_pb2.MetricDescriptor.UNSPECIFIED) @@ -88,8 +88,8 @@ def test_get_collector_point(self): float_counter = self._meter.create_metric( "testName", "testDescription", "unit", float, Counter ) - measure = self._meter.create_metric( - "testName", "testDescription", "unit", float, Measure + valuerecorder = self._meter.create_metric( + "testName", "testDescription", "unit", float, ValueRecorder ) result = metrics_exporter.get_collector_point( MetricRecord(aggregator, self._key_labels, int_counter) @@ -106,7 +106,7 @@ def test_get_collector_point(self): self.assertRaises( TypeError, metrics_exporter.get_collector_point( - MetricRecord(aggregator, self._key_labels, measure) + MetricRecord(aggregator, self._key_labels, valuerecorder) ), ) diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index f6f91fc586..cc44621ac4 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -79,7 +79,7 @@ UnknownMetricFamily, ) -from opentelemetry.metrics import Counter, Measure, Metric +from opentelemetry.metrics import Counter, Metric, ValueRecorder from opentelemetry.sdk.metrics.export import ( MetricRecord, MetricsExporter, @@ -164,7 +164,7 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): labels=label_values, value=metric_record.aggregator.checkpoint ) # TODO: Add support for histograms when supported in OT - elif isinstance(metric_record.metric, Measure): + elif isinstance(metric_record.metric, ValueRecorder): prometheus_metric = UnknownMetricFamily( name=metric_name, documentation=metric_record.metric.description, diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 386bd75d47..4b678cd422 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Rename Measure to ValueRecorder in metrics + ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) + ## 0.8b0 Released 2020-05-27 diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index b7ad62adb2..a91a4ce7b7 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -19,16 +19,13 @@ The `Meter` class is used to construct `Metric` s to record raw statistics as well as metrics with predefined aggregation. +`Meter` s can be obtained via the `MeterProvider`, corresponding to the name +of the instrumenting library and (optionally) a version. + See the `metrics api`_ spec for terminology and context clarification. .. _metrics api: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-metrics.md - -.. versionadded:: 0.1.0 -.. versionchanged:: 0.5.0 - ``meter_provider`` was replaced by `get_meter_provider`, - ``set_preferred_meter_provider_implementation`` was replaced by - `set_meter_provider`. + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/metrics/api.md """ import abc from logging import getLogger @@ -54,7 +51,7 @@ def add(self, value: ValueT) -> None: """ def record(self, value: ValueT) -> None: - """No-op implementation of `BoundMeasure` record. + """No-op implementation of `BoundValueRecorder` record. Args: value: The value to record to the bound metric instrument. @@ -73,12 +70,12 @@ def add(self, value: ValueT) -> None: """ -class BoundMeasure: +class BoundValueRecorder: def record(self, value: ValueT) -> None: - """Records the given ``value`` to this bound measure. + """Records the given ``value`` to this bound valuerecorder. Args: - value: The value to record to the bound measure. + value: The value to record to the bound valuerecorder. """ @@ -94,12 +91,7 @@ def bind(self, labels: Dict[str, str]) -> "object": """Gets a bound metric instrument. Bound metric instruments are useful to reduce the cost of repeatedly - recording a metric with a pre-defined set of label values. All metric - kinds (counter, measure) support declaring a set of required label - keys. The values corresponding to these keys should be specified in - every bound metric instrument. "Unspecified" label values, in cases - where a bound metric instrument is requested but a value was not - provided are permitted. + recording a metric with a pre-defined set of label values. Args: labels: Labels to associate with the bound instrument. @@ -126,10 +118,10 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None: """ def record(self, value: ValueT, labels: Dict[str, str]) -> None: - """No-op implementation of `Measure` record. + """No-op implementation of `ValueRecorder` record. Args: - value: The value to record to this measure metric. + value: The value to record to this valuerecorder metric. labels: Labels to associate with the bound instrument. """ @@ -150,21 +142,18 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None: """ -class Measure(Metric): - """A measure type metric that represent raw stats that are recorded. - - Measure metrics represent raw statistics that are recorded. - """ +class ValueRecorder(Metric): + """A valuerecorder type metric that represent raw stats.""" - def bind(self, labels: Dict[str, str]) -> "BoundMeasure": - """Gets a `BoundMeasure`.""" - return BoundMeasure() + def bind(self, labels: Dict[str, str]) -> "BoundValueRecorder": + """Gets a `BoundValueRecorder`.""" + return BoundValueRecorder() def record(self, value: ValueT, labels: Dict[str, str]) -> None: - """Records the ``value`` to the measure. + """Records the ``value`` to the valuerecorder. Args: - value: The value to record to this measure metric. + value: The value to record to this valuerecorder metric. labels: Labels to associate with the bound instrument. """ @@ -251,7 +240,7 @@ def get_meter( return DefaultMeter() -MetricT = TypeVar("MetricT", Counter, Measure, Observer) +MetricT = TypeVar("MetricT", Counter, ValueRecorder, Observer) ObserverCallbackT = Callable[[Observer], None] @@ -259,9 +248,9 @@ def get_meter( class Meter(abc.ABC): """An interface to allow the recording of metrics. - `Metric` s are used for recording pre-defined aggregation (counter), - or raw values (measure) in which the aggregation and labels - for the exported metric are deferred. + `Metric` s or metric instruments, are devices used for capturing raw + measurements. Each metric instrument supports a single method, each with + fixed interpretation to capture measurements. """ @abc.abstractmethod diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 3e760d3d98..897c7492e4 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -35,14 +35,14 @@ def test_counter_add(self): counter = metrics.Counter() counter.add(1, {}) - def test_measure(self): - measure = metrics.Measure() - bound_measure = measure.bind({}) - self.assertIsInstance(bound_measure, metrics.BoundMeasure) + def test_valuerecorder(self): + valuerecorder = metrics.ValueRecorder() + bound_valuerecorder = valuerecorder.bind({}) + self.assertIsInstance(bound_valuerecorder, metrics.BoundValueRecorder) - def test_measure_record(self): - measure = metrics.Measure() - measure.record(1, {}) + def test_valuerecorder_record(self): + valuerecorder = metrics.ValueRecorder() + valuerecorder.record(1, {}) def test_default_bound_metric(self): bound_instrument = metrics.DefaultBoundInstrument() @@ -52,9 +52,9 @@ def test_bound_counter(self): bound_counter = metrics.BoundCounter() bound_counter.add(1) - def test_bound_measure(self): - bound_measure = metrics.BoundMeasure() - bound_measure.record(1) + def test_bound_valuerecorder(self): + bound_valuerecorder = metrics.BoundValueRecorder() + bound_valuerecorder.record(1) def test_observer(self): observer = metrics.DefaultObserver() diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 50ea751e15..29aaed01dc 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Rename Measure to ValueRecorder in metrics + ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) + ## 0.8b0 Released 2020-05-27 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 1d35648fd3..b9284bae9f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -97,9 +97,9 @@ def add(self, value: metrics_api.ValueT) -> None: self.update(value) -class BoundMeasure(metrics_api.BoundMeasure, BaseBoundInstrument): +class BoundValueRecorder(metrics_api.BoundValueRecorder, BaseBoundInstrument): def record(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.BoundMeasure.record`.""" + """See `opentelemetry.metrics.BoundValueRecorder.record`.""" if self._validate_update(value): self.update(value) @@ -174,15 +174,15 @@ def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None: UPDATE_FUNCTION = add -class Measure(Metric, metrics_api.Measure): - """See `opentelemetry.metrics.Measure`.""" +class ValueRecorder(Metric, metrics_api.ValueRecorder): + """See `opentelemetry.metrics.ValueRecorder`.""" - BOUND_INSTR_TYPE = BoundMeasure + BOUND_INSTR_TYPE = BoundValueRecorder def record( self, value: metrics_api.ValueT, labels: Dict[str, str] ) -> None: - """See `opentelemetry.metrics.Measure.record`.""" + """See `opentelemetry.metrics.ValueRecorder.record`.""" bound_intrument = self.bind(labels) bound_intrument.record(value) bound_intrument.release() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index ea8c40a7e7..7e1baba2c7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -72,7 +72,7 @@ def merge(self, other): class MinMaxSumCountAggregator(Aggregator): - """Agregator for Measure metrics that keeps min, max, sum and count.""" + """Aggregator for ValueRecorder metrics that keeps min, max, sum, count.""" _TYPE = namedtuple("minmaxsumcount", "min max sum count") _EMPTY = _TYPE(None, None, None, 0) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 7b599f4c7d..eda504d568 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -15,7 +15,7 @@ import abc from typing import Sequence, Type -from opentelemetry.metrics import Counter, Measure, MetricT, Observer +from opentelemetry.metrics import Counter, MetricT, Observer, ValueRecorder from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, @@ -49,7 +49,7 @@ def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: # pylint:disable=R0201 if issubclass(metric_type, Counter): return CounterAggregator() - if issubclass(metric_type, Measure): + if issubclass(metric_type, ValueRecorder): return MinMaxSumCountAggregator() if issubclass(metric_type, Observer): return ObserverAggregator() diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 3298064705..a3c0f4294d 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -109,14 +109,14 @@ def test_record_batch_multiple(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - measure = metrics.Measure( + valuerecorder = metrics.ValueRecorder( "name", "desc", "unit", float, meter, label_keys ) - record_tuples = [(counter, 1.0), (measure, 3.0)] + record_tuples = [(counter, 1.0), (valuerecorder, 3.0)] meter.record_batch(labels, record_tuples) self.assertEqual(counter.bind(labels).aggregator.current, 1.0) self.assertEqual( - measure.bind(labels).aggregator.current, (3.0, 3.0, 3.0, 1) + valuerecorder.bind(labels).aggregator.current, (3.0, 3.0, 3.0, 1) ) def test_record_batch_exists(self): @@ -145,14 +145,14 @@ def test_create_metric(self): self.assertEqual(counter.name, "name") self.assertIs(counter.meter.resource, resource) - def test_create_measure(self): + def test_create_valuerecorder(self): meter = metrics.MeterProvider().get_meter(__name__) - measure = meter.create_metric( - "name", "desc", "unit", float, metrics.Measure, () + valuerecorder = meter.create_metric( + "name", "desc", "unit", float, metrics.ValueRecorder, () ) - self.assertIsInstance(measure, metrics.Measure) - self.assertEqual(measure.value_type, float) - self.assertEqual(measure.name, "name") + self.assertIsInstance(valuerecorder, metrics.ValueRecorder) + self.assertEqual(valuerecorder.value_type, float) + self.assertEqual(valuerecorder.name, "name") def test_register_observer(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -197,19 +197,19 @@ def test_direct_call_release_bound_instrument(self): meter.metrics.add(counter) counter.add(4.0, labels) - measure = metrics.Measure( + valuerecorder = metrics.ValueRecorder( "name", "desc", "unit", float, meter, label_keys ) - meter.metrics.add(measure) - measure.record(42.0, labels) + meter.metrics.add(valuerecorder) + valuerecorder.record(42.0, labels) self.assertEqual(len(counter.bound_instruments), 1) - self.assertEqual(len(measure.bound_instruments), 1) + self.assertEqual(len(valuerecorder.bound_instruments), 1) meter.collect() self.assertEqual(len(counter.bound_instruments), 0) - self.assertEqual(len(measure.bound_instruments), 0) + self.assertEqual(len(valuerecorder.bound_instruments), 0) def test_release_bound_instrument(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -223,30 +223,30 @@ def test_release_bound_instrument(self): bound_counter = counter.bind(labels) bound_counter.add(4.0) - measure = metrics.Measure( + valuerecorder = metrics.ValueRecorder( "name", "desc", "unit", float, meter, label_keys ) - meter.metrics.add(measure) - bound_measure = measure.bind(labels) - bound_measure.record(42) + meter.metrics.add(valuerecorder) + bound_valuerecorder = valuerecorder.bind(labels) + bound_valuerecorder.record(42) bound_counter.release() - bound_measure.release() + bound_valuerecorder.release() # be sure that bound instruments are only released after collection self.assertEqual(len(counter.bound_instruments), 1) - self.assertEqual(len(measure.bound_instruments), 1) + self.assertEqual(len(valuerecorder.bound_instruments), 1) meter.collect() self.assertEqual(len(counter.bound_instruments), 0) - self.assertEqual(len(measure.bound_instruments), 0) + self.assertEqual(len(valuerecorder.bound_instruments), 0) class TestMetric(unittest.TestCase): def test_bind(self): meter = metrics.MeterProvider().get_meter(__name__) - metric_types = [metrics.Counter, metrics.Measure] + metric_types = [metrics.Counter, metrics.ValueRecorder] labels = {"key": "value"} key_labels = tuple(sorted(labels.items())) for _type in metric_types: @@ -268,17 +268,19 @@ def test_add(self): self.assertEqual(bound_counter.aggregator.current, 5) -class TestMeasure(unittest.TestCase): +class TestValueRecorder(unittest.TestCase): def test_record(self): meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) + metric = metrics.ValueRecorder( + "name", "desc", "unit", int, meter, ("key",) + ) labels = {"key": "value"} - bound_measure = metric.bind(labels) + bound_valuerecorder = metric.bind(labels) values = (37, 42, 7) for val in values: metric.record(val, labels) self.assertEqual( - bound_measure.aggregator.current, + bound_valuerecorder.aggregator.current, (min(values), max(values), sum(values), len(values)), ) @@ -375,33 +377,37 @@ def test_update(self): self.assertEqual(bound_counter.aggregator.current, 4.0) -class TestBoundMeasure(unittest.TestCase): +class TestBoundValueRecorder(unittest.TestCase): def test_record(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, True, aggregator) - bound_measure.record(3) - self.assertEqual(bound_measure.aggregator.current, (3, 3, 3, 1)) + bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + bound_valuerecorder.record(3) + self.assertEqual(bound_valuerecorder.aggregator.current, (3, 3, 3, 1)) def test_record_disabled(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, False, aggregator) - bound_measure.record(3) + bound_valuerecorder = metrics.BoundValueRecorder( + int, False, aggregator + ) + bound_valuerecorder.record(3) self.assertEqual( - bound_measure.aggregator.current, (None, None, None, 0) + bound_valuerecorder.aggregator.current, (None, None, None, 0) ) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, True, aggregator) - bound_measure.record(3.0) + bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + bound_valuerecorder.record(3.0) self.assertEqual( - bound_measure.aggregator.current, (None, None, None, 0) + bound_valuerecorder.aggregator.current, (None, None, None, 0) ) self.assertTrue(logger_mock.warning.called) def test_update(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, True, aggregator) - bound_measure.update(4.0) - self.assertEqual(bound_measure.aggregator.current, (4.0, 4.0, 4.0, 1)) + bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + bound_valuerecorder.update(4.0) + self.assertEqual( + bound_valuerecorder.aggregator.current, (4.0, 4.0, 4.0, 1) + ) From 9825e90994b39838e870f413f399117efb4b26c7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 2 Jun 2020 16:37:56 -0600 Subject: [PATCH 07/12] ext/boto: Add boto instrumentation (#665) --- docs-requirements.txt | 1 + docs/ext/boto/boto.rst | 7 + ext/opentelemetry-ext-boto/CHANGELOG.md | 5 + ext/opentelemetry-ext-boto/LICENSE | 201 ++++++++++++++ ext/opentelemetry-ext-boto/MANIFEST.in | 9 + ext/opentelemetry-ext-boto/README.rst | 23 ++ ext/opentelemetry-ext-boto/setup.cfg | 58 +++++ ext/opentelemetry-ext-boto/setup.py | 26 ++ .../src/opentelemetry/ext/boto/__init__.py | 245 ++++++++++++++++++ .../src/opentelemetry/ext/boto/version.py | 15 ++ ext/opentelemetry-ext-boto/tests/__init__.py | 0 ext/opentelemetry-ext-boto/tests/conftest.py | 31 +++ .../tests/test_boto_instrumentation.py | 242 +++++++++++++++++ tox.ini | 9 + 14 files changed, 872 insertions(+) create mode 100644 docs/ext/boto/boto.rst create mode 100644 ext/opentelemetry-ext-boto/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-boto/LICENSE create mode 100644 ext/opentelemetry-ext-boto/MANIFEST.in create mode 100644 ext/opentelemetry-ext-boto/README.rst create mode 100644 ext/opentelemetry-ext-boto/setup.cfg create mode 100644 ext/opentelemetry-ext-boto/setup.py create mode 100644 ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py create mode 100644 ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py create mode 100644 ext/opentelemetry-ext-boto/tests/__init__.py create mode 100644 ext/opentelemetry-ext-boto/tests/conftest.py create mode 100644 ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py diff --git a/docs-requirements.txt b/docs-requirements.txt index cc999f1383..a61f0beedb 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -20,3 +20,4 @@ sqlalchemy>=1.0 thrift>=0.10.0 wrapt>=1.0.0,<2.0.0 psutil~=5.7.0 +boto~=2.0 diff --git a/docs/ext/boto/boto.rst b/docs/ext/boto/boto.rst new file mode 100644 index 0000000000..8bf40c7566 --- /dev/null +++ b/docs/ext/boto/boto.rst @@ -0,0 +1,7 @@ +OpenTelemetry Boto Integration +============================== + +.. automodule:: opentelemetry.ext.boto + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-boto/CHANGELOG.md b/ext/opentelemetry-ext-boto/CHANGELOG.md new file mode 100644 index 0000000000..3e04402cea --- /dev/null +++ b/ext/opentelemetry-ext-boto/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release diff --git a/ext/opentelemetry-ext-boto/LICENSE b/ext/opentelemetry-ext-boto/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/ext/opentelemetry-ext-boto/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-boto/MANIFEST.in b/ext/opentelemetry-ext-boto/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/ext/opentelemetry-ext-boto/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-boto/README.rst b/ext/opentelemetry-ext-boto/README.rst new file mode 100644 index 0000000000..e149ec424e --- /dev/null +++ b/ext/opentelemetry-ext-boto/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Boto Tracing +========================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-boto.svg + :target: https://pypi.org/project/opentelemetry-ext-boto/ + +This library allows tracing requests made by the Boto library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-boto + + +References +---------- + +* `OpenTelemetry Boto Tracing `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg new file mode 100644 index 0000000000..529e79be99 --- /dev/null +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -0,0 +1,58 @@ +# 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. +# +[metadata] +name = opentelemetry-ext-boto +description = Boto tracing for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-boto +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + boto ~= 2.0 + opentelemetry-api == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 + +[options.extras_require] +test = + boto~=2.0 + moto~=1.0 + opentelemetry-test == 0.9.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + django = opentelemetry.ext.boto:BotoInstrumentor diff --git a/ext/opentelemetry-ext-boto/setup.py b/ext/opentelemetry-ext-boto/setup.py new file mode 100644 index 0000000000..4c78e9b35f --- /dev/null +++ b/ext/opentelemetry-ext-boto/setup.py @@ -0,0 +1,26 @@ +# 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. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "boto", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py new file mode 100644 index 0000000000..fa66fda61d --- /dev/null +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py @@ -0,0 +1,245 @@ +# 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. +""" +Instrument `Boto`_ to trace service requests. + +There are two options for instrumenting code. The first option is to use the +``opentelemetry-auto-instrumentation`` executable which will automatically +instrument your Boto client. The second is to programmatically enable +instrumentation via the following code: + +.. _boto: https://pypi.org/project/boto/ + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.boto import BotoInstrumentor + from opentelemetry.sdk.trace import TracerProvider + import boto + + trace.set_tracer_provider(TracerProvider()) + + # Instrument Boto + BotoInstrumentor().instrument(tracer_provider=trace.get_tracer_provider()) + + # This will create a span with Boto-specific attributes + ec2 = boto.ec2.connect_to_region("us-west-2") + ec2.get_all_instances() + +API +--- +""" + +import logging +from inspect import currentframe + +from boto.connection import AWSAuthConnection, AWSQueryConnection +from wrapt import ObjectProxy, wrap_function_wrapper + +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext.boto.version import __version__ +from opentelemetry.trace import SpanKind, get_tracer + +logger = logging.getLogger(__name__) + + +def _get_instance_region_name(instance): + region = getattr(instance, "region", None) + + if not region: + return None + if isinstance(region, str): + return region.split(":")[1] + return region.name + + +class BotoInstrumentor(BaseInstrumentor): + """A instrumentor for Boto + + See `BaseInstrumentor` + """ + + def __init__(self): + super().__init__() + self._original_boto = None + + def _instrument(self, **kwargs): + # AWSQueryConnection and AWSAuthConnection are two different classes + # called by different services for connection. + # For exemple EC2 uses AWSQueryConnection and S3 uses + # AWSAuthConnection + + # FIXME should the tracer provider be accessed via Configuration + # instead? + # pylint: disable=attribute-defined-outside-init + self._tracer = get_tracer( + __name__, __version__, kwargs.get("tracer_provider") + ) + + wrap_function_wrapper( + "boto.connection", + "AWSQueryConnection.make_request", + self._patched_query_request, + ) + wrap_function_wrapper( + "boto.connection", + "AWSAuthConnection.make_request", + self._patched_auth_request, + ) + + def _uninstrument(self, **kwargs): + unwrap(AWSQueryConnection, "make_request") + unwrap(AWSAuthConnection, "make_request") + + def _common_request( # pylint: disable=too-many-locals + self, + args_name, + traced_args, + operation_name, + original_func, + instance, + args, + kwargs, + ): + + endpoint_name = getattr(instance, "host").split(".")[0] + + with self._tracer.start_as_current_span( + "{}.command".format(endpoint_name), kind=SpanKind.CONSUMER, + ) as span: + if args: + http_method = args[0] + span.resource = "%s.%s" % (endpoint_name, http_method.lower()) + else: + span.resource = endpoint_name + + add_span_arg_tags( + span, endpoint_name, args, args_name, traced_args, + ) + + # Obtaining region name + region_name = _get_instance_region_name(instance) + + meta = { + "aws.agent": "boto", + "aws.operation": operation_name, + } + if region_name: + meta["aws.region"] = region_name + + for key, value in meta.items(): + span.set_attribute(key, value) + + # Original func returns a boto.connection.HTTPResponse object + result = original_func(*args, **kwargs) + span.set_attribute("http.status_code", getattr(result, "status")) + span.set_attribute("http.method", getattr(result, "_method")) + + return result + + def _patched_query_request(self, original_func, instance, args, kwargs): + + return self._common_request( + ("operation_name", "params", "path", "verb"), + ["operation_name", "params", "path"], + args[0] if args else None, + original_func, + instance, + args, + kwargs, + ) + + def _patched_auth_request(self, original_func, instance, args, kwargs): + operation_name = None + + frame = currentframe().f_back + operation_name = None + while frame: + if frame.f_code.co_name == "make_request": + operation_name = frame.f_back.f_code.co_name + break + frame = frame.f_back + + return self._common_request( + ( + "method", + "path", + "headers", + "data", + "host", + "auth_path", + "sender", + ), + ["path", "data", "host"], + operation_name, + original_func, + instance, + args, + kwargs, + ) + + +def truncate_arg_value(value, max_len=1024): + """Truncate values which are bytes and greater than ``max_len``. + Useful for parameters like "Body" in ``put_object`` operations. + """ + if isinstance(value, bytes) and len(value) > max_len: + return b"..." + + return value + + +def add_span_arg_tags(span, endpoint_name, args, args_names, args_traced): + if endpoint_name not in ["kms", "sts"]: + tags = dict( + (name, value) + for (name, value) in zip(args_names, args) + if name in args_traced + ) + tags = flatten_dict(tags) + for key, value in { + k: truncate_arg_value(v) + for k, v in tags.items() + if k not in {"s3": ["params.Body"]}.get(endpoint_name, []) + }.items(): + span.set_attribute(key, value) + + +def flatten_dict(dict_, sep=".", prefix=""): + """ + Returns a normalized dict of depth 1 with keys in order of embedding + """ + # adapted from https://stackoverflow.com/a/19647596 + return ( + { + prefix + sep + k if prefix else k: v + for kk, vv in dict_.items() + for k, v in flatten_dict(vv, sep, kk).items() + } + if isinstance(dict_, dict) + else {prefix: dict_} + ) + + +def unwrap(obj, attr): + function = getattr(obj, attr, None) + if ( + function + and isinstance(function, ObjectProxy) + and hasattr(function, "__wrapped__") + ): + setattr(obj, attr, function.__wrapped__) diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py new file mode 100644 index 0000000000..bcf6a35777 --- /dev/null +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-boto/tests/__init__.py b/ext/opentelemetry-ext-boto/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-boto/tests/conftest.py b/ext/opentelemetry-ext-boto/tests/conftest.py new file mode 100644 index 0000000000..884c6753c1 --- /dev/null +++ b/ext/opentelemetry-ext-boto/tests/conftest.py @@ -0,0 +1,31 @@ +# 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 os import environ + + +def pytest_sessionstart(session): + # pylint: disable=unused-argument + environ["AWS_ACCESS_KEY_ID"] = "testing" + environ["AWS_SECRET_ACCESS_KEY"] = "testing" + environ["AWS_SECURITY_TOKEN"] = "testing" + environ["AWS_SESSION_TOKEN"] = "testing" + + +def pytest_sessionfinish(session): + # pylint: disable=unused-argument + environ.pop("AWS_ACCESS_KEY_ID") + environ.pop("AWS_SECRET_ACCESS_KEY") + environ.pop("AWS_SECURITY_TOKEN") + environ.pop("AWS_SESSION_TOKEN") diff --git a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py new file mode 100644 index 0000000000..492fac5a88 --- /dev/null +++ b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py @@ -0,0 +1,242 @@ +# 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 skipUnless + +import boto.awslambda +import boto.ec2 +import boto.elasticache +import boto.s3 +import boto.sts + +from moto import ( # pylint: disable=import-error + mock_ec2_deprecated, + mock_lambda_deprecated, + mock_s3_deprecated, + mock_sts_deprecated, +) +from opentelemetry.ext.boto import BotoInstrumentor +from opentelemetry.test.test_base import TestBase + + +def assert_span_http_status_code(span, code): + """Assert on the span's 'http.status_code' tag""" + tag = span.attributes["http.status_code"] + assert tag == code, "%r != %r" % (tag, code) + + +class TestBotoInstrumentor(TestBase): + """Botocore integration testsuite""" + + def setUp(self): + super().setUp() + BotoInstrumentor().instrument() + + def tearDown(self): + BotoInstrumentor().uninstrument() + + @mock_ec2_deprecated + def test_ec2_client(self): + ec2 = boto.ec2.connect_to_region("us-west-2") + + ec2.get_all_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.attributes["aws.operation"], "DescribeInstances") + assert_span_http_status_code(span, 200) + self.assertEqual(span.attributes["http.method"], "POST") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + + # Create an instance + ec2.run_instances(21) + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[1] + self.assertEqual(span.attributes["aws.operation"], "RunInstances") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "ec2.runinstances") + self.assertEqual(span.attributes["http.method"], "POST") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + self.assertEqual(span.name, "ec2.command") + + @mock_ec2_deprecated + def test_analytics_enabled_with_rate(self): + ec2 = boto.ec2.connect_to_region("us-west-2") + + ec2.get_all_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + + @mock_ec2_deprecated + def test_analytics_enabled_without_rate(self): + ec2 = boto.ec2.connect_to_region("us-west-2") + + ec2.get_all_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + + @mock_s3_deprecated + def test_s3_client(self): + s3 = boto.s3.connect_to_region("us-east-1") + + s3.get_all_buckets() + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + span = spans[0] + assert_span_http_status_code(span, 200) + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual(span.attributes["aws.operation"], "get_all_buckets") + + # Create a bucket command + s3.create_bucket("cheese") + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[1] + assert_span_http_status_code(span, 200) + self.assertEqual(span.attributes["http.method"], "PUT") + self.assertEqual(span.attributes["path"], "/") + self.assertEqual(span.attributes["aws.operation"], "create_bucket") + + # Get the created bucket + s3.get_bucket("cheese") + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 3) + span = spans[2] + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "s3.head") + self.assertEqual(span.attributes["http.method"], "HEAD") + self.assertEqual(span.attributes["aws.operation"], "head_bucket") + self.assertEqual(span.name, "s3.command") + + # Checking for resource incase of error + try: + s3.get_bucket("big_bucket") + except Exception: # pylint: disable=broad-except + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[2] + self.assertEqual(span.resource, "s3.head") + + @mock_s3_deprecated + def test_s3_put(self): + s3 = boto.s3.connect_to_region("us-east-1") + s3.create_bucket("mybucket") + bucket = s3.get_bucket("mybucket") + key = boto.s3.key.Key(bucket) + key.key = "foo" + key.set_contents_from_string("bar") + + spans = self.memory_exporter.get_finished_spans() + assert spans + # create bucket + self.assertEqual(len(spans), 3) + self.assertEqual(spans[0].attributes["aws.operation"], "create_bucket") + assert_span_http_status_code(spans[0], 200) + self.assertEqual(spans[0].resource, "s3.put") + # get bucket + self.assertEqual(spans[1].attributes["aws.operation"], "head_bucket") + self.assertEqual(spans[1].resource, "s3.head") + # put object + self.assertEqual( + spans[2].attributes["aws.operation"], "_send_file_internal" + ) + self.assertEqual(spans[2].resource, "s3.put") + + @mock_lambda_deprecated + def test_unpatch(self): + + lamb = boto.awslambda.connect_to_region("us-east-2") + + BotoInstrumentor().uninstrument() + + # multiple calls + lamb.list_functions() + spans = self.memory_exporter.get_finished_spans() + assert not spans, spans + + @mock_s3_deprecated + def test_double_patch(self): + s3 = boto.s3.connect_to_region("us-east-1") + + BotoInstrumentor().instrument() + BotoInstrumentor().instrument() + + # Get the created bucket + s3.create_bucket("cheese") + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + + @mock_lambda_deprecated + def test_lambda_client(self): + lamb = boto.awslambda.connect_to_region("us-east-2") + + # multiple calls + lamb.list_functions() + lamb.list_functions() + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[0] + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "lambda.get") + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual(span.attributes["aws.region"], "us-east-2") + self.assertEqual(span.attributes["aws.operation"], "list_functions") + + @mock_sts_deprecated + def test_sts_client(self): + sts = boto.sts.connect_to_region("us-west-2") + + sts.get_federation_token(12, duration=10) + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(span.resource, "sts.getfederationtoken") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + self.assertEqual( + span.attributes["aws.operation"], "GetFederationToken" + ) + + # checking for protection on sts against security leak + self.assertTrue("args.path" not in span.attributes.keys()) + + @skipUnless( + False, + ( + "Test to reproduce the case where args sent to patched function " + "are None, can't be mocked: needs AWS credentials" + ), + ) + def test_elasticache_client(self): + elasticache = boto.elasticache.connect_to_region("us-west-2") + + elasticache.describe_cache_clusters() + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(span.resource, "elasticache") + self.assertEqual(span.attributes["aws.region"], "us-west-2") diff --git a/tox.ini b/tox.ini index 3cec9fcfbd..978155616a 100644 --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,10 @@ envlist = py3{4,5,6,7,8}-test-ext-dbapi pypy3-test-ext-dbapi + ; opentelemetry-ext-boto + py3{5,6,7,8}-test-ext-boto + pypy3-test-ext-boto + ; opentelemetry-ext-flask py3{4,5,6,7,8}-test-ext-flask pypy3-test-ext-flask @@ -161,6 +165,7 @@ changedir = test-ext-sqlite3: ext/opentelemetry-ext-sqlite3/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests + test-ext-boto: ext/opentelemetry-ext-boto/tests test-ext-flask: ext/opentelemetry-ext-flask/tests test-example-app: docs/examples/opentelemetry-example-app/tests test-getting-started: docs/getting_started/tests @@ -188,6 +193,10 @@ commands_pre = wsgi,flask,django: pip install {toxinidir}/ext/opentelemetry-ext-wsgi flask,django: pip install {toxinidir}/opentelemetry-auto-instrumentation asgi: pip install {toxinidir}/ext/opentelemetry-ext-asgi + + boto: pip install {toxinidir}/opentelemetry-auto-instrumentation + boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] + flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] From 20e4ff4e9a961a4ecd89e66ea58bb54470983a51 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 2 Jun 2020 20:37:36 -0700 Subject: [PATCH 08/12] opentracing-shim: add testbed for otshim (#727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit ports the OpenTracing testbed[1] to check that the ot-shim is working as expected using different frameworks. Gevent doesn't support context vars yet[2], so those tests are not compatible with opentelemetry and were not ported. [1] https://github.com/opentracing/opentracing-python/tree/master/testbed [2] https://github.com/gevent/gevent/issues/1407 Co-authored-by: Mauricio Vásquez Co-authored-by: alrex --- dev-requirements.txt | 1 + .../tests/testbed/README.rst | 47 ++++++ .../tests/testbed/__init__.py | 0 .../tests/testbed/otel_ot_shim_tracer.py | 26 ++++ .../test_active_span_replacement/README.rst | 20 +++ .../test_active_span_replacement/__init__.py | 0 .../test_asyncio.py | 54 +++++++ .../test_threads.py | 50 +++++++ .../testbed/test_client_server/README.rst | 19 +++ .../testbed/test_client_server/__init__.py | 0 .../test_client_server/test_asyncio.py | 79 ++++++++++ .../test_client_server/test_threads.py | 75 ++++++++++ .../test_common_request_handler/README.rst | 23 +++ .../test_common_request_handler/__init__.py | 0 .../request_handler.py | 38 +++++ .../test_asyncio.py | 136 ++++++++++++++++++ .../test_threads.py | 119 +++++++++++++++ .../testbed/test_late_span_finish/README.rst | 18 +++ .../testbed/test_late_span_finish/__init__.py | 0 .../test_late_span_finish/test_asyncio.py | 51 +++++++ .../test_late_span_finish/test_threads.py | 44 ++++++ .../test_listener_per_request/README.rst | 19 +++ .../test_listener_per_request/__init__.py | 0 .../response_listener.py | 7 + .../test_listener_per_request/test_asyncio.py | 45 ++++++ .../test_listener_per_request/test_threads.py | 45 ++++++ .../test_multiple_callbacks/README.rst | 44 ++++++ .../test_multiple_callbacks/__init__.py | 0 .../test_multiple_callbacks/test_asyncio.py | 59 ++++++++ .../test_multiple_callbacks/test_threads.py | 59 ++++++++ .../testbed/test_nested_callbacks/README.rst | 47 ++++++ .../testbed/test_nested_callbacks/__init__.py | 0 .../test_nested_callbacks/test_asyncio.py | 57 ++++++++ .../test_nested_callbacks/test_threads.py | 59 ++++++++ .../test_subtask_span_propagation/README.rst | 42 ++++++ .../test_subtask_span_propagation/__init__.py | 0 .../test_asyncio.py | 32 +++++ .../test_threads.py | 33 +++++ .../tests/testbed/testcase.py | 46 ++++++ .../tests/testbed/utils.py | 78 ++++++++++ opentelemetry-api/tests/__init__.py | 9 ++ scripts/coverage.sh | 26 +++- tox.ini | 8 +- 43 files changed, 1506 insertions(+), 9 deletions(-) create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py diff --git a/dev-requirements.txt b/dev-requirements.txt index b8ae14c89c..be74d804b3 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,3 +10,4 @@ pytest!=5.2.3 pytest-cov>=2.8 readme-renderer~=24.0 httpretty~=1.0 +opentracing~=2.2.0 \ No newline at end of file diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst new file mode 100644 index 0000000000..ba7119cd68 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst @@ -0,0 +1,47 @@ + +Testbed suite for the OpenTelemetry-OpenTracing Bridge +====================================================== + +Testbed suite designed to test the API changes. + +Build and test. +--------------- + +.. code-block:: sh + + tox -e py37-test-opentracing-shim + +Alternatively, due to the organization of the suite, it's possible to run directly the tests using ``py.test``\ : + +.. code-block:: sh + + py.test -s testbed/test_multiple_callbacks/test_threads.py + +Tested frameworks +----------------- + +Currently the examples cover ``threading`` and ``asyncio``. + +List of patterns +---------------- + + +* `Active Span replacement `_ - Start an isolated task and query for its results in another task/thread. +* `Client-Server `_ - Typical client-server example. +* `Common Request Handler `_ - One request handler for all requests. +* `Late Span finish `_ - Late parent ``Span`` finish. +* `Multiple callbacks `_ - Multiple callbacks spawned at the same time. +* `Nested callbacks `_ - One callback at a time, defined in a pipeline fashion. +* `Subtask Span propagation `_ - ``Span`` propagation for subtasks/coroutines. + +Adding new patterns +------------------- + +A new pattern is composed of a directory under *testbed* with the *test_* prefix, and containing the files for each platform, also with the *test_* prefix: + +.. code-block:: + + testbed/ + test_new_pattern/ + test_threads.py + test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py new file mode 100644 index 0000000000..b3b4271f02 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py @@ -0,0 +1,26 @@ +import opentelemetry.ext.opentracing_shim as opentracingshim +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +class MockTracer(opentracingshim.TracerShim): + """Wrapper of `opentracingshim.TracerShim`. + + MockTracer extends `opentracingshim.TracerShim` by adding a in memory + span exporter that can be used to get the list of finished spans.""" + + def __init__(self): + tracer_provider = trace.TracerProvider() + oteltracer = tracer_provider.get_tracer(__name__) + super(MockTracer, self).__init__(oteltracer) + exporter = InMemorySpanExporter() + span_processor = SimpleExportSpanProcessor(exporter) + tracer_provider.add_span_processor(span_processor) + + self.exporter = exporter + + def finished_spans(self): + return self.exporter.get_finished_spans() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst new file mode 100644 index 0000000000..6bb4d2f35c --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst @@ -0,0 +1,20 @@ + +Active Span replacement example. +================================ + +This example shows a ``Span`` being created and then passed to an asynchronous task, which will temporary activate it to finish its processing, and further restore the previously active ``Span``. + +``threading`` implementation: + +.. code-block:: python + + # Create a new Span for this task + with self.tracer.start_active_span("task"): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span("subtask"): + pass diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py new file mode 100644 index 0000000000..cb555dc109 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py @@ -0,0 +1,54 @@ +from __future__ import print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import stop_loop_when + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span("initial") + self.submit_another_task(span) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 3, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["initial", "subtask", "task"]) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent, None) + + async def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span("task"): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span("subtask"): + pass + + def submit_another_task(self, span): + self.loop.create_task(self.task(span)) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py new file mode 100644 index 0000000000..e382d5d716 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py @@ -0,0 +1,50 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + # use max_workers=3 as a general example even if only one would suffice + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span("initial") + self.submit_another_task(span) + + self.executor.shutdown(True) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["initial", "subtask", "task"]) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent, None) + self.assertEqual(spans[2].parent, None) + + def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span("task"): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span("subtask"): + pass + + def submit_another_task(self, span): + self.executor.submit(self.task, span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst new file mode 100644 index 0000000000..730fd9295d --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst @@ -0,0 +1,19 @@ + +Client-Server example. +====================== + +This example shows a ``Span`` created by a ``Client``, which will send a ``Message`` / ``SpanContext`` to a ``Server``, which will in turn extract such context and use it as parent of a new (server-side) ``Span``. + +``Client.send()`` is used to send messages and inject the ``SpanContext`` using the ``TEXT_MAP`` format, and ``Server.process()`` will process received messages and will extract the context used as parent. + +.. code-block:: python + + def send(self): + with self.tracer.start_active_span("send") as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject(scope.span.context, + opentracing.Format.TEXT_MAP, + message) + self.queue.put(message) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py new file mode 100644 index 0000000000..5379584719 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py @@ -0,0 +1,79 @@ +from __future__ import print_function + +import asyncio + +import opentracing +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, get_one_by_tag, stop_loop_when + +logger = get_logger(__name__) + + +class Server: + def __init__(self, *args, **kwargs): + tracer = kwargs.pop("tracer") + queue = kwargs.pop("queue") + super(Server, self).__init__(*args, **kwargs) + + self.tracer = tracer + self.queue = queue + + async def run(self): + value = await self.queue.get() + self.process(value) + + def process(self, message): + logger.info("Processing message in server") + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span("receive", child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client: + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + async def send(self): + with self.tracer.start_active_span("send") as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject( + scope.span.context, opentracing.Format.TEXT_MAP, message + ) + await self.queue.put(message) + + logger.info("Sent message from client") + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.queue = asyncio.Queue() + self.loop = asyncio.get_event_loop() + self.server = Server(tracer=self.tracer, queue=self.queue) + + def test(self): + client = Client(self.tracer, self.queue) + self.loop.create_task(self.server.run()) + self.loop.create_task(client.send()) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 2, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + ) + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py new file mode 100644 index 0000000000..619781edec --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py @@ -0,0 +1,75 @@ +from __future__ import print_function + +from queue import Queue +from threading import Thread + +import opentracing +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import await_until, get_logger, get_one_by_tag + +logger = get_logger(__name__) + + +class Server(Thread): + def __init__(self, *args, **kwargs): + tracer = kwargs.pop("tracer") + queue = kwargs.pop("queue") + super(Server, self).__init__(*args, **kwargs) + + self.daemon = True + self.tracer = tracer + self.queue = queue + + def run(self): + value = self.queue.get() + self.process(value) + + def process(self, message): + logger.info("Processing message in server") + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span("receive", child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client: + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + def send(self): + with self.tracer.start_active_span("send") as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject( + scope.span.context, opentracing.Format.TEXT_MAP, message + ) + self.queue.put(message) + + logger.info("Sent message from client") + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.queue = Queue() + self.server = Server(tracer=self.tracer, queue=self.queue) + self.server.start() + + def test(self): + client = Client(self.tracer, self.queue) + client.send() + + await_until(lambda: len(self.tracer.finished_spans()) >= 2) + + spans = self.tracer.finished_spans() + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + ) + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst new file mode 100644 index 0000000000..1bcda539bb --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst @@ -0,0 +1,23 @@ + +Common Request Handler example. +=============================== + +This example shows a ``Span`` used with ``RequestHandler``, which is used as a middleware (as in web frameworks) to manage a new ``Span`` per operation through its ``before_request()`` / ``after_response()`` methods. + +Implementation details: + + +* For ``threading``, no active ``Span`` is consumed as the tasks may be run concurrently on different threads, and an explicit ``SpanContext`` has to be saved to be used as parent. + +RequestHandler implementation: + +.. code-block:: python + + def before_request(self, request, request_context): + + # If we should ignore the active Span, use any passed SpanContext + # as the parent. Else, use the active one. + span = self.tracer.start_span("send", + child_of=self.context, + ignore_active_span=True) + diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py new file mode 100644 index 0000000000..47ff088025 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py @@ -0,0 +1,38 @@ +from __future__ import print_function + +from opentracing.ext import tags + +from ..utils import get_logger + +logger = get_logger(__name__) + + +class RequestHandler: + def __init__(self, tracer, context=None, ignore_active_span=True): + self.tracer = tracer + self.context = context + self.ignore_active_span = ignore_active_span + + def before_request(self, request, request_context): + logger.info("Before request %s", request) + + # If we should ignore the active Span, use any passed SpanContext + # as the parent. Else, use the active one. + if self.ignore_active_span: + span = self.tracer.start_span( + "send", child_of=self.context, ignore_active_span=True + ) + else: + span = self.tracer.start_span("send") + + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + request_context["span"] = span + + def after_request(self, request, request_context): + # pylint: disable=no-self-use + logger.info("After request %s", request) + + span = request_context.get("span") + if span is not None: + span.finish() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py new file mode 100644 index 0000000000..b0216dd756 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py @@ -0,0 +1,136 @@ +from __future__ import print_function + +import asyncio + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, get_one_by_operation_name, stop_loop_when +from .request_handler import RequestHandler + +logger = get_logger(__name__) + + +class Client: + def __init__(self, request_handler, loop): + self.request_handler = request_handler + self.loop = loop + + async def send_task(self, message): + request_context = {} + + async def before_handler(): + self.request_handler.before_request(message, request_context) + + async def after_handler(): + self.request_handler.after_request(message, request_context) + + await before_handler() + await after_handler() + + return "%s::response" % message + + def send(self, message): + return self.send_task(message) + + def send_sync(self, message): + return self.loop.run_until_complete(self.send_task(message)) + + +class TestAsyncio(OpenTelemetryTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed in different Tasks, and no Span propagation + among them is done automatically. + Therefore we cannot use current active span and activate span. + So one issue here is setting correct parent span. + """ + + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + self.client = Client(RequestHandler(self.tracer), self.loop) + + def test_two_callbacks(self): + res_future1 = self.loop.create_task(self.client.send("message1")) + res_future2 = self.loop.create_task(self.client.send("message2")) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 2, + timeout=5.0, + ) + self.loop.run_forever() + + self.assertEqual("message1::response", res_future1.result()) + self.assertEqual("message2::response", res_future2.result()) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + for span in spans: + self.assertEqual( + span.attributes.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT, + ) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent) + self.assertIsNone(spans[1].parent) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child.""" + + async def do_task(): + with self.tracer.start_active_span("parent"): + response = await self.client.send_task("no_parent") + self.assertEqual("no_parent::response", response) + + self.loop.run_until_complete(do_task()) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + child_span = get_one_by_operation_name(spans, "send") + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_good_solution_to_set_parent(self): + """Asyncio and contextvars are integrated, in this case it is not needed + to activate current span by hand. + """ + + async def do_task(): + with self.tracer.start_active_span("parent"): + # Set ignore_active_span to False indicating that the + # framework will do it for us. + req_handler = RequestHandler( + self.tracer, ignore_active_span=False, + ) + client = Client(req_handler, self.loop) + response = await client.send_task("correct_parent") + + self.assertEqual("correct_parent::response", response) + + # Send second request, now there is no active parent, + # but it will be set, ups + response = await client.send_task("wrong_parent") + self.assertEqual("wrong_parent::response", response) + + self.loop.run_until_complete(do_task()) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsNotChildOf(spans[2], parent_span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py new file mode 100644 index 0000000000..4ab8b2a075 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py @@ -0,0 +1,119 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, get_one_by_operation_name +from .request_handler import RequestHandler + +logger = get_logger(__name__) + + +class Client: + def __init__(self, request_handler, executor): + self.request_handler = request_handler + self.executor = executor + + def send_task(self, message): + request_context = {} + + def before_handler(): + self.request_handler.before_request(message, request_context) + + def after_handler(): + self.request_handler.after_request(message, request_context) + + self.executor.submit(before_handler).result() + self.executor.submit(after_handler).result() + + return "%s::response" % message + + def send(self, message): + return self.executor.submit(self.send_task, message) + + def send_sync(self, message, timeout=5.0): + fut = self.executor.submit(self.send_task, message) + return fut.result(timeout=timeout) + + +class TestThreads(OpenTelemetryTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed concurrently in different threads which are + reused (executor). Therefore we cannot use current active span and + activate span. So one issue here is setting correct parent span. + """ + + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + self.client = Client(RequestHandler(self.tracer), self.executor) + + def test_two_callbacks(self): + response_future1 = self.client.send("message1") + response_future2 = self.client.send("message2") + + self.assertEqual("message1::response", response_future1.result(5.0)) + self.assertEqual("message2::response", response_future2.result(5.0)) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + for span in spans: + self.assertEqual( + span.attributes.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT, + ) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent) + self.assertIsNone(spans[1].parent) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child.""" + + with self.tracer.start_active_span("parent"): + response = self.client.send_sync("no_parent") + self.assertEqual("no_parent::response", response) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + child_span = get_one_by_operation_name(spans, "send") + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_bad_solution_to_set_parent(self): + """Solution is bad because parent is per client and is not automatically + activated depending on the context. + """ + + with self.tracer.start_active_span("parent") as scope: + client = Client( + # Pass a span context to be used ad the parent. + RequestHandler(self.tracer, scope.span.context), + self.executor, + ) + response = client.send_sync("correct_parent") + self.assertEqual("correct_parent::response", response) + + response = client.send_sync("wrong_parent") + self.assertEqual("wrong_parent::response", response) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsChildOf(spans[2], parent_span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst new file mode 100644 index 0000000000..8c4ffd864a --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst @@ -0,0 +1,18 @@ + +Late Span finish example. +========================= + +This example shows a ``Span`` for a top-level operation, with independent, unknown lifetime, acting as parent of a few asynchronous subtasks (which must re-activate it but not finish it). + +.. code-block:: python + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + def task(name, interval): + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + time.sleep(interval) + + self.executor.submit(task, "task1", 0.1) + self.executor.submit(task, "task2", 0.3) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py new file mode 100644 index 0000000000..128073b056 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py @@ -0,0 +1,51 @@ +from __future__ import print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, stop_loop_when + +logger = get_logger(__name__) + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + parent_span = self.tracer.start_span("parent") + self.submit_subtasks(parent_span) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 2, + timeout=5.0, + ) + self.loop.run_forever() + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["task1", "task2", "parent"]) + + for idx in range(2): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + self.assertTrue(spans[idx].end_time <= spans[-1].end_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + async def task(name): + logger.info("Running %s", name) + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + await asyncio.sleep(0.1) + + self.loop.create_task(task("task1")) + self.loop.create_task(task("task2")) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py new file mode 100644 index 0000000000..5972eb8b92 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py @@ -0,0 +1,44 @@ +from __future__ import print_function + +import time +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + parent_span = self.tracer.start_span("parent") + self.submit_subtasks(parent_span) + + # Wait for the threadpool to be done. + self.executor.shutdown(True) + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["task1", "task2", "parent"]) + + for idx in range(2): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + self.assertTrue(spans[idx].end_time <= spans[-1].end_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + def task(name, interval): + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + time.sleep(interval) + + self.executor.submit(task, "task1", 0.1) + self.executor.submit(task, "task2", 0.3) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst new file mode 100644 index 0000000000..952d1ec51d --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst @@ -0,0 +1,19 @@ + +Listener Response example. +========================== + +This example shows a ``Span`` created upon a message being sent to a ``Client``, and its handling along a related, **not shared** ``ResponseListener`` object with a ``on_response(self, response)`` method to finish it. + +.. code-block:: python + + def _task(self, message, listener): + res = "%s::response" % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span("send") + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.executor.submit(self._task, message, listener).result() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py new file mode 100644 index 0000000000..dd143c20b8 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py @@ -0,0 +1,7 @@ +class ResponseListener: + def __init__(self, span): + self.span = span + + def on_response(self, res): + del res + self.span.finish() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py new file mode 100644 index 0000000000..085c0ea813 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py @@ -0,0 +1,45 @@ +from __future__ import print_function + +import asyncio + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_one_by_tag +from .response_listener import ResponseListener + + +class Client: + def __init__(self, tracer, loop): + self.tracer = tracer + self.loop = loop + + async def task(self, message, listener): + res = "%s::response" % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span("send") + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.loop.run_until_complete(self.task(message, listener)) + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + client = Client(self.tracer, self.loop) + res = client.send_sync("message") + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py new file mode 100644 index 0000000000..8f82e1fb15 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py @@ -0,0 +1,45 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_one_by_tag +from .response_listener import ResponseListener + + +class Client: + def __init__(self, tracer): + self.tracer = tracer + self.executor = ThreadPoolExecutor(max_workers=3) + + def _task(self, message, listener): + # pylint: disable=no-self-use + res = "%s::response" % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span("send") + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.executor.submit(self._task, message, listener).result() + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + + def test_main(self): + client = Client(self.tracer) + res = client.send_sync("message") + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst new file mode 100644 index 0000000000..204f282cf2 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst @@ -0,0 +1,44 @@ + +Multiple callbacks example. +=========================== + +This example shows a ``Span`` created for a top-level operation, covering a set of asynchronous operations (representing callbacks), and have this ``Span`` finished when **all** of them have been executed. + +``Client.send()`` is used to create a new asynchronous operation (callback), and in turn every operation both restores the active ``Span``, and creates a child ``Span`` (useful for measuring the performance of each callback). + +Implementation details: + + +* For ``threading``, a thread-safe counter is put in each ``Span`` to keep track of the pending callbacks, and call ``Span.finish()`` when the count becomes 0. +* For ``asyncio`` the children corotuines representing the subtasks are simply yielded over, so no counter is needed. + +``threading`` implementation: + +.. code-block:: python + + def task(self, interval, parent_span): + logger.info("Starting task") + + try: + scope = self.tracer.scope_manager.activate(parent_span, False) + with self.tracer.start_active_span("task"): + time.sleep(interval) + finally: + scope.close() + if parent_span._ref_count.decr() == 0: + parent_span.finish() + +``asyncio`` implementation: + +.. code-block:: python + + async def task(self, interval, parent_span): + logger.info("Starting task") + + with self.tracer.start_active_span("task"): + await asyncio.sleep(interval) + + # Invoke and yield over the corotuines. + with self.tracer.start_active_span("parent"): + tasks = self.submit_callbacks() + await asyncio.gather(*tasks) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py new file mode 100644 index 0000000000..36043d0a9b --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +import asyncio +import random + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, stop_loop_when + +random.seed() +logger = get_logger(__name__) + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Need to run within a Task, as the scope manager depends + # on Task.current_task() + async def main_task(): + with self.tracer.start_active_span("parent"): + tasks = self.submit_callbacks() + await asyncio.gather(*tasks) + + self.loop.create_task(main_task()) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 4, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 4) + self.assertNamesEqual(spans, ["task", "task", "task", "parent"]) + + for idx in range(3): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + + async def task(self, interval, parent_span): + logger.info("Starting task") + + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span("task"): + await asyncio.sleep(interval) + + def submit_callbacks(self): + parent_span = self.tracer.scope_manager.active.span + tasks = [] + for _ in range(3): + interval = 0.1 + random.randint(200, 500) * 0.001 + task = self.loop.create_task(self.task(interval, parent_span)) + tasks.append(task) + + return tasks diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py new file mode 100644 index 0000000000..b24ae643e3 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +import random +import time +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import RefCount, get_logger + +random.seed() +logger = get_logger(__name__) + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + try: + scope = self.tracer.start_active_span( + "parent", finish_on_close=False + ) + scope.span.ref_count = RefCount(1) + self.submit_callbacks(scope.span) + finally: + scope.close() + if scope.span.ref_count.decr() == 0: + scope.span.finish() + + self.executor.shutdown(True) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 4) + self.assertNamesEqual(spans, ["task", "task", "task", "parent"]) + + for idx in range(3): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + + def task(self, interval, parent_span): + logger.info("Starting task") + + try: + scope = self.tracer.scope_manager.activate(parent_span, False) + with self.tracer.start_active_span("task"): + time.sleep(interval) + finally: + scope.close() + if parent_span.ref_count.decr() == 0: + parent_span.finish() + + def submit_callbacks(self, parent_span): + for _ in range(3): + parent_span.ref_count.incr() + self.executor.submit( + self.task, 0.1 + random.randint(200, 500) * 0.001, parent_span + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst new file mode 100644 index 0000000000..c191431ccc --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst @@ -0,0 +1,47 @@ + +Nested callbacks example. +========================= + +This example shows a ``Span`` for a top-level operation, and how it can be passed down on a list of nested callbacks (always one at a time), have it as the active one for each of them, and finished **only** when the last one executes. For Python, we have decided to do it in a **fire-and-forget** fashion. + +Implementation details: + + +* For ``threading``, the ``Span`` is manually activatted it in each corotuine/task. +* For ``asyncio``, the active ``Span`` is not activated down the chain as the ``Context`` automatically propagates it. + +``threading`` implementation: + +.. code-block:: python + + def submit(self): + span = self.tracer.scope_manager.active.span + + def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key1", "1") + + def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key2", "2") + ... + +``asyncio`` implementation: + +.. code-block:: python + + async def task1(): + span.set_tag("key1", "1") + + async def task2(): + span.set_tag("key2", "2") + + async def task3(): + span.set_tag("key3", "3") + span.finish() + + self.loop.create_task(task3()) + + self.loop.create_task(task2()) + + self.loop.create_task(task1()) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py new file mode 100644 index 0000000000..12eb436277 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py @@ -0,0 +1,57 @@ +from __future__ import print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import stop_loop_when + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + async def task(): + with self.tracer.start_active_span("one", finish_on_close=False): + self.submit() + + self.loop.create_task(task()) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) == 1, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "one") + + for idx in range(1, 4): + self.assertEqual( + spans[0].attributes.get("key%s" % idx, None), str(idx) + ) + + def submit(self): + span = self.tracer.scope_manager.active.span + + async def task1(): + span.set_tag("key1", "1") + + async def task2(): + span.set_tag("key2", "2") + + async def task3(): + span.set_tag("key3", "3") + span.finish() + + self.loop.create_task(task3()) + + self.loop.create_task(task2()) + + self.loop.create_task(task1()) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py new file mode 100644 index 0000000000..a1d35c35d8 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import await_until + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def tearDown(self): + self.executor.shutdown(False) + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + with self.tracer.start_active_span("one", finish_on_close=False): + self.submit() + + # Cannot shutdown the executor and wait for the callbacks + # to be run, as in such case only the first will be executed, + # and the rest will get canceled. + await_until(lambda: len(self.tracer.finished_spans()) == 1, 5) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "one") + + for idx in range(1, 4): + self.assertEqual( + spans[0].attributes.get("key%s" % idx, None), str(idx) + ) + + def submit(self): + span = self.tracer.scope_manager.active.span + + def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key1", "1") + + def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key2", "2") + + def task3(): + with self.tracer.scope_manager.activate( + span, True + ): + span.set_tag("key3", "3") + + self.executor.submit(task3) + + self.executor.submit(task2) + + self.executor.submit(task1) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst new file mode 100644 index 0000000000..eaeda8e6f8 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst @@ -0,0 +1,42 @@ + +Subtask Span propagation example. +================================= + +This example shows an active ``Span`` being simply propagated to the subtasks -either threads or coroutines-, and finished **by** the parent task. In real-life scenarios instrumentation libraries may help with ``Span`` propagation **if** not offered by default (see implementation details below), but we show here the case without such help. + +Implementation details: + +* For ``threading``, the ``Span`` is manually passed down the call chain, activating it in each corotuine/task. +* For ``asyncio``, the active ``Span`` is not passed nor activated down the chain as the ``Context`` automatically propagates it. + +``threading`` implementation: + +.. code-block:: python + + def parent_task(self, message): + with self.tracer.start_active_span("parent") as scope: + f = self.executor.submit(self.child_task, message, scope.span) + res = f.result() + + return res + + def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span("child"): + return "%s::response" % message + +``asyncio`` implementation: + +.. code-block:: python + + async def parent_task(self, message): # noqa + with self.tracer.start_active_span("parent"): + res = await self.child_task(message) + + return res + + async def child_task(self, message): + # No need to pass/activate the parent Span, as it stays in the context. + with self.tracer.start_active_span("child"): + return "%s::response" % message + diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py new file mode 100644 index 0000000000..6e54456070 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + res = self.loop.run_until_complete(self.parent_task("message")) + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ["child", "parent"]) + self.assertIsChildOf(spans[0], spans[1]) + + async def parent_task(self, message): # noqa + with self.tracer.start_active_span("parent"): + res = await self.child_task(message) + + return res + + async def child_task(self, message): + # No need to pass/activate the parent Span, as it stays in the context. + with self.tracer.start_active_span("child"): + return "%s::response" % message diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py new file mode 100644 index 0000000000..1ba5f697ca --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, print_function + +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + res = self.executor.submit(self.parent_task, "message").result() + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ["child", "parent"]) + self.assertIsChildOf(spans[0], spans[1]) + + def parent_task(self, message): + with self.tracer.start_active_span("parent") as scope: + fut = self.executor.submit(self.child_task, message, scope.span) + res = fut.result() + + return res + + def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span("child"): + return "%s::response" % message diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py new file mode 100644 index 0000000000..c1ce6ea5ab --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py @@ -0,0 +1,46 @@ +import unittest + +import opentelemetry.trace as trace_api + + +# pylint: disable=C0103 +class OpenTelemetryTestCase(unittest.TestCase): + def assertSameTrace(self, spanA, spanB): + return self.assertEqual(spanA.context.trace_id, spanB.context.trace_id) + + def assertNotSameTrace(self, spanA, spanB): + return self.assertNotEqual( + spanA.context.trace_id, spanB.context.trace_id + ) + + def assertIsChildOf(self, spanA, spanB): + # spanA is child of spanB + self.assertIsNotNone(spanA.parent) + + ctxA = spanA.parent + if isinstance(spanA.parent, trace_api.Span): + ctxA = spanA.parent.context + + ctxB = spanB + if isinstance(ctxB, trace_api.Span): + ctxB = spanB.context + + return self.assertEqual(ctxA.span_id, ctxB.span_id) + + def assertIsNotChildOf(self, spanA, spanB): + # spanA is NOT child of spanB + if spanA.parent is None: + return + + ctxA = spanA.parent + if isinstance(spanA.parent, trace_api.Span): + ctxA = spanA.parent.context + + ctxB = spanB + if isinstance(ctxB, trace_api.Span): + ctxB = spanB.context + + self.assertNotEqual(ctxA.span_id, ctxB.span_id) + + def assertNamesEqual(self, spans, names): + self.assertEqual(list(map(lambda x: x.name, spans)), names) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py new file mode 100644 index 0000000000..a7b977f3d7 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py @@ -0,0 +1,78 @@ +from __future__ import print_function + +import logging +import threading +import time + + +class RefCount: + """Thread-safe counter""" + + def __init__(self, count=1): + self._lock = threading.Lock() + self._count = count + + def incr(self): + with self._lock: + self._count += 1 + return self._count + + def decr(self): + with self._lock: + self._count -= 1 + return self._count + + +def await_until(func, timeout=5.0): + """Polls for func() to return True""" + end_time = time.time() + timeout + while time.time() < end_time and not func(): + time.sleep(0.01) + + +def stop_loop_when(loop, cond_func, timeout=5.0): + """ + Registers a periodic callback that stops the loop when cond_func() == True. + Compatible with both Tornado and asyncio. + """ + if cond_func() or timeout <= 0.0: + loop.stop() + return + + timeout -= 0.1 + loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) + + +def get_logger(name): + """Returns a logger with log level set to INFO""" + logging.basicConfig(level=logging.INFO) + return logging.getLogger(name) + + +def get_one_by_tag(spans, key, value): + """Return a single Span with a tag value/key from a list, + errors if more than one is found.""" + + found = [] + for span in spans: + if span.attributes.get(key) == value: + found.append(span) + + if len(found) > 1: + raise RuntimeError("Too many values") + + return found[0] if len(found) > 0 else None + + +def get_one_by_operation_name(spans, name): + """Return a single Span with a name from a list, + errors if more than one is found.""" + found = [] + for span in spans: + if span.name == name: + found.append(span) + + if len(found) > 1: + raise RuntimeError("Too many values") + + return found[0] if len(found) > 0 else None diff --git a/opentelemetry-api/tests/__init__.py b/opentelemetry-api/tests/__init__.py index b0a6f42841..bc48946761 100644 --- a/opentelemetry-api/tests/__init__.py +++ b/opentelemetry-api/tests/__init__.py @@ -11,3 +11,12 @@ # 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 pkg_resources + +# naming the tests module as a namespace package ensures that +# relative imports will resolve properly for other test packages, +# as it enables searching for a composite of multiple test modules. +# +# only the opentelemetry-api directory needs this code, as it is +# the first tests module found by pylint during eachdist.py lint +pkg_resources.declare_namespace(__name__) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 8e09ae23a3..0b45fbf643 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -3,13 +3,25 @@ set -e function cov { - pytest \ - --ignore-glob=*/setup.py \ - --cov ${1} \ - --cov-append \ - --cov-branch \ - --cov-report='' \ - ${1} + if [ ${TOX_ENV_NAME:0:4} == "py34" ] + then + pytest \ + --ignore-glob=*/setup.py \ + --ignore-glob=ext/opentelemetry-ext-opentracing-shim/tests/testbed/* \ + --cov ${1} \ + --cov-append \ + --cov-branch \ + --cov-report='' \ + ${1} + else + pytest \ + --ignore-glob=*/setup.py \ + --cov ${1} \ + --cov-append \ + --cov-branch \ + --cov-report='' \ + ${1} + fi } PYTHON_VERSION=$(python -c 'import sys; print(".".join(map(str, sys.version_info[:3])))') diff --git a/tox.ini b/tox.ini index 978155616a..381e8604e1 100644 --- a/tox.ini +++ b/tox.ini @@ -227,10 +227,11 @@ commands_pre = jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger - datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-datadog - + opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim + datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-datadog + zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin sqlalchemy: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-sqlalchemy @@ -258,6 +259,9 @@ commands = ; implicit Any due to unfollowed import would result). mypyinstalled: mypy --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict +[testenv:py34-test-opentracing-shim] +commands = + pytest --ignore-glob='*[asyncio].py' [testenv:lint] basepython: python3.8 From f0ead9347930921782ae1fb46d62c329925baa58 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 2 Jun 2020 21:44:55 -0700 Subject: [PATCH 09/12] chore: removing Oberon00 from approvers (#770) Co-authored-by: Yusuke Tsutsumi --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b297a77cd1..613b4e6acb 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Carlos Alberto Cortez](https://github.com/carlosalberto), LightStep - [Chris Kleinknecht](https://github.com/c24t), Google -- [Christian Neumüller](https://github.com/Oberon00), Dynatrace - [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Leighton Chen](https://github.com/lzchen), Microsoft @@ -119,6 +118,12 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Alex Boten](https://github.com/codeboten), LightStep - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group +### Thanks to all the people who already contributed! + + + + + *Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* ## Release Schedule From efacb60892b443c86cd83ac8c9d799824afc4e8a Mon Sep 17 00:00:00 2001 From: Seth Maxwell Date: Fri, 5 Jun 2020 14:03:37 +0000 Subject: [PATCH 10/12] Add test for byte type attributes --- opentelemetry-sdk/tests/trace/test_trace.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 7e6ab49c39..e511cbedd3 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -487,6 +487,11 @@ def test_invalid_attribute_values(self): self.assertEqual(len(root.attributes), 0) + def test_byte_type_attribute_value(self): + with self.tracer.start_as_current_span("root") as root: + root.set_attribute("byte-type-attribute", b"byte") + self.assertTrue(isinstance(root.attributes["byte-type-attribute"], str)) + def test_check_attribute_helper(self): # pylint: disable=protected-access self.assertFalse(trace._is_valid_attribute_value([1, 2, 3.4, "ss", 4])) From 20865158ba80cae6257285243bceff8f91c1519c Mon Sep 17 00:00:00 2001 From: Seth Maxwell Date: Fri, 5 Jun 2020 16:35:38 +0000 Subject: [PATCH 11/12] Add invalid byte sequence test --- opentelemetry-sdk/CHANGELOG.md | 2 ++ opentelemetry-sdk/tests/trace/test_trace.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 29aaed01dc..7ac7830453 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -4,6 +4,8 @@ - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) +- bugfix: byte type attributes are decoded before adding to attributes dict + ([#775](https://github.com/open-telemetry/opentelemetry-python/pull/775)) ## 0.8b0 diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e511cbedd3..f6fd16f1d8 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -489,8 +489,12 @@ def test_invalid_attribute_values(self): def test_byte_type_attribute_value(self): with self.tracer.start_as_current_span("root") as root: - root.set_attribute("byte-type-attribute", b"byte") - self.assertTrue(isinstance(root.attributes["byte-type-attribute"], str)) + with self.assertLogs(level=WARNING): + root.set_attribute("invalid-byte-type-attribute", b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1") + self.assertFalse("invalid-byte-type-attribute" in root.attributes) + + root.set_attribute("valid-byte-type-attribute", b"valid byte") + self.assertTrue(isinstance(root.attributes["valid-byte-type-attribute"], str)) def test_check_attribute_helper(self): # pylint: disable=protected-access From fbde3200d88c03abf364181e6c22df2b17110b6c Mon Sep 17 00:00:00 2001 From: Seth Maxwell Date: Fri, 5 Jun 2020 17:44:21 +0000 Subject: [PATCH 12/12] Fix linting issues --- opentelemetry-sdk/tests/trace/test_trace.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index f6fd16f1d8..32348d87d9 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -490,11 +490,18 @@ def test_invalid_attribute_values(self): def test_byte_type_attribute_value(self): with self.tracer.start_as_current_span("root") as root: with self.assertLogs(level=WARNING): - root.set_attribute("invalid-byte-type-attribute", b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1") - self.assertFalse("invalid-byte-type-attribute" in root.attributes) + root.set_attribute( + "invalid-byte-type-attribute", + b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1", + ) + self.assertFalse( + "invalid-byte-type-attribute" in root.attributes + ) root.set_attribute("valid-byte-type-attribute", b"valid byte") - self.assertTrue(isinstance(root.attributes["valid-byte-type-attribute"], str)) + self.assertTrue( + isinstance(root.attributes["valid-byte-type-attribute"], str) + ) def test_check_attribute_helper(self): # pylint: disable=protected-access