From 232bfdda0ccf4fc6ba6810edce87d5eb302b69d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 22 Apr 2020 21:58:08 -0500 Subject: [PATCH] sdk: Improve console span exporter (#505) The current version of the exporter prints everything in a single line, making it difficult to read. It's also missing events, links and attributes. This commit changes the console span exporter to use multiple lines and also adds the missing information about attributes, events and links. --- .../basic_tracer/tests/test_tracer.py | 6 +- docs/examples/http/tests/test_http.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 89 ++++++++++++++++--- .../sdk/trace/export/__init__.py | 2 +- .../tests/trace/export/test_export.py | 4 +- 5 files changed, 83 insertions(+), 20 deletions(-) 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)