Skip to content

Improve console span exporter #505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6 changes: 3 additions & 3 deletions docs/examples/basic_tracer/tests/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion docs/examples/http/tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
89 changes: 76 additions & 13 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions opentelemetry-sdk/tests/trace/export/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down