diff --git a/docs/examples/basic_tracer/tests/test_tracer.py b/docs/examples/basic_tracer/tests/test_tracer.py index 8f73c2bbb06..77b25be5f0b 100644 --- a/docs/examples/basic_tracer/tests/test_tracer.py +++ b/docs/examples/basic_tracer/tests/test_tracer.py @@ -25,6 +25,6 @@ def test_basic_tracer(self): (sys.executable, test_script) ).decode() - self.assertIn('name="foo"', output) - self.assertIn('name="bar"', output) - self.assertIn('name="baz"', output) + self.assertIn('"name": "foo"', output) + self.assertIn('"name": "bar"', output) + self.assertIn('"name": "baz"', output) diff --git a/docs/examples/http/tests/test_http.py b/docs/examples/http/tests/test_http.py index fe2a38cec0d..6749f9b7997 100644 --- a/docs/examples/http/tests/test_http.py +++ b/docs/examples/http/tests/test_http.py @@ -32,7 +32,7 @@ def test_http(self): output = subprocess.check_output( (sys.executable, test_script) ).decode() - self.assertIn('name="/"', output) + self.assertIn('"name": "/"', output) @classmethod def teardown_class(cls): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 3b64006d3d5..3d2fc96c1b8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -15,9 +15,12 @@ import abc import atexit +import json import logging +import os import random import threading +from collections import OrderedDict from contextlib import contextmanager from types import TracebackType from typing import Iterator, MutableSequence, Optional, Sequence, Tuple, Type @@ -276,19 +279,79 @@ def __repr__(self): type(self).__name__, self.name, self.context ) - def __str__(self): - return ( - '{}(name="{}", context={}, kind={}, ' - "parent={}, start_time={}, end_time={})" - ).format( - type(self).__name__, - self.name, - self.context, - self.kind, - repr(self.parent), - util.ns_to_iso_str(self.start_time) if self.start_time else "None", - util.ns_to_iso_str(self.end_time) if self.end_time else "None", - ) + @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 + 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): + 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["canonical_code"] = str(self.status.canonical_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) + + return json.dumps(f_span, indent=4) def get_context(self): return self.context diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 515962ca365..fbe30720ea7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -270,7 +270,7 @@ class ConsoleSpanExporter(SpanExporter): def __init__( self, out: typing.IO = sys.stdout, - formatter: typing.Callable[[Span], str] = lambda span: str(span) + formatter: typing.Callable[[Span], str] = lambda span: span.to_json() + os.linesep, ): self.out = out diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 7a63963648c..43b7893951f 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -286,10 +286,10 @@ def test_export(self): # pylint: disable=no-self-use # Mocking stdout interferes with debugging and test reporting, mock on # the exporter instance instead. - span = trace.Span("span name", mock.Mock()) + span = trace.Span("span name", trace_api.INVALID_SPAN_CONTEXT) with mock.patch.object(exporter, "out") as mock_stdout: exporter.export([span]) - mock_stdout.write.assert_called_once_with(str(span) + os.linesep) + mock_stdout.write.assert_called_once_with(span.to_json() + os.linesep) self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.flush.call_count, 1)