Repository: census-instrumentation/opencensus-python (python)

Issue:
Pass raw spans to exporters
Currently, span objects are converted to an intermediate json format before being sent to an exporter. As a result, there are some stackdriver specific conversions [being applied](https://github.com/census-instrumentation/opencensus-python/blob/master/opencensus/trace/span.py#L222) to all spans (such as string truncation) that may not necessarily apply to all export formats.

I am proposing to update the [base.Exporter](https://github.com/census-instrumentation/opencensus-python/blob/master/opencensus/trace/exporters/base.py#L18) emit and export methods to take a raw [span.Span](https://github.com/census-instrumentation/opencensus-python/blob/master/opencensus/trace/span.py#L28) object instead of a dict. Not only will this push stackdriver specific details to the stackdriver exporter, but it would also make the development of new exporters easier since there are less layers of conversion.

Happy to make a PR if everyone is ok with the change.

Pull request description:
Refactor exporters to accept SpanData objects instead of Spans
Fixes #127 

Patch:
diff --git a/opencensus/trace/exporters/base.py b/opencensus/trace/exporters/base.py
index 99f0c47..8011e86 100644
--- a/opencensus/trace/exporters/base.py
+++ b/opencensus/trace/exporters/base.py
@@ -20,13 +20,24 @@ class Exporter(object):
 
     Subclasses of :class:`Exporter` must override :meth:`export`.
     """
-    def emit(self, trace):
-        """Emit the trace."""
+
+    def emit(self, span_datas):
+        """
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to emit
+        """
         raise NotImplementedError
 
-    def export(self, trace):
+    def export(self, span_datas):
         """Export the trace. Send trace to transport, and transport will call
         exporter.emit() to actually send the trace to the specified tracing
         backend.
+
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to export
         """
         raise NotImplementedError
diff --git a/opencensus/trace/exporters/file_exporter.py b/opencensus/trace/exporters/file_exporter.py
index 37bfb3e..f250d1e 100644
--- a/opencensus/trace/exporters/file_exporter.py
+++ b/opencensus/trace/exporters/file_exporter.py
@@ -16,6 +16,7 @@
 
 import json
 
+from opencensus.trace import span_data
 from opencensus.trace.exporters import base
 from opencensus.trace.exporters.transports import sync
 
@@ -47,14 +48,25 @@ class FileExporter(base.Exporter):
         self.transport = transport(self)
         self.file_mode = file_mode
 
-    def emit(self, trace):
+    def emit(self, span_datas):
         """
-        :type trace: dict
-        :param trace: Trace collected.
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to emit
         """
         with open(self.file_name, self.file_mode) as file:
-            trace_str = json.dumps(trace)
+            # convert to the legacy trace json for easier refactoring
+            # TODO: refactor this to use the span data directly
+            legacy_trace_json = span_data.format_legacy_trace_json(span_datas)
+            trace_str = json.dumps(legacy_trace_json)
             file.write(trace_str)
 
-    def export(self, trace):
-        self.transport.export(trace)
+    def export(self, span_datas):
+        """
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to export
+        """
+        self.transport.export(span_datas)
diff --git a/opencensus/trace/exporters/logging_exporter.py b/opencensus/trace/exporters/logging_exporter.py
index 33d334b..83002d2 100644
--- a/opencensus/trace/exporters/logging_exporter.py
+++ b/opencensus/trace/exporters/logging_exporter.py
@@ -16,6 +16,7 @@
 
 import logging
 
+from opencensus.trace import span_data
 from opencensus.trace.exporters import base
 from opencensus.trace.exporters.transports import sync
 
@@ -63,12 +64,23 @@ class LoggingExporter(base.Exporter):
         self.logger.setLevel(logging.INFO)
         self.transport = transport(self)
 
-    def emit(self, trace):
+    def emit(self, span_datas):
         """
-        :type traces: dict
-        :param traces: Trace collected.
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to emit
         """
-        self.logger.info(trace)
+        # convert to the legacy trace json for easier refactoring
+        # TODO: refactor this to use the span data directly
+        legacy_trace_json = span_data.format_legacy_trace_json(span_datas)
+        self.logger.info(legacy_trace_json)
 
-    def export(self, trace):
-        self.transport.export(trace)
+    def export(self, span_datas):
+        """
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to export
+        """
+        self.transport.export(span_datas)
diff --git a/opencensus/trace/exporters/print_exporter.py b/opencensus/trace/exporters/print_exporter.py
index 33b298b..72a30bc 100644
--- a/opencensus/trace/exporters/print_exporter.py
+++ b/opencensus/trace/exporters/print_exporter.py
@@ -28,20 +28,24 @@ class PrintExporter(base.Exporter):
                       :class:`.SyncTransport`. The other option is
                       :class:`.BackgroundThreadTransport`.
     """
+
     def __init__(self, transport=sync.SyncTransport):
         self.transport = transport(self)
 
-    def emit(self, trace):
-        """export the traces by printing it out.
-
-        :type trace: dict
-        :param trace: Trace collected.
-
-        :rtype: dict
-        :returns: Trace printed.
+    def emit(self, span_datas):
         """
-        print(trace)
-        return trace
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to emit
+        """
+        print(span_datas)
 
-    def export(self, trace):
-        self.transport.export(trace)
+    def export(self, span_datas):
+        """
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to export
+        """
+        self.transport.export(span_datas)
diff --git a/opencensus/trace/exporters/stackdriver_exporter.py b/opencensus/trace/exporters/stackdriver_exporter.py
index 42a796c..d53c2cd 100644
--- a/opencensus/trace/exporters/stackdriver_exporter.py
+++ b/opencensus/trace/exporters/stackdriver_exporter.py
@@ -14,14 +14,14 @@
 
 import os
 
-from opencensus.trace.attributes import Attributes
+from google.cloud.trace.client import Client
+
 from opencensus.trace import attributes_helper
+from opencensus.trace import span_data
+from opencensus.trace.attributes import Attributes
 from opencensus.trace.exporters import base
 from opencensus.trace.exporters.transports import sync
 
-from google.cloud.trace.client import Client
-
-
 # OpenCensus Version
 VERSION = '0.1.3'
 
@@ -132,33 +132,46 @@ class StackdriverExporter(base.Exporter):
         self.project_id = client.project
         self.transport = transport(self)
 
-    def emit(self, spans):
+    def emit(self, span_datas):
         """
-        :type spans: dict
-        :param spans: Spans collected.
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to emit
         """
         name = 'projects/{}'.format(self.project_id)
-        stackdriver_spans = self.translate_to_stackdriver(spans)
+
+        # convert to the legacy trace json for easier refactoring
+        # TODO: refactor this to use the span data directly
+        trace = span_data.format_legacy_trace_json(span_datas)
+
+        stackdriver_spans = self.translate_to_stackdriver(trace)
         self.client.batch_write_spans(name, stackdriver_spans)
 
-    def export(self, trace):
-        self.transport.export(trace)
+    def export(self, span_datas):
+        """
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to export
+        """
+        self.transport.export(span_datas)
 
-    def translate_to_stackdriver(self, spans):
+    def translate_to_stackdriver(self, trace):
         """Translate the spans json to Stackdriver format.
 
         See: https://cloud.google.com/trace/docs/reference/v2/rest/v2/
              projects.traces/batchWrite
 
-        :type spans: dict
-        :param spans: Spans collected.
+        :type trace: dict
+        :param trace: Trace dictionary
 
         :rtype: dict
         :returns: Spans in Google Cloud StackDriver Trace format.
         """
-        set_attributes(spans)
-        spans_json = spans.get('spans')
-        trace_id = spans.get('traceId')
+        set_attributes(trace)
+        spans_json = trace.get('spans')
+        trace_id = trace.get('traceId')
         spans_list = []
 
         for span in spans_json:
diff --git a/opencensus/trace/exporters/transports/background_thread.py b/opencensus/trace/exporters/transports/background_thread.py
index 4700687..2a16511 100644
--- a/opencensus/trace/exporters/transports/background_thread.py
+++ b/opencensus/trace/exporters/transports/background_thread.py
@@ -81,8 +81,8 @@ class _Worker(object):
     def _thread_main(self):
         """The entry point for the worker thread.
 
-        Pulls pending spans off the queue and writes them in batches to
-        the specified tracing backend using the exporter.
+        Pulls pending SpanData tuples off the queue and writes them in
+        batches to the specified tracing backend using the exporter.
         """
         print('Background thread started.')
 
@@ -90,8 +90,7 @@ class _Worker(object):
 
         while True:
             items = self._get_items()
-            trace_id = None
-            spans = []
+            span_datas = []
 
             for item in items:
                 if item is _WORKER_TERMINATOR:
@@ -99,16 +98,10 @@ class _Worker(object):
                     # Continue processing items, don't break, try to process
                     # all items we got back before quitting.
                 else:
-                    trace_id = item.get('traceId')
-                    spans.extend(item.get('spans'))
+                    span_datas.extend(item)
 
-            if spans and trace_id:
-                spans_json = {
-                    'traceId': trace_id,
-                    'spans': spans,
-                }
-
-                self.exporter.emit(spans_json)
+            if span_datas:
+                self.exporter.emit(span_datas)
 
             for _ in range(len(items)):
                 self._queue.task_done()
@@ -181,9 +174,9 @@ class _Worker(object):
         else:
             print('Failed to send pending spans.')
 
-    def enqueue(self, trace):
-        """Queues a trace to be written by the background thread."""
-        self._queue.put_nowait(trace)
+    def enqueue(self, span_datas):
+        """Queues span_datas to be written by the background thread."""
+        self._queue.put_nowait(span_datas)
 
     def flush(self):
         """Submit any pending spans."""
@@ -214,9 +207,9 @@ class BackgroundThreadTransport(base.Transport):
         self.worker = _Worker(exporter, grace_period, max_batch_size)
         self.worker.start()
 
-    def export(self, trace):
+    def export(self, span_datas):
         """Put the trace to be exported into queue."""
-        self.worker.enqueue(trace)
+        self.worker.enqueue(span_datas)
 
     def flush(self):
         """Submit any pending traces."""
diff --git a/opencensus/trace/exporters/transports/base.py b/opencensus/trace/exporters/transports/base.py
index 6d992bd..693ac98 100644
--- a/opencensus/trace/exporters/transports/base.py
+++ b/opencensus/trace/exporters/transports/base.py
@@ -20,8 +20,8 @@ class Transport(object):
 
     Subclasses of :class:`Transport` must override :meth:`export`.
     """
-    def export(self, trace):
-        """Export the trace by calling exporter.emit()."""
+    def export(self, span_datas):
+        """Export the SpanData tuples by calling exporter.emit()."""
         raise NotImplementedError
 
     def flush(self):
diff --git a/opencensus/trace/exporters/transports/sync.py b/opencensus/trace/exporters/transports/sync.py
index cb625e6..7a043a3 100644
--- a/opencensus/trace/exporters/transports/sync.py
+++ b/opencensus/trace/exporters/transports/sync.py
@@ -19,5 +19,5 @@ class SyncTransport(base.Transport):
     def __init__(self, exporter):
         self.exporter = exporter
 
-    def export(self, trace):
-        self.exporter.emit(trace)
+    def export(self, span_datas):
+        self.exporter.emit(span_datas)
diff --git a/opencensus/trace/exporters/zipkin_exporter.py b/opencensus/trace/exporters/zipkin_exporter.py
index 94e3a00..7b151d5 100644
--- a/opencensus/trace/exporters/zipkin_exporter.py
+++ b/opencensus/trace/exporters/zipkin_exporter.py
@@ -14,12 +14,14 @@
 
 """Export the spans data to Zipkin Collector."""
 
+import calendar
 import datetime
 import json
 import logging
+
 import requests
-import calendar
 
+from opencensus.trace import span_data
 from opencensus.trace.exporters import base
 from opencensus.trace.exporters.transports import sync
 
@@ -90,12 +92,18 @@ class ZipkinExporter(base.Exporter):
             self.port,
             self.endpoint)
 
-    def emit(self, trace):
-        """Send trace to Zipkin server, default using the v2 API.
+    def emit(self, span_datas):
+        """Send SpanData tuples to Zipkin server, default using the v2 API.
 
-        :type trace: dict
-        :param trace: Trace data in dictionary format.
+        :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+        :param list of opencensus.trace.span_data.SpanData span_datas:
+            SpanData tuples to emit
         """
+        # convert to the legacy trace json for easier refactoring
+        # TODO: refactor this to use the span data directly
+        trace = span_data.format_legacy_trace_json(span_datas)
+
         trace_id = trace.get('traceId')
         spans = trace.get('spans')
 
@@ -113,8 +121,8 @@ class ZipkinExporter(base.Exporter):
         except Exception as e:  # pragma: NO COVER
             logging.error(getattr(e, 'message', e))
 
-    def export(self, trace):
-        self.transport.export(trace)
+    def export(self, span_datas):
+        self.transport.export(span_datas)
 
     def translate_to_zipkin(self, trace_id, spans):
         """Translate the opencensus spans to zipkin spans.
diff --git a/opencensus/trace/span.py b/opencensus/trace/span.py
index b0acebe..7995dfd 100644
--- a/opencensus/trace/span.py
+++ b/opencensus/trace/span.py
@@ -25,6 +25,12 @@ from opencensus.trace.tracers import base
 from opencensus.trace.utils import _get_truncatable_str
 
 
+class SpanKind(object):
+    UNSPECIFIED = 0
+    SERVER = 1
+    CLIENT = 2
+
+
 class Span(object):
     """A span is an individual timed event which forms a node of the trace
     tree. Each span has its name, span id and parent id. The parent id
@@ -85,6 +91,11 @@ class Span(object):
                            method in the tracer class to finish a span. If no
                            tracer is passed in, then just finish the span using
                            the finish method in the Span class.
+
+    :type span_kind: int
+    :param span_kind: (Optional) Highly recommended flag that denotes the type
+                        of span (valid values defined by :class:
+                        `opencensus.trace.span.SpanKind`)
     """
 
     def __init__(
@@ -100,7 +111,8 @@ class Span(object):
             links=None,
             status=None,
             same_process_as_parent_span=None,
-            context_tracer=None):
+            context_tracer=None,
+            span_kind=SpanKind.UNSPECIFIED):
         self.name = name
         self.parent_span = parent_span
         self.start_time = start_time
@@ -132,6 +144,7 @@ class Span(object):
         self.same_process_as_parent_span = same_process_as_parent_span
         self._child_spans = []
         self.context_tracer = context_tracer
+        self.span_kind = span_kind
 
     @property
     def children(self):
diff --git a/opencensus/trace/span_data.py b/opencensus/trace/span_data.py
index ee44676..3ff217f 100644
--- a/opencensus/trace/span_data.py
+++ b/opencensus/trace/span_data.py
@@ -13,21 +13,26 @@
 # limitations under the License.
 
 import collections
+from opencensus.trace import utils
+from opencensus.trace import attributes
 
 _SpanData = collections.namedtuple(
     '_SpanData',
     (
         'name',
+        'context',
         'span_id',
         'parent_span_id',
         'attributes',
         'start_time',
         'end_time',
+        'child_span_count',
         'stack_trace',
         'time_events',
         'links',
         'status',
         'same_process_as_parent_span',
+        'span_kind',
     ),
 )
 
@@ -39,6 +44,9 @@ class SpanData(_SpanData):
     :type name: str
     :param name: The name of the span.
 
+    :type: context: :class: `~opencensus.trace.span_context.SpanContext`
+    :param context: The SpanContext of the Span
+
     :type span_id: int
     :param span_id: Identifier for the span, unique within a trace.
 
@@ -81,6 +89,81 @@ class SpanData(_SpanData):
                                         trace crosses a process boundary.
                                         True when the parent_span belongs to
                                         the same process as the current span.
+    :type span_kind: int
+    :param span_kind: (Optional) Highly recommended flag that denotes the type
+                        of span (valid values defined by :class:
+                        `opencensus.trace.span.SpanKind`)
 
     """
     __slots__ = ()
+
+
+def _format_legacy_span_json(span_data):
+    """
+    :param SpanData span_data: SpanData object to convert
+    :rtype: dict
+    :return: Dictionary representing the Span
+    """
+    span_json = {
+        'displayName': utils._get_truncatable_str(span_data.name),
+        'spanId': span_data.span_id,
+        'startTime': span_data.start_time,
+        'endTime': span_data.end_time,
+        'childSpanCount': span_data.child_span_count,
+        'kind': span_data.span_kind
+    }
+
+    if span_data.parent_span_id is not None:
+        span_json['parentSpanId'] = span_data.parent_span_id
+
+    if span_data.attributes:
+        span_json['attributes'] = attributes.Attributes(
+            span_data.attributes).format_attributes_json()
+
+    if span_data.stack_trace is not None:
+        span_json['stackTrace'] = \
+            span_data.stack_trace.format_stack_trace_json()
+
+    if span_data.time_events:
+        span_json['timeEvents'] = {
+            'timeEvent': [time_event.format_time_event_json()
+                          for time_event in span_data.time_events]
+        }
+
+    if span_data.links:
+        span_json['links'] = {
+            'link': [
+                link.format_link_json() for link in span_data.links]
+        }
+
+    if span_data.status is not None:
+        span_json['status'] = span_data.status.format_status_json()
+
+    if span_data.same_process_as_parent_span is not None:
+        span_json['sameProcessAsParentSpan'] = \
+            span_data.same_process_as_parent_span
+
+    return span_json
+
+
+def format_legacy_trace_json(span_datas):
+    """Formats a list of SpanData tuples into the legacy 'trace' dictionary
+    format for backwards compatibility
+    :type span_datas: list of :class:
+            `~opencensus.trace.span_data.SpanData`
+    :param list of opencensus.trace.span_data.SpanData span_datas:
+        SpanData tuples to emit
+    :rtype: dict
+    :return: Legacy 'trace' dictionary representing given SpanData tuples
+    """
+    if not span_datas:
+        return {}
+    top_span = span_datas[0]
+    assert isinstance(top_span, SpanData)
+    trace_id = top_span.context.trace_id if top_span.context is not None \
+        else None
+    assert trace_id is not None
+    return {
+        'traceId': trace_id,
+        'spans': [_format_legacy_span_json(sd) for sd in span_datas],
+    }
diff --git a/opencensus/trace/tracers/context_tracer.py b/opencensus/trace/tracers/context_tracer.py
index 33e0e6a..6d245ff 100644
--- a/opencensus/trace/tracers/context_tracer.py
+++ b/opencensus/trace/tracers/context_tracer.py
@@ -17,6 +17,7 @@ import logging
 from opencensus.trace import execution_context
 from opencensus.trace.span_context import SpanContext
 from opencensus.trace import span as trace_span
+from opencensus.trace import span_data as span_data_module
 from opencensus.trace.exporters import print_exporter
 from opencensus.trace.tracers import base
 
@@ -28,6 +29,7 @@ class ContextTracer(base.Tracer):
     :param span_context: SpanContext encapsulates the current context within
                          the request's trace.
     """
+
     def __init__(self, exporter=None, span_context=None):
         if exporter is None:
             exporter = print_exporter.PrintExporter()
@@ -108,8 +110,8 @@ class ContextTracer(base.Tracer):
         else:
             execution_context.set_current_span(None)
 
-        span_json = self.get_trace_json(cur_span)
-        self.exporter.export(span_json)
+        span_datas = self.get_span_datas(cur_span)
+        self.exporter.export(span_datas)
 
         return cur_span
 
@@ -134,15 +136,32 @@ class ContextTracer(base.Tracer):
         current_span = self.current_span()
         current_span.add_attribute(attribute_key, attribute_value)
 
-    def get_trace_json(self, span):
-        """Get the JSON format trace."""
-        span_tree = list(iter(span))
-        span_tree_list = [trace_span.format_span_json(span)
-                          for span in span_tree]
-
-        trace = {
-            'traceId': self.trace_id,
-            'spans': span_tree_list,
-        }
+    def get_span_datas(self, span):
+        """Extracts a list of SpanData tuples from a span
 
-        return trace
+        :rtype: list of opencensus.trace.span_data.SpanData
+        :return list of SpanData tuples
+        """
+        span_tree = list(iter(span))
+        span_datas = [
+            span_data_module.SpanData(
+                name=span.name,
+                context=self.span_context,
+                span_id=span.span_id,
+                parent_span_id=span.parent_span.span_id,
+                attributes=span.attributes,
+                start_time=span.start_time,
+                end_time=span.end_time,
+                child_span_count=len(span.children),
+                stack_trace=span.stack_trace,
+                time_events=span.time_events,
+                links=span.links,
+                status=span.status,
+                same_process_as_parent_span=span.same_process_as_parent_span,
+                span_kind=span.span_kind
+
+            )
+            for span in span_tree
+        ]
+
+        return span_datas


Test patch:
diff --git a/tests/unit/trace/exporters/test_base_exporter.py b/tests/unit/trace/exporters/test_base_exporter.py
index cc6dc16..2c2eabd 100644
--- a/tests/unit/trace/exporters/test_base_exporter.py
+++ b/tests/unit/trace/exporters/test_base_exporter.py
@@ -14,6 +14,7 @@
 
 import unittest
 
+
 class TestBaseExporter(unittest.TestCase):
 
     def test_export_abstract(self):
diff --git a/tests/unit/trace/exporters/test_logging_exporter.py b/tests/unit/trace/exporters/test_logging_exporter.py
index 7828cdc..6cac8c0 100644
--- a/tests/unit/trace/exporters/test_logging_exporter.py
+++ b/tests/unit/trace/exporters/test_logging_exporter.py
@@ -12,10 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import mock
 import logging
 import unittest
 
+import mock
+
+from opencensus.trace import span_context
+from opencensus.trace import span_data as span_data_module
 from opencensus.trace.exporters import logging_exporter
 
 
@@ -38,10 +41,29 @@ class TestLoggingExporter(unittest.TestCase):
         logger = mock.Mock()
         exporter.logger = logger
 
-        traces = '{traces: test}'
-        exporter.emit(traces)
-
-        logger.info.assert_called_once_with(traces)
+        span_datas = [
+            span_data_module.SpanData(
+                name='span',
+                context=span_context.SpanContext(trace_id='1'),
+                span_id='1111',
+                parent_span_id=None,
+                attributes=None,
+                start_time=None,
+                end_time=None,
+                child_span_count=None,
+                stack_trace=None,
+                time_events=None,
+                links=None,
+                status=None,
+                same_process_as_parent_span=None,
+                span_kind=0,
+            )
+        ]
+        exporter.emit(span_datas)
+
+        logger.info.assert_called_once_with(
+            span_data_module.format_legacy_trace_json(span_datas)
+        )
 
     def test_export(self):
         exporter = logging_exporter.LoggingExporter(transport=MockTransport)
diff --git a/tests/unit/trace/exporters/test_print_exporter.py b/tests/unit/trace/exporters/test_print_exporter.py
index 0251b6c..9db7bbd 100644
--- a/tests/unit/trace/exporters/test_print_exporter.py
+++ b/tests/unit/trace/exporters/test_print_exporter.py
@@ -30,8 +30,7 @@ class TestPrintExporter(unittest.TestCase):
         traces = {}
         exporter = self._make_one()
 
-        printed = exporter.emit(traces)
-        self.assertEqual(printed, traces)
+        exporter.emit(traces)
 
     def test_export(self):
         exporter = self._make_one(transport=MockTransport)
diff --git a/tests/unit/trace/exporters/test_stackdriver_exporter.py b/tests/unit/trace/exporters/test_stackdriver_exporter.py
index 972d9c9..8cc9896 100644
--- a/tests/unit/trace/exporters/test_stackdriver_exporter.py
+++ b/tests/unit/trace/exporters/test_stackdriver_exporter.py
@@ -16,6 +16,8 @@ import unittest
 
 import mock
 
+from opencensus.trace import span_context
+from opencensus.trace import span_data as span_data_module
 from opencensus.trace.exporters import stackdriver_exporter
 
 
@@ -67,17 +69,25 @@ class TestStackdriverExporter(unittest.TestCase):
         self.assertTrue(exporter.transport.export_called)
 
     def test_emit(self):
-        spans = {'spans':
-            [
-                {
-                    'displayName': {
-                        'value': 'span',
-                        'truncated_byte_count': 0
-                    },
-                    'spanId': '1111',
-                }
-            ]
-        }
+        trace_id = '6e0c63257de34c92bf9efcd03927272e'
+        span_datas = [
+            span_data_module.SpanData(
+                name='span',
+                context=span_context.SpanContext(trace_id=trace_id),
+                span_id='1111',
+                parent_span_id=None,
+                attributes=None,
+                start_time=None,
+                end_time=None,
+                child_span_count=None,
+                stack_trace=None,
+                time_events=None,
+                links=None,
+                status=None,
+                same_process_as_parent_span=None,
+                span_kind=0,
+            )
+        ]
 
         stackdriver_spans = {
             'spans': [
@@ -92,7 +102,9 @@ class TestStackdriverExporter(unittest.TestCase):
                             'g.co/agent': {
                                 'string_value': {
                                     'truncated_byte_count': 0,
-                                    'value': 'opencensus-python [{}]'.format(stackdriver_exporter.VERSION)
+                                    'value': 'opencensus-python [{}]'.format(
+                                        stackdriver_exporter.VERSION
+                                    )
                                 }
                             }
                         }
@@ -103,7 +115,9 @@ class TestStackdriverExporter(unittest.TestCase):
                             'truncated_byte_count': 0,
                             'value': 'span'
                         },
-                    'name': 'projects/PROJECT/traces/None/spans/1111',
+                    'name': 'projects/PROJECT/traces/{}/spans/1111'.format(
+                        trace_id
+                    ),
                     'timeEvents': None,
                     'endTime': None,
                     'sameProcessAsParentSpan': None
@@ -119,7 +133,7 @@ class TestStackdriverExporter(unittest.TestCase):
             client=client,
             project_id=project_id)
 
-        exporter.emit(spans)
+        exporter.emit(span_datas)
 
         name = 'projects/{}'.format(project_id)
 
@@ -185,7 +199,9 @@ class TestStackdriverExporter(unittest.TestCase):
                             'g.co/agent': {
                                 'string_value': {
                                     'truncated_byte_count': 0,
-                                    'value': 'opencensus-python [{}]'.format(stackdriver_exporter.VERSION)
+                                    'value': 'opencensus-python [{}]'.format(
+                                        stackdriver_exporter.VERSION
+                                    )
                                 }
                             },
                             'key': {
@@ -248,7 +264,9 @@ class Test_set_attributes_gae(unittest.TestCase):
                     'g.co/agent': {
                         'string_value': {
                             'truncated_byte_count': 0,
-                            'value': 'opencensus-python [{}]'.format(stackdriver_exporter.VERSION)
+                            'value': 'opencensus-python [{}]'.format(
+                                stackdriver_exporter.VERSION
+                            )
                         }
                     },
                 }
diff --git a/tests/unit/trace/exporters/test_zipkin_exporter.py b/tests/unit/trace/exporters/test_zipkin_exporter.py
index 4836226..69035c9 100644
--- a/tests/unit/trace/exporters/test_zipkin_exporter.py
+++ b/tests/unit/trace/exporters/test_zipkin_exporter.py
@@ -16,12 +16,12 @@ import unittest
 
 import mock
 
-
+from opencensus.trace import span_context
+from opencensus.trace import span_data as span_data_module
 from opencensus.trace.exporters import zipkin_exporter
 
 
 class TestZipkinExporter(unittest.TestCase):
-
     def test_constructor(self):
         service_name = 'my_service'
         host_name = '0.0.0.0'
@@ -54,7 +54,7 @@ class TestZipkinExporter(unittest.TestCase):
 
     @mock.patch('requests.post')
     @mock.patch.object(zipkin_exporter.ZipkinExporter,
-                'translate_to_zipkin')
+                       'translate_to_zipkin')
     def test_emit_succeeded(self, translate_mock, requests_mock):
         import json
 
@@ -65,7 +65,7 @@ class TestZipkinExporter(unittest.TestCase):
         response.status_code = 202
         requests_mock.return_value = response
         translate_mock.return_value = trace
-        exporter.emit(trace)
+        exporter.emit([])
 
         requests_mock.assert_called_once_with(
             url=exporter.url,
@@ -74,7 +74,7 @@ class TestZipkinExporter(unittest.TestCase):
 
     @mock.patch('requests.post')
     @mock.patch.object(zipkin_exporter.ZipkinExporter,
-                'translate_to_zipkin')
+                       'translate_to_zipkin')
     def test_emit_failed(self, translate_mock, requests_mock):
         import json
 
@@ -85,7 +85,7 @@ class TestZipkinExporter(unittest.TestCase):
         response.status_code = 400
         requests_mock.return_value = response
         translate_mock.return_value = trace
-        exporter.emit(trace)
+        exporter.emit([])
 
         requests_mock.assert_called_once_with(
             url=exporter.url,
@@ -93,62 +93,66 @@ class TestZipkinExporter(unittest.TestCase):
             headers=zipkin_exporter.ZIPKIN_HEADERS)
 
     def test_translate_to_zipkin_span_kind_none(self):
-        span1 = {
-            'displayName': {'value': 'child_span'},
-            'parentSpanId': 1111111111,
-            'spanId': 1234567890,
-            'startTime': '2017-08-15T18:02:26.071158Z',
-            'endTime': '2017-08-15T18:02:36.071158Z',
-            'attributes': {
-                'attributeMap': {
-                    'test_key': {
-                        'string_value': {
-                            'value': 'test_value'
-                        }
-                    }
-                }
-            },
-        }
-
-        span2 = {
-            'displayName': {'value': 'child_span'},
-            'kind': 0,
-            'parentSpanId': 1111111111,
-            'spanId': 1234567890,
-            'startTime': '2017-08-15T18:02:26.071158Z',
-            'endTime': '2017-08-15T18:02:36.071158Z',
-            'attributes': {
-                'attributeMap': {
-                    'test_key':  {
-                        'int_value': 1
-                    }
-                }
-            },
-        }
-
-        span3 = {
-            'displayName': {'value': 'child_span'},
-            'kind': 1,
-            'spanId': 1234567890,
-            'startTime': '2017-08-15T18:02:26.071158Z',
-            'endTime': '2017-08-15T18:02:36.071158Z',
-            'attributes': {
-                'attributeMap': {
-                    'test_key':  {
-                        'bool_value': False
-                    },
-                    # these tags are malformed and should be omitted
-                    'test_key2': 'raw_value',
-                    'test_key3': {
-                        'float_value': 0.1
-                    },
-                }
-            },
-        }
+        trace_id = '6e0c63257de34c92bf9efcd03927272e'
+        spans_ipv4 = [
+            span_data_module.SpanData(
+                name='child_span',
+                context=span_context.SpanContext(trace_id=trace_id),
+                span_id=1234567890,
+                parent_span_id=1111111111,
+                attributes={'test_key': 'test_value'},
+                start_time='2017-08-15T18:02:26.071158Z',
+                end_time='2017-08-15T18:02:36.071158Z',
+                child_span_count=None,
+                stack_trace=None,
+                time_events=None,
+                links=None,
+                status=None,
+                same_process_as_parent_span=None,
+                span_kind=0,
+            ),
+            span_data_module.SpanData(
+                name='child_span',
+                context=span_context.SpanContext(trace_id=trace_id),
+                span_id=1234567890,
+                parent_span_id=1111111111,
+                attributes={'test_key': 1},
+                start_time='2017-08-15T18:02:26.071158Z',
+                end_time='2017-08-15T18:02:36.071158Z',
+                child_span_count=None,
+                stack_trace=None,
+                time_events=None,
+                links=None,
+                status=None,
+                same_process_as_parent_span=None,
+                span_kind=None,
+            ),
+        ]
 
         trace_id = '6e0c63257de34c92bf9efcd03927272e'
-        spans_ipv4 = [span1, span2]
-        spans_ipv6 = [span3]
+        spans_ipv6 = [
+            span_data_module.SpanData(
+                name='child_span',
+                context=span_context.SpanContext(trace_id=trace_id),
+                span_id=1234567890,
+                parent_span_id=None,
+                attributes={
+                    'test_key': False,
+                    'test_key2': 'raw_value',
+                    # these tags are malformed and should be omitted
+                    'test_key3': 0.1,
+                },
+                start_time='2017-08-15T18:02:26.071158Z',
+                end_time='2017-08-15T18:02:36.071158Z',
+                child_span_count=None,
+                stack_trace=None,
+                time_events=None,
+                links=None,
+                status=None,
+                same_process_as_parent_span=None,
+                span_kind=1,
+            ),
+        ]
 
         ipv4 = '127.0.0.1'
         ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
@@ -196,7 +200,7 @@ class TestZipkinExporter(unittest.TestCase):
                 'timestamp': 1502820146000000,
                 'duration': 10000000,
                 'localEndpoint': local_endpoint_ipv6,
-                'tags': {'test_key': 'False'},
+                'tags': {'test_key': 'False', 'test_key2': 'raw_value'},
                 'kind': 'SERVER',
             },
         ]
@@ -204,21 +208,46 @@ class TestZipkinExporter(unittest.TestCase):
         # Test ipv4 local endpoint
         exporter_ipv4 = zipkin_exporter.ZipkinExporter(
             service_name='my_service', ipv4=ipv4)
+        ipv4_trace = span_data_module.format_legacy_trace_json(spans_ipv4)
         zipkin_spans_ipv4 = exporter_ipv4.translate_to_zipkin(
             trace_id=trace_id,
-            spans=spans_ipv4)
+            spans=ipv4_trace.get('spans'))
 
         self.assertEqual(zipkin_spans_ipv4, expected_zipkin_spans_ipv4)
 
         # Test ipv6 local endpoint
         exporter_ipv6 = zipkin_exporter.ZipkinExporter(
             service_name='my_service', ipv6=ipv6)
+        ipv6_trace = span_data_module.format_legacy_trace_json(spans_ipv6)
         zipkin_spans_ipv6 = exporter_ipv6.translate_to_zipkin(
             trace_id=trace_id,
-            spans=spans_ipv6)
+            spans=ipv6_trace.get('spans')
+        )
 
         self.assertEqual(zipkin_spans_ipv6, expected_zipkin_spans_ipv6)
 
+    def test_ignore_incorrect_spans(self):
+        span1 = {
+            'attributes': {
+                'attributeMap': {
+                    'float_value': {
+                        'value': 0.1
+                    }
+                }
+            }
+        }
+        self.assertEqual(zipkin_exporter._extract_tags_from_span(span1), {})
+
+        span2 = {
+            'attributes': {
+                'attributeMap': {
+                    'bool_value': False
+                }
+
+            }
+        }
+        self.assertEqual(zipkin_exporter._extract_tags_from_span(span2), {})
+
 
 class MockTransport(object):
     def __init__(self, exporter=None):
diff --git a/tests/unit/trace/exporters/transports/test_background_thread.py b/tests/unit/trace/exporters/transports/test_background_thread.py
index 47cb1ae..c5d649c 100644
--- a/tests/unit/trace/exporters/transports/test_background_thread.py
+++ b/tests/unit/trace/exporters/transports/test_background_thread.py
@@ -18,7 +18,6 @@ import mock
 
 from opencensus.trace.exporters.transports import background_thread
 
-
 class Test_Worker(unittest.TestCase):
 
     def _start_worker(self, worker):
@@ -186,24 +185,17 @@ class Test_Worker(unittest.TestCase):
         # the exit.
         worker._queue.put_nowait(background_thread._WORKER_TERMINATOR)
 
-        trace1 = {
-            'traceId': 'test1',
-            'spans': [{}, {}],
-        }
-
-        # Worker should be terminated after sending trace1, and trace2 won't be
-        # exported.
-        trace2 = {
-            'traceId': 'test2',
-            'spans': [{}, {}],
-        }
+        # Worker should be terminated after sending span_data1, and
+        # span_data2 won't be exported.
+        span_data1 = [mock.Mock()]
+        span_data2 = [mock.Mock()]
 
-        worker.enqueue(trace1)
-        worker.enqueue(trace2)
+        worker.enqueue(span_data1)
+        worker.enqueue(span_data2)
 
         worker._thread_main()
 
-        self.assertEqual(exporter.exported, [trace1])
+        self.assertEqual(exporter.exported, [span_data1])
 
         # trace2 should be left in the queue because worker is terminated.
         self.assertEqual(worker._queue.qsize(), 1)
diff --git a/tests/unit/trace/test_span_data.py b/tests/unit/trace/test_span_data.py
index 2d7eefc..e7dc85d 100644
--- a/tests/unit/trace/test_span_data.py
+++ b/tests/unit/trace/test_span_data.py
@@ -15,13 +15,19 @@
 import datetime
 import unittest
 
+from opencensus.trace import link
+from opencensus.trace import span_context
 from opencensus.trace import span_data as span_data_module
+from opencensus.trace import stack_trace
+from opencensus.trace import status
+from opencensus.trace import time_event
 
 
-class TestSpan(unittest.TestCase):
+class TestSpanData(unittest.TestCase):
     def test_create_span_data(self):
         span_data_module.SpanData(
             name='root',
+            context=None,
             span_id=2,
             parent_span_id=1,
             attributes={'key1': 'value1'},
@@ -31,12 +37,15 @@ class TestSpan(unittest.TestCase):
             links=None,
             status=None,
             time_events=None,
-            same_process_as_parent_span=None
+            same_process_as_parent_span=None,
+            child_span_count=None,
+            span_kind=0,
         )
 
     def test_span_data_immutable(self):
         span_data = span_data_module.SpanData(
             name='root',
+            context=None,
             span_id=2,
             parent_span_id=1,
             attributes={'key1': 'value1'},
@@ -46,10 +55,41 @@ class TestSpan(unittest.TestCase):
             links=None,
             status=None,
             time_events=None,
-            same_process_as_parent_span=None
+            same_process_as_parent_span=None,
+            child_span_count=None,
+            span_kind=0,
         )
         with self.assertRaises(AttributeError):
             span_data.name = 'child'
 
         with self.assertRaises(AttributeError):
             span_data.new_attr = 'a'
+
+    def test_format_legacy_trace_json(self):
+        trace_id = '2dd43a1d6b2549c6bc2a1a54c2fc0b05'
+        span_data = span_data_module.SpanData(
+            name='root',
+            context=span_context.SpanContext(
+                trace_id=trace_id,
+                span_id=1111
+            ),
+            span_id=2,
+            parent_span_id=1,
+            attributes={'key1': 'value1'},
+            start_time=datetime.datetime.utcnow().isoformat() + 'Z',
+            end_time=datetime.datetime.utcnow().isoformat() + 'Z',
+            stack_trace=stack_trace.StackTrace(stack_trace_hash_id='111'),
+            links=[link.Link('1111', span_id='111')],
+            status=status.Status(code=0, message='pok'),
+            time_events=[
+                time_event.TimeEvent(
+                    timestamp=datetime.datetime(1970, 1, 1)
+                )
+            ],
+            same_process_as_parent_span=False,
+            child_span_count=0,
+            span_kind=0,
+        )
+        trace_json = span_data_module.format_legacy_trace_json([span_data])
+        self.assertEqual(trace_json.get('traceId'), trace_id)
+        self.assertEqual(len(trace_json.get('spans')), 1)
diff --git a/tests/unit/trace/test_tracer.py b/tests/unit/trace/test_tracer.py
index e6c7975..4cfc3e8 100644
--- a/tests/unit/trace/test_tracer.py
+++ b/tests/unit/trace/test_tracer.py
@@ -61,7 +61,6 @@ class TestTracer(unittest.TestCase):
         assert isinstance(tracer.tracer, noop_tracer.NoopTracer)
 
     def test_should_sample_force_not_trace(self):
-        from opencensus.trace import span_context
 
         span_context = mock.Mock()
         span_context.trace_options.enabled = False
@@ -194,10 +193,10 @@ class TestTracer(unittest.TestCase):
         sampler.should_sample.return_value = True
         tracer = tracer_module.Tracer(sampler=sampler)
         span = mock.Mock()
-        span._child_spans = []
         span.attributes = {}
         span.time_events = []
         span.links = []
+        span.children = []
         span.__iter__ = mock.Mock(
             return_value=iter([span]))
         execution_context.set_current_span(span)
diff --git a/tests/unit/trace/tracers/test_context_tracer.py b/tests/unit/trace/tracers/test_context_tracer.py
index d0199ad..2a2c2c5 100644
--- a/tests/unit/trace/tracers/test_context_tracer.py
+++ b/tests/unit/trace/tracers/test_context_tracer.py
@@ -118,7 +118,7 @@ class TestContextTracer(unittest.TestCase):
         tracer = context_tracer.ContextTracer(exporter=exporter)
         mock_span = mock.Mock()
         mock_span.name = 'span'
-        mock_span._child_spans = []
+        mock_span.children = []
         mock_span.status = None
         mock_span.links = None
         mock_span.stack_trace = None
@@ -142,7 +142,7 @@ class TestContextTracer(unittest.TestCase):
         tracer = context_tracer.ContextTracer()
         mock_span = mock.Mock()
         mock_span.name = 'span'
-        mock_span._child_spans = []
+        mock_span.children = []
         mock_span.status = None
         mock_span.links = None
         mock_span.stack_trace = None
@@ -166,7 +166,7 @@ class TestContextTracer(unittest.TestCase):
         tracer._spans_list = [span]
         mock_span = mock.Mock()
         mock_span.name = 'span'
-        mock_span._child_spans = []
+        mock_span.children = []
         mock_span.status = None
         mock_span.links = None
         mock_span.stack_trace = None