diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7debd81c9db..dfa5f957bd7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 43df76e5ed69f45d993c98ea68daea3c4622ea2d + CONTRIB_REPO_SHA: 263adc5f7f524fae2c84571f656cef0896de0868 jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index e93c12b215b..b8f8546d45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow missing carrier headers to continue without raising AttributeError ([#1545](https://github.com/open-telemetry/opentelemetry-python/pull/1545)) + +### Changed +- Read-only Span attributes have been moved to ReadableSpan class + ([#1560](https://github.com/open-telemetry/opentelemetry-python/pull/1560)) + ### Removed - Remove Configuration ([#1523](https://github.com/open-telemetry/opentelemetry-python/pull/1523)) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py index cc7354217a9..f97977516db 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py @@ -24,7 +24,7 @@ VERSION_KEY, Translator, ) -from opentelemetry.sdk.trace import Span, StatusCode +from opentelemetry.sdk.trace import ReadableSpan, StatusCode from opentelemetry.util import types # pylint: disable=no-member,too-many-locals,no-self-use @@ -93,7 +93,7 @@ def _translate_attribute( return translated -def _extract_resource_tags(span: Span) -> Sequence[model_pb2.KeyValue]: +def _extract_resource_tags(span: ReadableSpan) -> Sequence[model_pb2.KeyValue]: """Extracts resource attributes from span and returns list of jaeger keyvalues. @@ -143,7 +143,7 @@ class ProtobufTranslator(Translator): def __init__(self, svc_name): self.svc_name = svc_name - def _translate_span(self, span: Span) -> model_pb2.Span: + def _translate_span(self, span: ReadableSpan) -> model_pb2.Span: ctx = span.get_span_context() # pb2 span expects in byte format @@ -177,7 +177,9 @@ def _translate_span(self, span: Span) -> model_pb2.Span: ) return jaeger_span - def _extract_tags(self, span: Span) -> Sequence[model_pb2.KeyValue]: + def _extract_tags( + self, span: ReadableSpan + ) -> Sequence[model_pb2.KeyValue]: translated = [] if span.attributes: for key, value in span.attributes.items(): @@ -226,7 +228,7 @@ def _extract_tags(self, span: Span) -> Sequence[model_pb2.KeyValue]: return translated def _extract_refs( - self, span: Span + self, span: ReadableSpan ) -> Optional[Sequence[model_pb2.SpanRef]]: if not span.links: return None @@ -244,7 +246,9 @@ def _extract_refs( ) return refs - def _extract_logs(self, span: Span) -> Optional[Sequence[model_pb2.Log]]: + def _extract_logs( + self, span: ReadableSpan + ) -> Optional[Sequence[model_pb2.Log]]: if not span.events: return None diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py index 8ca371c1b5b..9df9c716882 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py @@ -23,7 +23,7 @@ _convert_int_to_i64, _nsec_to_usec_round, ) -from opentelemetry.sdk.trace import Span, StatusCode +from opentelemetry.sdk.trace import ReadableSpan, StatusCode from opentelemetry.util import types @@ -75,7 +75,7 @@ def _translate_attribute( class ThriftTranslator(Translator): - def _translate_span(self, span: Span) -> TCollector.Span: + def _translate_span(self, span: ReadableSpan) -> TCollector.Span: ctx = span.get_span_context() trace_id = ctx.trace_id span_id = ctx.span_id @@ -106,7 +106,7 @@ def _translate_span(self, span: Span) -> TCollector.Span: ) return jaeger_span - def _extract_tags(self, span: Span) -> Sequence[TCollector.Tag]: + def _extract_tags(self, span: ReadableSpan) -> Sequence[TCollector.Tag]: translated = [] if span.attributes: @@ -151,7 +151,7 @@ def _extract_tags(self, span: Span) -> Sequence[TCollector.Tag]: return translated def _extract_refs( - self, span: Span + self, span: ReadableSpan ) -> Optional[Sequence[TCollector.SpanRef]]: if not span.links: return None @@ -170,7 +170,9 @@ def _extract_refs( ) return refs - def _extract_logs(self, span: Span) -> Optional[Sequence[TCollector.Log]]: + def _extract_logs( + self, span: ReadableSpan + ) -> Optional[Sequence[TCollector.Log]]: """Returns jaeger logs if events exists, otherwise None. Args: diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py index 26cfc41498e..80cbdfb55dd 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py @@ -156,12 +156,24 @@ def test_translate_to_jaeger(self): events=(event,), links=(link,), kind=trace_api.SpanKind.CLIENT, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[1], context=parent_span_context, parent=None + name=span_names[1], + context=parent_span_context, + parent=None, + resource=Resource({}), ), trace._Span( - name=span_names[2], context=other_context, parent=None + name=span_names[2], + context=other_context, + parent=None, + resource=Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), ), ] @@ -171,25 +183,17 @@ def test_translate_to_jaeger(self): otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) otel_spans[0].set_attribute("key_tuple", ("tuple_element",)) - otel_spans[0].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[0].set_status( Status(StatusCode.ERROR, "Example description") ) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].resource = Resource({}) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].resource = Resource({}) otel_spans[2].set_status(Status(StatusCode.OK, "Example description")) otel_spans[2].end(end_time=end_times[2]) - otel_spans[2].instrumentation_info = InstrumentationInfo( - name="name", version="version" - ) translate = Translate(otel_spans) # pylint: disable=protected-access diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index 947597ab469..c0faafc1b6e 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -235,12 +235,24 @@ def test_translate_to_jaeger(self): events=(event,), links=(link,), kind=trace_api.SpanKind.CLIENT, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[1], context=parent_span_context, parent=None + name=span_names[1], + context=parent_span_context, + parent=None, + resource=Resource({}), ), trace._Span( - name=span_names[2], context=other_context, parent=None + name=span_names[2], + context=other_context, + parent=None, + resource=Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), ), ] @@ -250,25 +262,17 @@ def test_translate_to_jaeger(self): otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) otel_spans[0].set_attribute("key_tuple", ("tuple_element",)) - otel_spans[0].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[0].set_status( Status(StatusCode.ERROR, "Example description") ) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].resource = Resource({}) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) - otel_spans[2].resource = Resource({}) otel_spans[2].set_status(Status(StatusCode.OK, "Example description")) otel_spans[2].end(end_time=end_times[2]) - otel_spans[2].instrumentation_info = InstrumentationInfo( - name="name", version="version" - ) translate = Translate(otel_spans) # pylint: disable=protected-access diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py index 613ee6482ba..6a779c09fec 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter/__init__.py @@ -25,7 +25,7 @@ from opencensus.proto.trace.v1 import trace_pb2 import opentelemetry.exporter.opencensus.util as utils -from opentelemetry.sdk.trace import Span +from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult DEFAULT_ENDPOINT = "localhost:55678" @@ -62,7 +62,7 @@ def __init__( self.node = utils.get_node(service_name, host_name) - def export(self, spans: Sequence[Span]) -> SpanExportResult: + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: try: responses = self.client.Export(self.generate_span_requests(spans)) @@ -87,7 +87,7 @@ def generate_span_requests(self, spans): # pylint: disable=too-many-branches -def translate_to_collector(spans: Sequence[Span]): +def translate_to_collector(spans: Sequence[ReadableSpan]): collector_spans = [] for span in spans: status = None diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index 96dbbc084b8..a357734bc1c 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -45,7 +45,7 @@ OTEL_EXPORTER_OTLP_SPAN_INSECURE, OTEL_EXPORTER_OTLP_SPAN_TIMEOUT, ) -from opentelemetry.sdk.trace import Span as SDKSpan +from opentelemetry.sdk.trace import Span as ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace.status import StatusCode @@ -55,7 +55,9 @@ # pylint: disable=no-member class OTLPSpanExporter( SpanExporter, - OTLPExporterMixin[SDKSpan, ExportTraceServiceRequest, SpanExportResult], + OTLPExporterMixin[ + ReadableSpan, ExportTraceServiceRequest, SpanExportResult + ], ): # pylint: disable=unsubscriptable-object """OTLP span exporter @@ -107,34 +109,34 @@ def __init__( } ) - def _translate_name(self, sdk_span: SDKSpan) -> None: + def _translate_name(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs["name"] = sdk_span.name - def _translate_start_time(self, sdk_span: SDKSpan) -> None: + def _translate_start_time(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs[ "start_time_unix_nano" ] = sdk_span.start_time - def _translate_end_time(self, sdk_span: SDKSpan) -> None: + def _translate_end_time(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs["end_time_unix_nano"] = sdk_span.end_time - def _translate_span_id(self, sdk_span: SDKSpan) -> None: + def _translate_span_id(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs[ "span_id" ] = sdk_span.context.span_id.to_bytes(8, "big") - def _translate_trace_id(self, sdk_span: SDKSpan) -> None: + def _translate_trace_id(self, sdk_span: ReadableSpan) -> None: self._collector_span_kwargs[ "trace_id" ] = sdk_span.context.trace_id.to_bytes(16, "big") - def _translate_parent(self, sdk_span: SDKSpan) -> None: + def _translate_parent(self, sdk_span: ReadableSpan) -> None: if sdk_span.parent is not None: self._collector_span_kwargs[ "parent_span_id" ] = sdk_span.parent.span_id.to_bytes(8, "big") - def _translate_context_trace_state(self, sdk_span: SDKSpan) -> None: + def _translate_context_trace_state(self, sdk_span: ReadableSpan) -> None: if sdk_span.context.trace_state is not None: self._collector_span_kwargs["trace_state"] = ",".join( [ @@ -143,7 +145,7 @@ def _translate_context_trace_state(self, sdk_span: SDKSpan) -> None: ] ) - def _translate_attributes(self, sdk_span: SDKSpan) -> None: + def _translate_attributes(self, sdk_span: ReadableSpan) -> None: if sdk_span.attributes: self._collector_span_kwargs["attributes"] = [] @@ -157,7 +159,7 @@ def _translate_attributes(self, sdk_span: SDKSpan) -> None: except Exception as error: # pylint: disable=broad-except logger.exception(error) - def _translate_events(self, sdk_span: SDKSpan) -> None: + def _translate_events(self, sdk_span: ReadableSpan) -> None: if sdk_span.events: self._collector_span_kwargs["events"] = [] @@ -181,7 +183,7 @@ def _translate_events(self, sdk_span: SDKSpan) -> None: collector_span_event ) - def _translate_links(self, sdk_span: SDKSpan) -> None: + def _translate_links(self, sdk_span: ReadableSpan) -> None: if sdk_span.links: self._collector_span_kwargs["links"] = [] @@ -207,7 +209,7 @@ def _translate_links(self, sdk_span: SDKSpan) -> None: collector_span_link ) - def _translate_status(self, sdk_span: SDKSpan) -> None: + def _translate_status(self, sdk_span: ReadableSpan) -> None: # pylint: disable=no-member if sdk_span.status is not None: deprecated_code = Status.DEPRECATED_STATUS_CODE_OK @@ -220,7 +222,7 @@ def _translate_status(self, sdk_span: SDKSpan) -> None: ) def _translate_data( - self, data: Sequence[SDKSpan] + self, data: Sequence[ReadableSpan] ) -> ExportTraceServiceRequest: # pylint: disable=attribute-defined-outside-init @@ -279,5 +281,5 @@ def _translate_data( ) ) - def export(self, spans: Sequence[SDKSpan]) -> SpanExportResult: + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return self._export(spans) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 5c2e0e7e4d0..36320b78c38 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -189,20 +189,36 @@ def test_export_json(self): parent=parent_span_context, events=(event,), links=(link,), + resource=Resource({}), ), trace._Span( - name=span_names[1], context=parent_span_context, parent=None + name=span_names[1], + context=parent_span_context, + parent=None, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[2], context=other_context, parent=None + name=span_names[2], + context=other_context, + parent=None, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[3], context=other_context, parent=None + name=span_names[3], + context=other_context, + parent=None, + resource=Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), ), ] otel_spans[0].start(start_time=start_times[0]) - otel_spans[0].resource = Resource({}) # added here to preserve order otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") @@ -213,24 +229,14 @@ def test_export_json(self): otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) otel_spans[2].set_attribute("key_string", "hello_world") - otel_spans[2].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[2].end(end_time=end_times[2]) otel_spans[3].start(start_time=start_times[3]) - otel_spans[3].resource = Resource({}) otel_spans[3].end(end_time=end_times[3]) - otel_spans[3].instrumentation_info = InstrumentationInfo( - name="name", version="version" - ) service_name = "test-service" local_endpoint = {"serviceName": service_name, "port": 9411} @@ -359,10 +365,10 @@ def test_export_json_zero_padding(self): name=span_names[0], context=span_context, parent=parent_span_context, + resource=Resource({}), ) otel_span.start(start_time=start_time) - otel_span.resource = Resource({}) otel_span.end(end_time=end_time) service_name = "test-service" @@ -416,10 +422,11 @@ def test_export_json_max_tag_length(self): trace_flags=TraceFlags(TraceFlags.SAMPLED), ) - span = trace._Span(name="test-span", context=span_context,) + span = trace._Span( + name="test-span", context=span_context, resource=Resource({}) + ) span.start() - span.resource = Resource({}) # added here to preserve order span.set_attribute("string1", "v" * 500) span.set_attribute("string2", "v" * 50) @@ -704,22 +711,38 @@ def test_export_protobuf(self): name=span_names[0], context=span_context, parent=parent_span_context, + resource=Resource({}), events=(event,), links=(link,), ), trace._Span( - name=span_names[1], context=parent_span_context, parent=None + name=span_names[1], + context=parent_span_context, + parent=None, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[2], context=other_context, parent=None + name=span_names[2], + context=other_context, + parent=None, + resource=Resource( + attributes={"key_resource": "some_resource"} + ), ), trace._Span( - name=span_names[3], context=other_context, parent=None + name=span_names[3], + context=other_context, + parent=None, + resource=Resource({}), + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), ), ] otel_spans[0].start(start_time=start_times[0]) - otel_spans[0].resource = Resource({}) # added here to preserve order otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") @@ -730,25 +753,15 @@ def test_export_protobuf(self): otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) - otel_spans[1].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[1].set_status(Status(StatusCode.OK)) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) otel_spans[2].set_attribute("key_string", "hello_world") - otel_spans[2].resource = Resource( - attributes={"key_resource": "some_resource"} - ) otel_spans[2].end(end_time=end_times[2]) otel_spans[3].start(start_time=start_times[3]) - otel_spans[3].resource = Resource({}) otel_spans[3].end(end_time=end_times[3]) - otel_spans[3].instrumentation_info = InstrumentationInfo( - name="name", version="version" - ) service_name = "test-service" local_endpoint = zipkin_pb2.Endpoint( @@ -869,10 +882,11 @@ def test_export_protobuf_max_tag_length(self): trace_flags=TraceFlags(TraceFlags.SAMPLED), ) - span = trace._Span(name="test-span", context=span_context,) + span = trace._Span( + name="test-span", context=span_context, resource=Resource({}) + ) span.start() - span.resource = Resource({}) # added here to preserve order span.set_attribute("k1", "v" * 500) span.set_attribute("k2", "v" * 50) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index b579e5ccf78..218c8256f0d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -139,6 +139,9 @@ def attributes(self) -> types.Attributes: return self._attributes +_Links = typing.Optional[typing.Sequence[Link]] + + class SpanKind(enum.Enum): """Specifies additional details on how this span relates to its parent span. @@ -231,7 +234,7 @@ def start_span( context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, - links: typing.Sequence[Link] = (), + links: _Links = None, start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, @@ -286,7 +289,7 @@ def start_as_current_span( context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, - links: typing.Sequence[Link] = (), + links: _Links = None, start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, @@ -380,7 +383,7 @@ def start_span( context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, - links: typing.Sequence[Link] = (), + links: _Links = None, start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, @@ -395,7 +398,7 @@ def start_as_current_span( context: typing.Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, - links: typing.Sequence[Link] = (), + links: _Links = None, start_time: typing.Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 2ecb82a0c3d..0a6eefbd843 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. - +# pylint: disable=too-many-lines import abc import atexit import concurrent.futures @@ -95,7 +95,7 @@ def on_start( parent_context: The parent context of the span that just started. """ - def on_end(self, span: "Span") -> None: + def on_end(self, span: "ReadableSpan") -> None: """Called when a :class:`opentelemetry.trace.Span` is ended. This method is called synchronously on the thread that ends the @@ -106,8 +106,7 @@ def on_end(self, span: "Span") -> None: """ def shutdown(self) -> None: - """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown. - """ + """Called when a :class:`opentelemetry.sdk.trace.Tracer` is shutdown.""" def force_flush(self, timeout_millis: int = 30000) -> bool: """Export all ended spans to the configured Exporter that have not yet @@ -149,13 +148,12 @@ def on_start( for sp in self._span_processors: sp.on_start(span, parent_context=parent_context) - def on_end(self, span: "Span") -> None: + def on_end(self, span: "ReadableSpan") -> None: for sp in self._span_processors: sp.on_end(span) def shutdown(self) -> None: - """Sequentially shuts down all underlying span processors. - """ + """Sequentially shuts down all underlying span processors.""" for sp in self._span_processors: sp.shutdown() @@ -234,7 +232,7 @@ def on_start( lambda sp: sp.on_start, span, parent_context=parent_context ) - def on_end(self, span: "Span") -> None: + def on_end(self, span: "ReadableSpan") -> None: self._submit_and_await(lambda sp: sp.on_end, span) def shutdown(self) -> None: @@ -387,7 +385,7 @@ def _check_span_ended(func): def wrapper(self, *args, **kwargs): already_ended = False with self._lock: # pylint: disable=protected-access - if self.end_time is None: + if self._end_time is None: func(self, *args, **kwargs) else: already_ended = True @@ -398,7 +396,167 @@ def wrapper(self, *args, **kwargs): return wrapper -class Span(trace_api.Span): +class ReadableSpan: + """Provides read-only access to span attributes""" + + def __init__( + self, + name: str = None, + context: trace_api.SpanContext = None, + parent: Optional[trace_api.SpanContext] = None, + resource: Resource = Resource.create({}), + attributes: types.Attributes = None, + events: Sequence[Event] = None, + links: Sequence[trace_api.Link] = (), + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, + instrumentation_info: InstrumentationInfo = None, + status: Status = Status(StatusCode.UNSET), + start_time: Optional[int] = None, + end_time: Optional[int] = None, + ) -> None: + self._name = name + self._context = context + self._kind = kind + self._instrumentation_info = instrumentation_info + self._parent = parent + self._start_time = start_time + self._end_time = end_time + self._attributes = attributes + self._events = events + self._links = links + self._resource = resource + self._status = status + + @property + def name(self) -> str: + return self._name + + def get_span_context(self): + return self._context + + @property + def context(self): + return self._context + + @property + def kind(self) -> trace_api.SpanKind: + return self._kind + + @property + def parent(self) -> Optional[trace_api.SpanContext]: + return self._parent + + @property + def start_time(self) -> Optional[int]: + return self._start_time + + @property + def end_time(self) -> Optional[int]: + return self._end_time + + @property + def status(self) -> trace_api.Status: + return self._status + + @property + def attributes(self) -> types.Attributes: + return MappingProxyType(self._attributes) + + @property + def events(self) -> Sequence[Event]: + return MappingProxyType(self._events) + + @property + def links(self) -> Sequence[trace_api.Link]: + return MappingProxyType(self._links) + + @property + def resource(self) -> Resource: + return self._resource + + @property + def instrumentation_info(self) -> InstrumentationInfo: + return self._instrumentation_info + + def to_json(self, indent=4): + parent_id = None + if self.parent is not None: + if isinstance(self.parent, Span): + ctx = self.parent.context + parent_id = trace_api.format_span_id(ctx.span_id) + elif isinstance(self.parent, SpanContext): + parent_id = trace_api.format_span_id(self.parent.span_id) + + start_time = None + if self._start_time: + start_time = util.ns_to_iso_str(self._start_time) + + end_time = None + if self._end_time: + end_time = util.ns_to_iso_str(self._end_time) + + if self._status is not None: + status = OrderedDict() + status["status_code"] = str(self._status.status_code.name) + if self._status.description: + status["description"] = self._status.description + + f_span = OrderedDict() + + f_span["name"] = self._name + f_span["context"] = self._format_context(self._context) + f_span["kind"] = str(self.kind) + f_span["parent_id"] = parent_id + f_span["start_time"] = start_time + f_span["end_time"] = end_time + if self._status is not None: + f_span["status"] = status + f_span["attributes"] = self._format_attributes(self._attributes) + f_span["events"] = self._format_events(self._events) + f_span["links"] = self._format_links(self._links) + f_span["resource"] = self._resource.attributes + + return json.dumps(f_span, indent=indent) + + @staticmethod + def _format_context(context): + x_ctx = OrderedDict() + x_ctx["trace_id"] = trace_api.format_trace_id(context.trace_id) + x_ctx["span_id"] = trace_api.format_span_id(context.span_id) + x_ctx["trace_state"] = repr(context.trace_state) + return x_ctx + + @staticmethod + def _format_attributes(attributes): + if isinstance(attributes, BoundedDict): + return attributes._dict # pylint: disable=protected-access + if isinstance(attributes, MappingProxyType): + return attributes.copy() + return attributes + + @staticmethod + def _format_events(events): + f_events = [] + for event in events: + f_event = OrderedDict() + f_event["name"] = event.name + f_event["timestamp"] = util.ns_to_iso_str(event.timestamp) + f_event["attributes"] = Span._format_attributes(event.attributes) + f_events.append(f_event) + return f_events + + @staticmethod + def _format_links(links): + f_links = [] + for link in links: + f_link = OrderedDict() + f_link["context"] = Span._format_context(link.context) + f_link["attributes"] = Span._format_attributes(link.attributes) + f_links.append(f_link) + return f_links + + +class Span(trace_api.Span, ReadableSpan): """See `opentelemetry.trace.Span`. Users should create `Span` objects via the `Tracer` instead of this @@ -442,30 +600,30 @@ def __init__( record_exception: bool = True, set_status_on_exception: bool = True, ) -> None: - - self.name = name - self.context = context - self.parent = parent - self.sampler = sampler - self.trace_config = trace_config - self.resource = resource - self.kind = kind + super().__init__( + name=name, + context=context, + parent=parent, + kind=kind, + resource=resource, + instrumentation_info=instrumentation_info, + ) + self._sampler = sampler + self._trace_config = trace_config self._record_exception = record_exception self._set_status_on_exception = set_status_on_exception - - self.span_processor = span_processor - self.status = Status(StatusCode.UNSET) + self._span_processor = span_processor self._lock = threading.Lock() _filter_attribute_values(attributes) if not attributes: - self.attributes = self._new_attributes() + self._attributes = self._new_attributes() else: - self.attributes = BoundedDict.from_map( + self._attributes = BoundedDict.from_map( SPAN_ATTRIBUTE_COUNT_LIMIT, attributes ) - self.events = self._new_events() + self._events = self._new_events() if events: for event in events: _filter_attribute_values(event.attributes) @@ -473,28 +631,16 @@ def __init__( event._attributes = _create_immutable_attributes( event.attributes ) - self.events.append(event) + self._events.append(event) if links is None: - self.links = self._new_links() + self._links = self._new_links() else: - self.links = BoundedList.from_seq(SPAN_LINK_COUNT_LIMIT, links) - - self._end_time = None # type: Optional[int] - self._start_time = None # type: Optional[int] - self.instrumentation_info = instrumentation_info - - @property - def start_time(self): - return self._start_time - - @property - def end_time(self): - return self._end_time + self._links = BoundedList.from_seq(SPAN_LINK_COUNT_LIMIT, links) def __repr__(self): return '{}(name="{}", context={})'.format( - type(self).__name__, self.name, self.context + type(self).__name__, self._name, self._context ) @staticmethod @@ -509,91 +655,14 @@ def _new_events(): def _new_links(): return BoundedList(SPAN_LINK_COUNT_LIMIT) - @staticmethod - def _format_context(context): - x_ctx = OrderedDict() - x_ctx["trace_id"] = trace_api.format_trace_id(context.trace_id) - x_ctx["span_id"] = trace_api.format_span_id(context.span_id) - x_ctx["trace_state"] = repr(context.trace_state) - return x_ctx - - @staticmethod - def _format_attributes(attributes): - if isinstance(attributes, BoundedDict): - return attributes._dict # pylint: disable=protected-access - if isinstance(attributes, MappingProxyType): - return attributes.copy() - return attributes - - @staticmethod - def _format_events(events): - f_events = [] - for event in events: - f_event = OrderedDict() - f_event["name"] = event.name - f_event["timestamp"] = util.ns_to_iso_str(event.timestamp) - f_event["attributes"] = Span._format_attributes(event.attributes) - f_events.append(f_event) - return f_events - - @staticmethod - def _format_links(links): - f_links = [] - for link in links: - f_link = OrderedDict() - f_link["context"] = Span._format_context(link.context) - f_link["attributes"] = Span._format_attributes(link.attributes) - f_links.append(f_link) - return f_links - - def to_json(self, indent=4): - parent_id = None - if self.parent is not None: - if isinstance(self.parent, Span): - ctx = self.parent.context - parent_id = trace_api.format_span_id(ctx.span_id) - elif isinstance(self.parent, SpanContext): - parent_id = trace_api.format_span_id(self.parent.span_id) - - start_time = None - if self.start_time: - start_time = util.ns_to_iso_str(self.start_time) - - end_time = None - if self.end_time: - end_time = util.ns_to_iso_str(self.end_time) - - if self.status is not None: - status = OrderedDict() - status["status_code"] = str(self.status.status_code.name) - if self.status.description: - status["description"] = self.status.description - - f_span = OrderedDict() - - f_span["name"] = self.name - f_span["context"] = self._format_context(self.context) - f_span["kind"] = str(self.kind) - f_span["parent_id"] = parent_id - f_span["start_time"] = start_time - f_span["end_time"] = end_time - if self.status is not None: - f_span["status"] = status - f_span["attributes"] = self._format_attributes(self.attributes) - f_span["events"] = self._format_events(self.events) - f_span["links"] = self._format_links(self.links) - f_span["resource"] = self.resource.attributes - - return json.dumps(f_span, indent=indent) - def get_span_context(self): - return self.context + return self._context def set_attributes( self, attributes: Dict[str, types.AttributeValue] ) -> None: with self._lock: - if self.end_time is not None: + if self._end_time is not None: logger.warning("Setting attribute on ended span.") return @@ -614,14 +683,14 @@ def set_attributes( except ValueError: logger.warning("Byte attribute could not be decoded.") return - self.attributes[key] = value + self._attributes[key] = value def set_attribute(self, key: str, value: types.AttributeValue) -> None: return self.set_attributes({key: value}) @_check_span_ended def _add_event(self, event: EventBase) -> None: - self.events.append(event) + self._events.append(event) def add_event( self, @@ -639,43 +708,59 @@ def add_event( ) ) + def _readable_span(self) -> ReadableSpan: + return ReadableSpan( + name=self._name, + context=self._context, + parent=self._parent, + resource=self._resource, + attributes=self._attributes, + events=self._events, + links=self._links, + kind=self.kind, + status=self._status, + start_time=self._start_time, + end_time=self._end_time, + instrumentation_info=self._instrumentation_info, + ) + def start( self, start_time: Optional[int] = None, parent_context: Optional[context_api.Context] = None, ) -> None: with self._lock: - if self.start_time is not None: + if self._start_time is not None: logger.warning("Calling start() on a started span.") return self._start_time = ( start_time if start_time is not None else time_ns() ) - self.span_processor.on_start(self, parent_context=parent_context) + self._span_processor.on_start(self, parent_context=parent_context) def end(self, end_time: Optional[int] = None) -> None: with self._lock: - if self.start_time is None: + if self._start_time is None: raise RuntimeError("Calling end() on a not started span.") - if self.end_time is not None: + if self._end_time is not None: logger.warning("Calling end() on an ended span.") return self._end_time = end_time if end_time is not None else time_ns() - self.span_processor.on_end(self) + self._span_processor.on_end(self._readable_span()) @_check_span_ended def update_name(self, name: str) -> None: - self.name = name + self._name = name def is_recording(self) -> bool: return self._end_time is None @_check_span_ended def set_status(self, status: trace_api.Status) -> None: - self.status = status + self._status = status def __exit__( self, @@ -692,7 +777,7 @@ def __exit__( # Records status if span is used as context manager # i.e. with tracer.start_span() as span: if ( - self.status.status_code is StatusCode.UNSET + self._status.status_code is StatusCode.UNSET and self._set_status_on_exception ): self.set_status( @@ -737,13 +822,13 @@ def record_exception( class _Span(Span): """Protected implementation of `opentelemetry.trace.Span`. - This constructor should only be used internally. + This constructor exists to prevent the instantiation of the `Span` class + by other mechanisms than through the `Tracer`. """ class Tracer(trace_api.Tracer): - """See `opentelemetry.trace.Tracer`. - """ + """See `opentelemetry.trace.Tracer`.""" def __init__( self, @@ -886,7 +971,7 @@ def use_span( # Records status if use_span is used # i.e. with tracer.start_as_current_span() as span: if ( - span.status.status_code is StatusCode.UNSET + span._status.status_code is StatusCode.UNSET and span._set_status_on_exception ): span.set_status( @@ -905,8 +990,7 @@ def use_span( class TracerProvider(trace_api.TracerProvider): - """See `opentelemetry.trace.TracerProvider`. - """ + """See `opentelemetry.trace.TracerProvider`.""" def __init__( self, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 4d4cc70224f..932d05f2335 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -28,7 +28,7 @@ OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_SCHEDULE_DELAY, ) -from opentelemetry.sdk.trace import Span, SpanProcessor +from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -49,7 +49,9 @@ class SpanExporter: `SimpleExportSpanProcessor` or a `BatchExportSpanProcessor`. """ - def export(self, spans: typing.Sequence[Span]) -> "SpanExportResult": + def export( + self, spans: typing.Sequence[ReadableSpan] + ) -> "SpanExportResult": """Exports a batch of telemetry data. Args: @@ -81,7 +83,7 @@ def on_start( ) -> None: pass - def on_end(self, span: Span) -> None: + def on_end(self, span: ReadableSpan) -> None: if not span.context.trace_flags.sampled: return token = attach(set_value("suppress_instrumentation", True)) @@ -185,7 +187,7 @@ def on_start( ) -> None: pass - def on_end(self, span: Span) -> None: + def on_end(self, span: ReadableSpan) -> None: if self.done: logger.warning("Already shutdown, dropping span.") return @@ -329,7 +331,7 @@ def _export_batch(self) -> int: return idx def _drain_queue(self): - """"Export all elements until queue is empty. + """Export all elements until queue is empty. Can only be called from the worker thread context because it invokes `export` that is not thread safe. @@ -378,14 +380,16 @@ def __init__( self, service_name: Optional[str] = None, out: typing.IO = sys.stdout, - formatter: typing.Callable[[Span], str] = lambda span: span.to_json() + formatter: typing.Callable[ + [ReadableSpan], str + ] = lambda span: span.to_json() + linesep, ): self.out = out self.formatter = formatter self.service_name = service_name - def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: + def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult: for span in spans: self.out.write(self.formatter(span)) self.out.flush() 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 index 967d29b3a80..e46266b93b1 100644 --- 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 @@ -15,7 +15,7 @@ import threading import typing -from opentelemetry.sdk.trace import Span +from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -42,7 +42,7 @@ def get_finished_spans(self): with self._lock: return tuple(self._finished_spans) - def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: + def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult: """Stores a list of spans in memory.""" if self._stopped: return SpanExportResult.FAILURE diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index c235e71d83e..7f9539738ca 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -109,4 +109,4 @@ def test_with_asyncio(self): for span in span_list: if span is expected_parent: continue - self.assertEqual(span.parent, expected_parent.get_span_context()) + self.assertEqual(span.parent, expected_parent.context) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 66a5a82c309..6969b65b116 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -289,10 +289,10 @@ def test_span_processor_for_source(self): # pylint:disable=protected-access self.assertIs( - span1.span_processor, tracer_provider._active_span_processor + span1._span_processor, tracer_provider._active_span_processor ) self.assertIs( - span2.span_processor, tracer_provider._active_span_processor + span2._span_processor, tracer_provider._active_span_processor ) def test_start_span_implicit(self): @@ -1132,7 +1132,7 @@ def on_start( ) -> None: self.span_list.append(span_event_start_fmt(self.name, span.name)) - def on_end(self, span: "trace.Span") -> None: + def on_end(self, span: "trace.ReadableSpan") -> None: self.span_list.append(span_event_end_fmt(self.name, span.name)) @@ -1237,8 +1237,7 @@ def test_to_json(self): is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - span = trace._Span("span-name", context) - span.resource = Resource({}) + span = trace._Span("span-name", context, resource=Resource({})) self.assertEqual( span.to_json(), @@ -1274,8 +1273,7 @@ def test_attributes_to_json(self): is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - span = trace._Span("span-name", context) - span.resource = Resource({}) + span = trace._Span("span-name", context, resource=Resource({})) span.set_attribute("key", "value") span.add_event("event", {"key2": "value2"}, 123) date_str = ns_to_iso_str(123) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py b/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py index c1ce6ea5abd..3c16682fad3 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py @@ -18,11 +18,11 @@ def assertIsChildOf(self, spanA, spanB): self.assertIsNotNone(spanA.parent) ctxA = spanA.parent - if isinstance(spanA.parent, trace_api.Span): + if not isinstance(ctxA, trace_api.SpanContext): ctxA = spanA.parent.context ctxB = spanB - if isinstance(ctxB, trace_api.Span): + if not isinstance(ctxB, trace_api.SpanContext): ctxB = spanB.context return self.assertEqual(ctxA.span_id, ctxB.span_id) @@ -33,11 +33,11 @@ def assertIsNotChildOf(self, spanA, spanB): return ctxA = spanA.parent - if isinstance(spanA.parent, trace_api.Span): + if not isinstance(ctxA, trace_api.SpanContext): ctxA = spanA.parent.context ctxB = spanB - if isinstance(ctxB, trace_api.Span): + if not isinstance(ctxB, trace_api.SpanContext): ctxB = spanB.context self.assertNotEqual(ctxA.span_id, ctxB.span_id) diff --git a/tox.ini b/tox.ini index f6e2e18aaa4..9f30b408310 100644 --- a/tox.ini +++ b/tox.ini @@ -171,6 +171,7 @@ commands = python scripts/eachdist.py lint --check-only [testenv:docs] +recreate = True deps = -c {toxinidir}/dev-requirements.txt -r {toxinidir}/docs-requirements.txt