From 06d171394141dfe7355ce2360430621a060ab6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 16 Mar 2020 07:01:36 -0500 Subject: [PATCH 1/8] Improve console span exporter 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. --- .../src/opentelemetry/sdk/trace/__init__.py | 128 ++++++++++++++++-- .../tests/trace/export/test_export.py | 2 +- 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0c98cc33af6..977e60778fa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -15,6 +15,7 @@ import atexit import logging +import os import random import threading from contextlib import contextmanager @@ -204,19 +205,120 @@ 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", - ) + def __str__(self, indent=0): + def format_context(context, indent=0): + text = [ + type(context).__name__ + "(", + "trace_id=" + trace_api.format_trace_id(context.trace_id), + "span_id=" + trace_api.format_span_id(context.span_id), + "trace_state=" + repr(context.trace_state), + ")", + ] + + # add indentation + for index in range(1, len(text) - 1): + text[index] = " " * (2 + indent) + text[index] + text[-1] = " " * indent + text[-1] + + return os.linesep.join(text) + + def format_attributes(attributes, indent=0): + if not attributes: + return "None" + text = [ + " " * (2 + indent) + k + "=" + str(v) + for (k, v) in attributes.items() + ] + return os.linesep + os.linesep.join(text) + + def format_event(event, indent=0): + text = [ + "Event(", + 'name="' + event.name + '"', + "timestamp=" + util.ns_to_iso_str(event.timestamp), + "attributes=" + + format_attributes(event.attributes, indent + 2), + ")", + ] + + for index in range(1, len(text) - 1): + text[index] = " " * (2 + indent) + text[index] + + text[0] = " " * indent + text[0] + text[-1] = " " * indent + text[-1] + + return os.linesep.join(text) + + def format_events(events, indent=0): + if not events: + return "None" + text = "" + for event in events: + text += os.linesep + format_event(event, indent) + return text + + def format_link(link, indent=0): + text = [ + "Link(", + "context=" + + format_context(link.context, 2 + indent + len("context=")), + "attributes=" + format_attributes(link.attributes, indent + 2), + ")", + ] + + # add indentation + for index in range(1, len(text) - 1): + text[index] = " " * (2 + indent) + text[index] + text[0] = " " * indent + text[0] + text[-1] = " " * indent + text[-1] + + return os.linesep.join(text) + + def format_links(links, indent=0): + if not links: + return "None" + text = "" + for link in links: + text += os.linesep + format_link(link, indent) + return text + + 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) + + text = [ + type(self).__name__ + "(", + 'name="' + self.name + '"', + "context=" + + format_context(self.context, 2 + indent + len("context=")), + "kind=" + str(self.kind), + "parent_id=" + parent_id, + "start_time=" + start_time, + "end_time=" + end_time, + "attributes=" + format_attributes(self.attributes, 2), + "events=" + format_events(self.events, indent + 4), + "links=" + format_links(self.links, indent + 4), + ")", + ] + + # add indentation + for index in range(1, len(text) - 1): + text[index] = " " * (2 + indent) + text[index] + text[-1] = " " * indent + text[-1] + + return os.linesep.join(text) def get_context(self): return self.context diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index cedb5967666..060d970827e 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -286,7 +286,7 @@ 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) From 828f0d5285d845021978ead63603dab0ca5c8953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 18 Mar 2020 20:33:03 -0500 Subject: [PATCH 2/8] format using json --- .../src/opentelemetry/sdk/trace/__init__.py | 145 ++++++------------ 1 file changed, 47 insertions(+), 98 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 977e60778fa..8a411242ab2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -14,10 +14,12 @@ 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, Optional, Sequence, Tuple, Type @@ -205,84 +207,39 @@ def __repr__(self): type(self).__name__, self.name, self.context ) - def __str__(self, indent=0): - def format_context(context, indent=0): - text = [ - type(context).__name__ + "(", - "trace_id=" + trace_api.format_trace_id(context.trace_id), - "span_id=" + trace_api.format_span_id(context.span_id), - "trace_state=" + repr(context.trace_state), - ")", - ] - - # add indentation - for index in range(1, len(text) - 1): - text[index] = " " * (2 + indent) + text[index] - text[-1] = " " * indent + text[-1] - - return os.linesep.join(text) - - def format_attributes(attributes, indent=0): - if not attributes: - return "None" - text = [ - " " * (2 + indent) + k + "=" + str(v) - for (k, v) in attributes.items() - ] - return os.linesep + os.linesep.join(text) - - def format_event(event, indent=0): - text = [ - "Event(", - 'name="' + event.name + '"', - "timestamp=" + util.ns_to_iso_str(event.timestamp), - "attributes=" - + format_attributes(event.attributes, indent + 2), - ")", - ] - - for index in range(1, len(text) - 1): - text[index] = " " * (2 + indent) + text[index] - - text[0] = " " * indent + text[0] - text[-1] = " " * indent + text[-1] - - return os.linesep.join(text) - - def format_events(events, indent=0): - if not events: - return "None" - text = "" + def __str__(self): + 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 + + def format_attributes(attributes): + if isinstance(attributes, BoundedDict): + return attributes._dict # pylint: disable=protected-access + return attributes + + def format_events(events): + f_events = [] for event in events: - text += os.linesep + format_event(event, indent) - return text - - def format_link(link, indent=0): - text = [ - "Link(", - "context=" - + format_context(link.context, 2 + indent + len("context=")), - "attributes=" + format_attributes(link.attributes, indent + 2), - ")", - ] - - # add indentation - for index in range(1, len(text) - 1): - text[index] = " " * (2 + indent) + text[index] - text[0] = " " * indent + text[0] - text[-1] = " " * indent + text[-1] - - return os.linesep.join(text) - - def format_links(links, indent=0): - if not links: - return "None" - text = "" + f_event = OrderedDict() + f_event["name"] = event.name + f_event["timestamp"] = util.ns_to_iso_str(event.timestamp) + f_event["attributes"] = format_attributes(event.attributes) + f_events.append(f_event) + return f_events + + def format_links(links): + f_links = [] for link in links: - text += os.linesep + format_link(link, indent) - return text + f_link = OrderedDict() + f_link["context"] = format_context(link.context) + f_link["attributes"] = format_attributes(link.attributes) + f_links.append(f_link) + return f_links - parent_id = "None" + parent_id = None if self.parent is not None: if isinstance(self.parent, Span): ctx = self.parent.context @@ -290,35 +247,27 @@ def format_links(links, indent=0): elif isinstance(self.parent, SpanContext): parent_id = trace_api.format_span_id(self.parent.span_id) - start_time = "None" + start_time = None if self.start_time: start_time = util.ns_to_iso_str(self.start_time) - end_time = "None" + end_time = None if self.end_time: end_time = util.ns_to_iso_str(self.end_time) - text = [ - type(self).__name__ + "(", - 'name="' + self.name + '"', - "context=" - + format_context(self.context, 2 + indent + len("context=")), - "kind=" + str(self.kind), - "parent_id=" + parent_id, - "start_time=" + start_time, - "end_time=" + end_time, - "attributes=" + format_attributes(self.attributes, 2), - "events=" + format_events(self.events, indent + 4), - "links=" + format_links(self.links, indent + 4), - ")", - ] - - # add indentation - for index in range(1, len(text) - 1): - text[index] = " " * (2 + indent) + text[index] - text[-1] = " " * indent + text[-1] - - return os.linesep.join(text) + f_span = OrderedDict() + + f_span["name"] = self.name + f_span["context"] = 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 + f_span["attributes"] = format_attributes(self.attributes) + f_span["events"] = format_events(self.events) + f_span["links"] = format_links(self.links) + + return json.dumps(f_span, indent=4) def get_context(self): return self.context From 82d1d88476f6745fa7419fd804d04a7dd2817767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Thu, 19 Mar 2020 07:15:16 -0500 Subject: [PATCH 3/8] fix tests --- docs/examples/basic_tracer/tests/test_tracer.py | 6 +++--- docs/examples/http/tests/test_http.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/basic_tracer/tests/test_tracer.py b/docs/examples/basic_tracer/tests/test_tracer.py index 4bd1a25c0ea..952b0a3029e 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 0ae81fe7de7..f552599df10 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): From daadaedf45058bd6cceb86906a07b59b22052118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 13 Apr 2020 07:55:20 -0500 Subject: [PATCH 4/8] Update console span exporter - move helpers methods to be part class instead of inlined functions - use to_json() instead of __str__() --- .../src/opentelemetry/sdk/trace/__init__.py | 74 ++++++++++--------- .../sdk/trace/export/__init__.py | 2 +- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 8a411242ab2..3536b8a88ba 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -207,38 +207,42 @@ def __repr__(self): type(self).__name__, self.name, self.context ) - def __str__(self): - 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 - - def format_attributes(attributes): - if isinstance(attributes, BoundedDict): - return attributes._dict # pylint: disable=protected-access - return attributes - - 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"] = format_attributes(event.attributes) - f_events.append(f_event) - return f_events - - def format_links(links): - f_links = [] - for link in links: - f_link = OrderedDict() - f_link["context"] = format_context(link.context) - f_link["attributes"] = format_attributes(link.attributes) - f_links.append(f_link) - return f_links + @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): @@ -258,14 +262,14 @@ def format_links(links): f_span = OrderedDict() f_span["name"] = self.name - f_span["context"] = format_context(self.context) + 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 - f_span["attributes"] = format_attributes(self.attributes) - f_span["events"] = format_events(self.events) - f_span["links"] = format_links(self.links) + 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) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index e5d96eff9e9..e9ba927f687 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -271,7 +271,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 From 00b21dc60a42cfe5c190536128663ab55168bd6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 17 Apr 2020 11:52:37 -0500 Subject: [PATCH 5/8] test-http-example: look for span name too --- docs/examples/http/tests/test_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/http/tests/test_http.py b/docs/examples/http/tests/test_http.py index 5d2165f27cf..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): From 6fa466e919ca1af99b565dcdfe759f02f09c5fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Fri, 17 Apr 2020 17:05:42 -0500 Subject: [PATCH 6/8] fix sdk tests --- opentelemetry-sdk/tests/trace/export/test_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index d629213a18d..43b7893951f 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -289,7 +289,7 @@ def test_export(self): # pylint: disable=no-self-use 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) From 1c95153df015b3fe4248e9abefe0002f3fc7b0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Mon, 20 Apr 2020 07:54:50 -0500 Subject: [PATCH 7/8] Add span status --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 9396a42db2c..1ad8b154023 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -331,6 +331,11 @@ def to_json(self): if self.end_time: end_time = util.ns_to_iso_str(self.end_time) + 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 @@ -339,6 +344,7 @@ def to_json(self): f_span["parent_id"] = parent_id f_span["start_time"] = start_time f_span["end_time"] = end_time + 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) From 7a83567bec71e9af2012aac551abfb818c73777a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 22 Apr 2020 16:07:44 -0500 Subject: [PATCH 8/8] fix span status in to_json --- .../src/opentelemetry/sdk/trace/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 1ad8b154023..3d2fc96c1b8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -331,10 +331,11 @@ def to_json(self): if self.end_time: end_time = util.ns_to_iso_str(self.end_time) - status = OrderedDict() - status["canonical_code"] = str(self.status.canonical_code.name) - if self.status.description: - status["description"] = self.status.description + 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() @@ -344,7 +345,8 @@ def to_json(self): f_span["parent_id"] = parent_id f_span["start_time"] = start_time f_span["end_time"] = end_time - f_span["status"] = status + 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)