diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py new file mode 100644 index 0000000000..0c011f9976 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -0,0 +1,78 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import typing +from enum import Enum + +from .. import Span, SpanProcessor + +logger = logging.getLogger(__name__) + + +class SpanExportResult(Enum): + SUCCESS = 0 + FAILED_RETRYABLE = 1 + FAILED_NOT_RETRYABLE = 2 + + +class SpanExporter: + """Interface for exporting spans. + + Interface to be implemented by services that want to export recorded in + its own format. + + To export data this MUST be registered to the :class`..Tracer` using a + `SimpleExportSpanProcessor` or a `BatchSpanProcessor`. + """ + + def export(self, spans: typing.Sequence[Span]) -> "SpanExportResult": + """Exports a batch of telemetry data. + + Args: + spans: The list of `Span`s to be exported + + Returns: + The result of the export + """ + + def shutdown(self) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + + +class SimpleExportSpanProcessor(SpanProcessor): + """Simple SpanProcessor implementation. + + SimpleExportSpanProcessor is an implementation of `SpanProcessor` that + passes ended spans directly to the configured `SpanExporter`. + """ + + def __init__(self, span_exporter: SpanExporter): + self.span_exporter = span_exporter + + def on_start(self, span: Span) -> None: + pass + + def on_end(self, span: Span) -> None: + try: + self.span_exporter.export((span,)) + # pylint: disable=broad-except + except Exception as exc: + logger.warning("Exception while exporting data: %s", exc) + + def shutdown(self) -> None: + self.span_exporter.shutdown() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py new file mode 100644 index 0000000000..b536120560 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/in_memory_span_exporter.py @@ -0,0 +1,58 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import typing + +from .. import Span +from . import SpanExporter, SpanExportResult + + +class InMemorySpanExporter(SpanExporter): + """Implementation of :class:`.Exporter` that stores spans in memory. + + This class can be used for testing purposes. It stores the exported spans + in a list in memory that can be retrieved using the + :func:`.get_finished_spans` method. + """ + + def __init__(self): + self._finished_spans = [] + self._stopped = False + self._lock = threading.Lock() + + def clear(self): + """Clear list of collected spans.""" + with self._lock: + self._finished_spans.clear() + + def get_finished_spans(self): + """Get list of collected spans.""" + with self._lock: + return tuple(self._finished_spans) + + def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: + """Stores a list of spans in memory.""" + if self._stopped: + return SpanExportResult.FAILED_NOT_RETRYABLE + with self._lock: + self._finished_spans.extend(spans) + return SpanExportResult.SUCCESS + + def shutdown(self): + """Shut downs the exporter. + + Calls to export after the exporter has been shut down will fail. + """ + self._stopped = True diff --git a/opentelemetry-sdk/tests/trace/export/__init__.py b/opentelemetry-sdk/tests/trace/export/__init__.py new file mode 100644 index 0000000000..d853a7bcf6 --- /dev/null +++ b/opentelemetry-sdk/tests/trace/export/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py new file mode 100644 index 0000000000..ef9786ca63 --- /dev/null +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -0,0 +1,44 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import export + + +class TestSimpleExportSpanProcessor(unittest.TestCase): + def test_simple_span_processor(self): + class MySpanExporter(export.SpanExporter): + def __init__(self, destination): + self.destination = destination + + def export(self, spans: trace.Span) -> export.SpanExportResult: + self.destination.extend(span.name for span in spans) + return export.SpanExportResult.SUCCESS + + tracer = trace.Tracer() + + spans_names_list = [] + + my_exporter = MySpanExporter(destination=spans_names_list) + span_processor = export.SimpleExportSpanProcessor(my_exporter) + tracer.add_span_processor(span_processor) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py new file mode 100644 index 0000000000..01d4487e2e --- /dev/null +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -0,0 +1,97 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry import trace as trace_api +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace import export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +class TestInMemorySpanExporter(unittest.TestCase): + def test_get_finished_spans(self): + tracer = trace.Tracer() + + memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(memory_exporter) + tracer.add_span_processor(span_processor) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + span_list = memory_exporter.get_finished_spans() + spans_names_list = [span.name for span in span_list] + self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) + + def test_clear(self): + tracer = trace.Tracer() + + memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(memory_exporter) + tracer.add_span_processor(span_processor) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + memory_exporter.clear() + span_list = memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + def test_shutdown(self): + tracer = trace.Tracer() + + memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(memory_exporter) + tracer.add_span_processor(span_processor) + + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + span_list = memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + memory_exporter.shutdown() + + # after shutdown no new spans are accepted + with tracer.start_span("foo"): + with tracer.start_span("bar"): + with tracer.start_span("xxx"): + pass + + span_list = memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + def test_return_code(self): + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + span_list = (span,) + memory_exporter = InMemorySpanExporter() + + ret = memory_exporter.export(span_list) + self.assertEqual(ret, export.SpanExportResult.SUCCESS) + + memory_exporter.shutdown() + + # after shutdown export should fail + ret = memory_exporter.export(span_list) + self.assertEqual(ret, export.SpanExportResult.FAILED_NOT_RETRYABLE)