Repository: census-instrumentation/opencensus-python
Pull request #1 (2017-07-25 01:35:36)

Implement trace, trace_span, span_context and reporters
Mostly reviewed in google-cloud-python repository, just changed the namespace to opencensus.trace and added unit tests and reporters.


Patch:
diff --git a/trace/.coveragerc b/trace/.coveragerc
new file mode 100644
index 00000000..a54b99a
--- /dev/null
+++ b/trace/.coveragerc
@@ -0,0 +1,11 @@
+[run]
+branch = True
+
+[report]
+fail_under = 100
+show_missing = True
+exclude_lines =
+    # Re-enable the standard pragma
+    pragma: NO COVER
+    # Ignore debug-only repr
+    def __repr__
diff --git a/trace/README.rst b/trace/README.rst
new file mode 100644
index 00000000..6b3e025
--- /dev/null
+++ b/trace/README.rst
@@ -0,0 +1,12 @@
+OpenCensus - A stats collection and distributed tracing framework
+=================================================================
+
+OpenCensus provides a framework to define and collect stats against metrics and
+to break those stats down across user-defined dimensions. The library is in
+pre-alpha stage and the API is subject to change.
+
+
+Disclaimer
+----------
+
+This is not an official Google product.
diff --git a/trace/nox.py b/trace/nox.py
new file mode 100644
index 00000000..b8c64ba
--- /dev/null
+++ b/trace/nox.py
@@ -0,0 +1,76 @@
+# Copyright 2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+
+import nox
+
+
+@nox.session
+@nox.parametrize('python_version', ['2.7', '3.4', '3.5', '3.6'])
+def unit_tests(session, python_version):
+    """Run the unit test suite."""
+
+    # Run unit tests against all supported versions of Python.
+    session.interpreter = 'python{}'.format(python_version)
+
+    # Install all test dependencies, then install this package in-place.
+    session.install('mock', 'pytest', 'pytest-cov', 'google-cloud-core')
+    session.install('-e', '.')
+
+    # Run py.test against the unit tests.
+    session.run(
+        'py.test',
+        '--quiet',
+        '--cov=opencensus.trace',
+        '--cov-append',
+        '--cov-config=.coveragerc',
+        '--cov-report=',
+        '--cov-fail-under=97',
+        'tests/',
+        *session.posargs
+    )
+
+
+@nox.session
+def lint(session):
+    """Run flake8.
+    Returns a failure if flake8 finds linting errors or sufficiently
+    serious code quality issues.
+    """
+    session.interpreter = 'python3.6'
+    session.install('flake8')
+    session.install('.')
+    session.run('flake8', 'opencensus/trace')
+
+
+@nox.session
+def lint_setup_py(session):
+    """Verify that setup.py is valid (including RST check)."""
+    session.interpreter = 'python3.6'
+    session.install('docutils', 'pygments')
+    session.run(
+        'python', 'setup.py', 'check', '--restructuredtext', '--strict')
+
+
+@nox.session
+def cover(session):
+    """Run the final coverage report.
+    This outputs the coverage report aggregating coverage from the unit
+    test runs (not system test runs), and then erases coverage data.
+    """
+    session.interpreter = 'python3.6'
+    session.install('coverage', 'pytest-cov')
+    session.run('coverage', 'report', '--show-missing', '--fail-under=100')
+    session.run('coverage', 'erase')
diff --git a/trace/opencensus/__init__.py b/trace/opencensus/__init__.py
new file mode 100644
index 00000000..5286f31
--- /dev/null
+++ b/trace/opencensus/__init__.py
@@ -0,0 +1,20 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+try:
+    import pkg_resources
+    pkg_resources.declare_namespace(__name__)
+except ImportError:
+    import pkgutil
+    __path__ = pkgutil.extend_path(__path__, __name__)
diff --git a/trace/opencensus/trace/__init__.py b/trace/opencensus/trace/__init__.py
new file mode 100644
index 00000000..a3489e4
--- /dev/null
+++ b/trace/opencensus/trace/__init__.py
@@ -0,0 +1,19 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from opencensus.trace.trace import Trace
+from opencensus.trace.trace_span import TraceSpan
+
+
+__all__ = ['Trace', 'TraceSpan']
diff --git a/trace/opencensus/trace/enums.py b/trace/opencensus/trace/enums.py
new file mode 100644
index 00000000..a26c2d7
--- /dev/null
+++ b/trace/opencensus/trace/enums.py
@@ -0,0 +1,41 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Wrappers for protocol buffer enum types.
+
+See
+https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools.
+cloudtrace.v1#google.devtools.cloudtrace.v1.ListTracesRequest.ViewType
+
+https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools.
+cloudtrace.v1#google.devtools.cloudtrace.v1.TraceSpan.SpanKind
+"""
+
+
+class Enum(object):
+    class SpanKind(object):
+        """
+        Type of span. Can be used to specify additional relationships between
+        spans in addition to a parent/child relationship.
+
+        Attributes:
+          SPAN_KIND_UNSPECIFIED (int): Unspecified.
+          RPC_SERVER (int): Indicates that the span covers server-side handling
+                            of an RPC or other remote network request.
+          RPC_CLIENT (int): Indicates that the span covers the client-side
+                            wrapper around an RPC or other remote request.
+        """
+        SPAN_KIND_UNSPECIFIED = 0
+        RPC_SERVER = 1
+        RPC_CLIENT = 2
diff --git a/trace/opencensus/trace/reporters/__init__.py b/trace/opencensus/trace/reporters/__init__.py
new file mode 100644
index 00000000..7c07b24
--- /dev/null
+++ b/trace/opencensus/trace/reporters/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/trace/opencensus/trace/reporters/file_reporter.py b/trace/opencensus/trace/reporters/file_reporter.py
new file mode 100644
index 00000000..65d2174
--- /dev/null
+++ b/trace/opencensus/trace/reporters/file_reporter.py
@@ -0,0 +1,37 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Export the trace spans to a local file."""
+
+import json
+
+
+class FileReporter(object):
+    """
+    :type file_name: str
+    :param file_name: The name of the output file.
+    """
+
+    def __init__(self, file_name):
+        self.file_name = file_name
+
+    def report(self, traces):
+        """Report the traces by printing it out.
+
+        :type traces: dict
+        :param traces: Traces collected.
+        """
+        with open(self.file_name, 'w+') as file:
+            traces_str = json.dumps(traces)
+            file.write(traces_str)
diff --git a/trace/opencensus/trace/reporters/print_reporter.py b/trace/opencensus/trace/reporters/print_reporter.py
new file mode 100644
index 00000000..b1e2c79
--- /dev/null
+++ b/trace/opencensus/trace/reporters/print_reporter.py
@@ -0,0 +1,29 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Export the trace spans by printing them out."""
+
+
+class PrintReporter(object):
+    def report(self, traces):
+        """Report the traces by printing it out.
+
+        :type traces: dict
+        :param traces: Traces collected.
+
+        :rtype: dict
+        :returns: Traces printed.
+        """
+        print(traces)
+        return traces
diff --git a/trace/opencensus/trace/span_context.py b/trace/opencensus/trace/span_context.py
new file mode 100644
index 00000000..43bc0ca
--- /dev/null
+++ b/trace/opencensus/trace/span_context.py
@@ -0,0 +1,138 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""SpanContext encapsulates the current context within the request's trace."""
+
+import logging
+import re
+import sys
+
+from opencensus.trace import trace
+
+_INVALID_TRACE_ID = '0' * 32
+_INVALID_SPAN_ID = 0
+_TRACE_HEADER_KEY = 'X_CLOUD_TRACE_CONTEXT'
+_TRACE_ID_FORMAT = '[0-9a-f]{32}?'
+
+
+class SpanContext(object):
+    """SpanContext includes 3 fields: traceId, spanId, and an enabled flag
+    which indicates whether or not the request is being traced. It contains the
+    current context to be propagated to the child spans.
+
+    :type trace_id: str
+    :param trace_id: (Optional) Trace_id is a 32 digits uuid for the trace.
+                     If not given, will generate one automatically.
+
+    :type span_id: int
+    :param span_id: (Optional) Identifier for the span, unique within a trace.
+
+    :type enabled: bool
+    :param enabled: (Optional) Indicates whether the request is traced or not.
+
+    :type from_header: bool
+    :param from_header: (Optional) Indicates whether the trace context is
+                        generated from request header.
+    """
+    def __init__(
+            self,
+            trace_id=None,
+            span_id=None,
+            enabled=True,
+            from_header=False):
+        if trace_id is None:
+            trace_id = trace.generate_trace_id()
+
+        self.trace_id = self.check_trace_id(trace_id)
+        self.span_id = self.check_span_id(span_id)
+        self.enabled = enabled
+        self.from_header = from_header
+
+    def __str__(self):
+        """Returns a string form of the SpanContext. This is the format of
+        the Trace Context Header and should be forwarded to downstream
+        requests as the X-Cloud-Trace-Context header.
+
+        :rtype: str
+        :returns: String form of the SpanContext.
+        """
+        header = '{}/{};o={}'.format(
+            self.trace_id,
+            self.span_id,
+            int(self.enabled))
+        return header
+
+    def check_span_id(self, span_id):
+        """Check the type of span_id to ensure it is int. If it is not int,
+        first try to convert it to int, if failed to convert, then log a
+        warning message and set the span_id to None.
+
+        :type span_id: int
+        :param span_id: Identifier for the span, unique within a trace.
+
+        :rtype: int
+        :returns: Span_id for the current span.
+        """
+        if span_id is None:
+            return None
+
+        if span_id == 0:
+            logging.warning(
+                'Span_id {} is invalid, cannot be zero.'.format(span_id))
+            self.from_header = False
+            return None
+
+        if not isinstance(span_id, int):
+            try:
+                span_id = int(span_id)
+            except (TypeError, ValueError):
+                logging.warning(
+                    'The type of span_id should be int, got {}.'.format(
+                        span_id.__class__.__name__))
+                self.from_header = False
+                span_id = None
+
+        return span_id
+
+    def check_trace_id(self, trace_id):
+        """Check the format of the trace_id to ensure it is 32-character hex
+        value representing a 128-bit number. Also the trace_id cannot be zero.
+
+        :type trace_id: str
+        :param trace_id:
+
+        :rtype: str
+        :returns: Trace_id for the current context.
+        """
+        assert isinstance(trace_id, str)
+
+        if trace_id is _INVALID_TRACE_ID:
+            logging.warning(
+                'Trace_id {} is invalid (cannot be all zero), '
+                'generate a new one.'.format(trace_id))
+            self.from_header = False
+            return trace.generate_trace_id()
+
+        trace_id_pattern = re.compile(_TRACE_ID_FORMAT)
+
+        match = trace_id_pattern.match(trace_id)
+
+        if match:
+            return trace_id
+        else:
+            logging.warning(
+                'Trace_id {} does not the match the required format,'
+                'generate a new one instead.'.format(trace_id))
+            self.from_header = False
+            return trace.generate_trace_id()
diff --git a/trace/opencensus/trace/trace.py b/trace/opencensus/trace/trace.py
new file mode 100644
index 00000000..794433e
--- /dev/null
+++ b/trace/opencensus/trace/trace.py
@@ -0,0 +1,119 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module is for generating Trace object which contains spans."""
+
+import uuid
+
+from opencensus.trace.reporters import file_reporter
+from opencensus.trace import trace_span
+
+
+class Trace(object):
+    """A trace describes how long it takes for an application to perform
+    an operation. It consists of a set of spans, each of which represent
+    a single timed event within the operation. Node that Trace is not
+    thread-safe and must not be shared between threads.
+
+    See
+    https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools.
+    cloudtrace.v1#google.devtools.cloudtrace.v1.Trace
+
+    :type project_id: str
+    :param project_id: (Optional) The project_id for the trace.
+
+    :type trace_id: str
+    :param trace_id: (Optional) Trace_id is a 32 hex-digits uuid for the trace.
+                     If not given, will generate one automatically.
+    """
+    def __init__(self, project_id=None, trace_id=None, reporter=None):
+        if trace_id is None:
+            trace_id = generate_trace_id()
+
+        if reporter is None:
+            file_name = '{}_{}'.format(project_id, trace_id)
+            reporter = file_reporter.FileReporter(file_name=file_name)
+
+        self.project_id = project_id
+        self.trace_id = trace_id
+        self.reporter = reporter
+        self.spans = []
+
+    def __enter__(self):
+        self.start()
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.finish()
+
+    def start(self):
+        """Start a trace, initialize an empty list of spans."""
+        self.spans = []
+
+    def finish(self):
+        """Send the trace to Stackdriver Trace API and clear the spans."""
+        self.send()
+        self.spans = []
+
+    def span(self, name='span'):
+        """Create a new span for the trace and append it to the spans list.
+
+        :type name: str
+        :param name: (Optional) The name of the span.
+
+        :rtype: :class:`~google.cloud.trace.trace_span.TraceSpan`
+        :returns: A TraceSpan to be added to the current Trace.
+        """
+        span = trace_span.TraceSpan(name)
+        self.spans.append(span)
+        return span
+
+    def send(self):
+        """API call: Patch trace to Stackdriver Trace.
+
+        See
+        https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools.
+        cloudtrace.v1#google.devtools.cloudtrace.v1.TraceService.PatchTraces
+        """
+        spans_list = []
+        for root_span in self.spans:
+            span_tree = list(iter(root_span))
+            span_tree_json = [trace_span.format_span_json(span)
+                              for span in span_tree]
+            spans_list.extend(span_tree_json)
+
+        if len(spans_list) == 0:
+            return
+
+        trace = {
+            'projectId': self.project_id,
+            'traceId': self.trace_id,
+            'spans': spans_list,
+        }
+
+        traces = {
+            'traces': [trace],
+        }
+
+        self.reporter.report(traces)
+
+
+def generate_trace_id():
+    """Generate a trace_id randomly.
+
+    :rtype: str
+    :returns: 32 digits randomly generated trace ID.
+    """
+    trace_id = uuid.uuid4().hex
+    return trace_id
diff --git a/trace/opencensus/trace/trace_span.py b/trace/opencensus/trace/trace_span.py
new file mode 100644
index 00000000..9dcea8c
--- /dev/null
+++ b/trace/opencensus/trace/trace_span.py
@@ -0,0 +1,176 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import random
+
+from itertools import chain
+
+from datetime import datetime
+from opencensus.trace.enums import Enum
+
+
+class TraceSpan(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
+    indicates the causal relationships between the individual spans in a
+    single distributed trace. Span that does not have a parent id is called
+    root span. All spans associated with a specific trace also share a common
+    trace id. Spans do not need to be continuous, there can be gaps between
+    two spans.
+
+    See
+    https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools.
+    cloudtrace.v1#google.devtools.cloudtrace.v1.TraceSpan
+
+    :type name: str
+    :param name: The name of the span.
+
+    :type kind: :class:`~opencensus.trace.enums.TraceSpan.SpanKind`
+    :param kind: Distinguishes between spans generated in a particular context.
+                 For example, two spans with the same name may be
+                 distinguished using RPC_CLIENT and RPC_SERVER to identify
+                 queueing latency associated with the span.
+
+    :type parent_span_id: int
+    :param parent_span_id: (Optional) ID of the parent span.
+
+    :type labels: dict
+    :param labels: Collection of labels associated with the span.
+                   Label keys must be less than 128 bytes.
+                   Label values must be less than 16 kilobytes.
+
+    :type start_time: str
+    :param start_time: (Optional) Start of the time interval (inclusive)
+                       during which the trace data was collected from the
+                       application.
+
+    :type end_time: str
+    :param end_time: (Optional) End of the time interval (inclusive) during
+                     which the trace data was collected from the application.
+
+    :type span_id: int
+    :param span_id: Identifier for the span, unique within a trace.
+    """
+
+    def __init__(
+            self,
+            name,
+            kind=Enum.SpanKind.SPAN_KIND_UNSPECIFIED,
+            parent_span_id=None,
+            labels={},
+            start_time=None,
+            end_time=None,
+            span_id=None):
+        self.name = name
+        self.kind = kind
+        self.parent_span_id = parent_span_id
+        self.labels = labels
+        self.start_time = start_time
+        self.end_time = end_time
+
+        if span_id is None:
+            span_id = generate_span_id()
+
+        self.span_id = span_id
+        self._child_spans = []
+
+    @property
+    def children(self):
+        """The child spans of the current span."""
+        return self._child_spans
+
+    def span(self, name='child_span'):
+        """Create a child span for the current span and append it to the child
+        spans list.
+
+        :type name: str
+        :param name: (Optional) The name of the child span.
+
+        :rtype: :class: `~google.cloud.trace.trace_span.TraceSpan`
+        :returns: A child TraceSpan to be added to the current span.
+        """
+        child_span = TraceSpan(name, parent_span_id=self.span_id)
+        self._child_spans.append(child_span)
+        return child_span
+
+    def add_label(self, label_key, label_value):
+        """Add label to span.
+
+        :type label_key: str
+        :param label_key: Label key.
+
+        :type label_value:str
+        :param label_value: Label value.
+        """
+        self.labels[label_key] = label_value
+
+    def start(self):
+        """Set the start time for a span."""
+        self.start_time = datetime.utcnow().isoformat() + 'Z'
+
+    def finish(self):
+        """Set the end time for a span."""
+        self.end_time = datetime.utcnow().isoformat() + 'Z'
+
+    def __iter__(self):
+        """Iterate through the span tree."""
+        for span in chain(*(map(iter, self.children))):
+            yield span
+        yield self
+
+    def __enter__(self):
+        """Start a span."""
+        self.start()
+        return self
+
+    def __exit__(self, exception_type, exception_value, traceback):
+        """Finish a span."""
+        self.finish()
+
+
+def generate_span_id():
+    """Return the random generated span ID for a span.
+
+    :rtype: int
+    :returns: Identifier for the span. Must be a 64-bit integer other
+              than 0 and unique within a trace.
+    """
+    span_id = random.getrandbits(64)
+    return span_id
+
+
+def format_span_json(span):
+    """Helper to format a TraceSpan in JSON format.
+
+    :type span: :class:`~google.cloud.trace.trace_span.TraceSpan`
+    :param span: A TraceSpan to be transferred to JSON format.
+
+    :rtype: dict
+    :returns: Formatted TraceSpan.
+    """
+    span_json = {
+        'name': span.name,
+        'kind': span.kind,
+        'spanId': span.span_id,
+        'startTime': span.start_time,
+        'endTime': span.end_time,
+    }
+
+    if span.parent_span_id is not None:
+        span_json['parentSpanId'] = span.parent_span_id
+
+    if span.labels is not None:
+        span_json['labels'] = span.labels
+
+    return span_json
diff --git a/trace/requirements.txt b/trace/requirements.txt
new file mode 100644
index 00000000..bd417ec
--- /dev/null
+++ b/trace/requirements.txt
@@ -0,0 +1,1 @@
+google-gax>=0.15.7, <0.16dev
diff --git a/trace/setup.cfg b/trace/setup.cfg
new file mode 100644
index 00000000..2a9acf1
--- /dev/null
+++ b/trace/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/trace/setup.py b/trace/setup.py
new file mode 100644
index 00000000..850a340
--- /dev/null
+++ b/trace/setup.py
@@ -0,0 +1,51 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A setup module for Open Source Census Instrumentation Library"""
+
+import io
+from setuptools import setup, find_packages
+
+install_requires = [
+    'google-gax>=0.15.7, <0.16dev',
+    'googleapis-common-protos[grpc]>=1.5.2, <2.0dev',
+    'google-cloud-core >= 0.24.0, < 0.25dev',
+]
+
+setup(
+    name='opencensus',
+    version='0.0.1',
+    author='OpenCensus Contributors',
+    author_email='opencensus-io@googlegroups.com',
+    classifiers=[
+        'Intended Audience :: Developers',
+        'Development Status :: 2 - Pre-Alpha',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: Apache Software License',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: Implementation :: CPython',
+    ],
+    description='A stats collection and distributed tracing framework',
+    include_package_data=True,
+    long_description=open('README.rst').read(),
+    install_requires=install_requires,
+    license='Apache-2.0',
+    packages=find_packages(),
+    namespace_packages=[],
+    url='https://github.com/census-instrumentation/opencensus-python')


Test patch:
diff --git a/trace/tests/__init__.py b/trace/tests/__init__.py
new file mode 100644
index 00000000..0fe161d
--- /dev/null
+++ b/trace/tests/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/trace/tests/unit/reporters/test_file_reporter.py b/trace/tests/unit/reporters/test_file_reporter.py
new file mode 100644
index 00000000..890809f
--- /dev/null
+++ b/trace/tests/unit/reporters/test_file_reporter.py
@@ -0,0 +1,44 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import unittest
+
+
+class TestFileReporter(unittest.TestCase):
+
+    @staticmethod
+    def _get_target_class():
+        from opencensus.trace.reporters.file_reporter import FileReporter
+
+        return FileReporter
+
+    def _make_one(self, *args, **kw):
+        return self._get_target_class()(*args, **kw)
+
+    def test_constructor(self):
+        file_name = 'file_name'
+        reporter = self._make_one(file_name=file_name)
+
+        self.assertEqual(reporter.file_name, file_name)
+
+    def test_report(self):
+        import os
+        traces = {}
+        file_name = 'file_name'
+        reporter = self._make_one(file_name=file_name)
+
+        reporter.report(traces)
+        assert os.path.exists(file_name) == 1
+        os.remove(file_name)
diff --git a/trace/tests/unit/reporters/test_print_reporter.py b/trace/tests/unit/reporters/test_print_reporter.py
new file mode 100644
index 00000000..b243a82
--- /dev/null
+++ b/trace/tests/unit/reporters/test_print_reporter.py
@@ -0,0 +1,34 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+
+class TestFileReporter(unittest.TestCase):
+
+    @staticmethod
+    def _get_target_class():
+        from opencensus.trace.reporters.print_reporter import PrintReporter
+
+        return PrintReporter
+
+    def _make_one(self, *args, **kw):
+        return self._get_target_class()(*args, **kw)
+
+    def test_report(self):
+        traces = {}
+        reporter = self._make_one()
+
+        printed = reporter.report(traces)
+        self.assertEqual(printed, traces)
diff --git a/trace/tests/unit/test_span_context.py b/trace/tests/unit/test_span_context.py
new file mode 100644
index 00000000..3df6eac
--- /dev/null
+++ b/trace/tests/unit/test_span_context.py
@@ -0,0 +1,104 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+
+class TestSpanContext(unittest.TestCase):
+
+    project = 'PROJECT'
+
+    @staticmethod
+    def _get_target_class():
+        from opencensus.trace.span_context import SpanContext
+
+        return SpanContext
+
+    def _make_one(self, *args, **kw):
+        return self._get_target_class()(*args, **kw)
+
+    def test_constructor(self):
+        trace_id = '6e0c63257de34c92bf9efcd03927272e'
+        span_id = 1234
+
+        span_context = self._make_one(trace_id=trace_id, span_id=span_id)
+
+        self.assertEqual(span_context.trace_id, trace_id)
+        self.assertEqual(span_context.span_id, span_id)
+
+    def test__str__(self):
+        trace_id = '6e0c63257de34c92bf9efcd03927272e'
+        span_id = 1234
+
+        span_context = self._make_one(
+            trace_id=trace_id,
+            span_id=span_id,
+            enabled=True)
+
+        header_expected = '6e0c63257de34c92bf9efcd03927272e/1234;o=1'
+        header = span_context.__str__()
+
+        self.assertEqual(header_expected, header)
+
+    def test_check_span_id_none(self):
+        span_context = self._make_one(from_header=True)
+        span_id = span_context.check_span_id(None)
+        self.assertIsNone(span_id)
+
+    def test_check_span_id_zero(self):
+        span_context = self._make_one(from_header=True)
+        span_id = span_context.check_span_id(0)
+        self.assertFalse(span_context.from_header)
+        self.assertIsNone(span_id)
+
+    def test_check_span_id_not_int(self):
+        span_id = {}
+        span_context = self._make_one()
+        span_id_checked = span_context.check_span_id(span_id)
+        self.assertIsNone(span_id_checked)
+        self.assertFalse(span_context.from_header)
+
+    def test_check_span_id_valid(self):
+        span_id = 1234
+        span_context = self._make_one(from_header=True)
+        span_id_checked = span_context.check_span_id(span_id)
+        self.assertEqual(span_id, span_id_checked)
+
+    def test_check_trace_id_invalid(self):
+        from opencensus.trace.span_context import _INVALID_TRACE_ID
+
+        span_context = self._make_one(from_header=True)
+
+        trace_id_checked = span_context.check_trace_id(_INVALID_TRACE_ID)
+
+        self.assertFalse(span_context.from_header)
+        self.assertNotEqual(trace_id_checked, _INVALID_TRACE_ID)
+
+    def test_check_trace_id_not_match(self):
+        trace_id_test = 'test_trace_id'
+
+        span_context = self._make_one(from_header=True)
+        trace_id_checked = span_context.check_trace_id(trace_id_test)
+
+        self.assertFalse(span_context.from_header)
+        self.assertNotEqual(trace_id_checked, trace_id_test)
+
+    def test_check_trace_id_match(self):
+        trace_id = '6e0c63257de34c92bf9efcd03927272e'
+
+        span_context = self._make_one(from_header=True)
+        trace_id_checked = span_context.check_trace_id(trace_id)
+
+        self.assertEqual(trace_id, trace_id_checked)
+        self.assertTrue(span_context.from_header)
diff --git a/trace/tests/unit/test_trace.py b/trace/tests/unit/test_trace.py
new file mode 100644
index 00000000..6e4541c
--- /dev/null
+++ b/trace/tests/unit/test_trace.py
@@ -0,0 +1,208 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import mock
+
+
+class TestTrace(unittest.TestCase):
+
+    project = 'PROJECT'
+
+    @staticmethod
+    def _get_target_class():
+        from opencensus.trace.trace import Trace
+
+        return Trace
+
+    def _make_one(self, *args, **kw):
+        return self._get_target_class()(*args, **kw)
+
+    def test_constructor_defaults(self):
+        trace_id = 'test_trace_id'
+
+        patch = mock.patch(
+            'opencensus.trace.trace.generate_trace_id',
+            return_value=trace_id)
+
+        with patch:
+            trace = self._make_one()
+
+        self.assertEqual(trace.trace_id, trace_id)
+
+    def test_constructor_explicit(self):
+        trace_id = 'test_trace_id'
+        reporter = mock.Mock()
+
+        trace = self._make_one(
+            project_id=self.project,
+            trace_id=trace_id,
+            reporter=reporter)
+
+        self.assertEqual(trace.project_id, self.project)
+        self.assertEqual(trace.trace_id, trace_id)
+        self.assertIs(trace.reporter, reporter)
+
+    def test_start(self):
+        trace = self._make_one(project_id=self.project)
+        trace.start()
+
+        self.assertEqual(trace.spans, [])
+
+    def test_finish_with_valid_span(self):
+        from opencensus.trace.enums import Enum
+        from opencensus.trace.trace_span import TraceSpan
+
+        reporter = mock.Mock()
+        trace = self._make_one(reporter=reporter)
+
+        span_name = 'span'
+        span_id = 123
+        kind = Enum.SpanKind.SPAN_KIND_UNSPECIFIED
+        start_time = '2017-06-25'
+        end_time = '2017-06-26'
+
+        span = mock.Mock(spec=TraceSpan)
+        span.name = span_name
+        span.kind = kind
+        span.parent_span_id = None
+        span.span_id = span_id
+        span.start_time = start_time
+        span.end_time = end_time
+        span.labels = None
+        span.children = []
+        span.__iter__ = mock.Mock(return_value=iter([span]))
+
+        with trace:
+            trace.spans = [span]
+            self.assertEqual(trace.spans, [span])
+
+        self.assertEqual(trace.spans, [])
+
+    def test_span(self):
+        from opencensus.trace.trace_span import TraceSpan
+
+        span_name = 'test_span_name'
+
+        trace = self._make_one(project_id=self.project)
+        trace.spans = []
+
+        trace.span(name=span_name)
+        self.assertEqual(len(trace.spans), 1)
+
+        result_span = trace.spans[0]
+        self.assertIsInstance(result_span, TraceSpan)
+        self.assertEqual(result_span.name, span_name)
+
+    def test_send_without_spans(self):
+        trace_id = 'test_trace_id'
+        reporter = mock.Mock()
+        trace = self._make_one(
+            project_id=self.project,
+            trace_id=trace_id,
+            reporter=reporter)
+        trace.spans = []
+
+        trace.send()
+
+        self.assertFalse(reporter.called)
+        self.assertEqual(trace.project_id, self.project)
+        self.assertEqual(trace.trace_id, trace_id)
+        self.assertEqual(trace.spans, [])
+
+    def test_send_with_spans(self):
+        from opencensus.trace.enums import Enum
+        from opencensus.trace.trace_span import TraceSpan
+
+        trace_id = 'test_trace_id'
+        reporter = mock.Mock()
+        trace = self._make_one(
+            project_id=self.project,
+            trace_id=trace_id,
+            reporter=reporter)
+        child_span_name = 'child_span'
+        root_span_name = 'root_span'
+        child_span_id = 123
+        root_span_id = 456
+        kind = Enum.SpanKind.SPAN_KIND_UNSPECIFIED
+        start_time = '2017-06-25'
+        end_time = '2017-06-26'
+        labels = {
+            '/http/status_code': '200',
+            '/component': 'HTTP load balancer',
+        }
+
+        child_span = mock.Mock(spec=TraceSpan)
+        child_span.name = child_span_name
+        child_span.kind = kind
+        child_span.parent_span_id = root_span_id
+        child_span.span_id = child_span_id
+        child_span.start_time = start_time
+        child_span.end_time = end_time
+        child_span.labels = labels
+        child_span.children = []
+        child_span.__iter__ = mock.Mock(return_value=iter([child_span]))
+
+        root_span = mock.Mock(spec=TraceSpan)
+        root_span.name = root_span_name
+        root_span.kind = kind
+        root_span.parent_span_id = None
+        root_span.span_id = root_span_id
+        root_span.start_time = start_time
+        root_span.end_time = end_time
+        root_span.labels = None
+        root_span.children = []
+        root_span.__iter__ = mock.Mock(
+            return_value=iter([root_span, child_span]))
+
+        child_span_json = {
+            'name': child_span.name,
+            'kind': kind,
+            'parentSpanId': root_span_id,
+            'spanId': child_span_id,
+            'startTime': start_time,
+            'endTime': end_time,
+            'labels': labels,
+        }
+
+        root_span_json = {
+            'name': root_span.name,
+            'kind': kind,
+            'spanId': root_span_id,
+            'startTime': start_time,
+            'endTime': end_time,
+        }
+
+        trace.spans = [root_span]
+        traces = {
+            'traces': [
+                {
+                    'projectId': self.project,
+                    'traceId': trace_id,
+                    'spans': [
+                        root_span_json,
+                        child_span_json
+                    ]
+                }
+            ]
+        }
+
+        trace.send()
+
+        reporter.report.assert_called_with(traces)
+
+        self.assertEqual(trace.project_id, self.project)
+        self.assertEqual(trace.trace_id, trace_id)
+        self.assertEqual(trace.spans, [root_span])
diff --git a/trace/tests/unit/test_trace_span.py b/trace/tests/unit/test_trace_span.py
new file mode 100644
index 00000000..324bc4d
--- /dev/null
+++ b/trace/tests/unit/test_trace_span.py
@@ -0,0 +1,163 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import mock
+
+
+class TestTraceSpan(unittest.TestCase):
+
+    project = 'PROJECT'
+
+    @staticmethod
+    def _get_target_class():
+        from opencensus.trace.trace_span import TraceSpan
+
+        return TraceSpan
+
+    def _make_one(self, *args, **kw):
+        return self._get_target_class()(*args, **kw)
+
+    def test_constructor_defaults(self):
+        from opencensus.trace.enums import Enum
+
+        span_id = 'test_span_id'
+        span_name = 'test_span_name'
+
+        patch = mock.patch(
+            'opencensus.trace.trace_span.generate_span_id',
+            return_value=span_id)
+
+        with patch:
+            span = self._make_one(span_name)
+
+        self.assertEqual(span.name, span_name)
+        self.assertEqual(span.span_id, span_id)
+        self.assertEqual(span.kind, Enum.SpanKind.SPAN_KIND_UNSPECIFIED)
+        self.assertIsNone(span.parent_span_id)
+        self.assertEqual(span.labels, {})
+        self.assertIsNone(span.start_time)
+        self.assertIsNone(span.end_time)
+        self.assertEqual(span.children, [])
+
+    def test_constructor_explicit(self):
+        from opencensus.trace.enums import Enum
+
+        from datetime import datetime
+
+        span_id = 'test_span_id'
+        span_name = 'test_span_name'
+        kind = Enum.SpanKind.RPC_CLIENT
+        parent_span_id = 1234
+        start_time = datetime.utcnow().isoformat() + 'Z'
+        end_time = datetime.utcnow().isoformat() + 'Z'
+        labels = {
+            '/http/status_code': '200',
+            '/component': 'HTTP load balancer',
+        }
+
+        span = self._make_one(
+            name=span_name,
+            kind=kind,
+            parent_span_id=parent_span_id,
+            labels=labels,
+            start_time=start_time,
+            end_time=end_time,
+            span_id=span_id)
+
+        self.assertEqual(span.name, span_name)
+        self.assertEqual(span.span_id, span_id)
+        self.assertEqual(span.kind, kind)
+        self.assertEqual(span.parent_span_id, parent_span_id)
+        self.assertEqual(span.labels, labels)
+        self.assertEqual(span.start_time, start_time)
+        self.assertEqual(span.end_time, end_time)
+        self.assertEqual(span.children, [])
+
+    def test_span(self):
+        from opencensus.trace.enums import Enum
+
+        span_id = 'test_span_id'
+        root_span_name = 'root_span'
+        child_span_name = 'child_span'
+        root_span = self._make_one(root_span_name)
+        root_span._child_spans = []
+        kind = Enum.SpanKind.SPAN_KIND_UNSPECIFIED
+
+        patch = mock.patch(
+            'opencensus.trace.trace_span.generate_span_id',
+            return_value=span_id)
+
+        with patch:
+            with root_span:
+                root_span.span(child_span_name)
+
+        self.assertEqual(len(root_span._child_spans), 1)
+
+        result_child_span = root_span._child_spans[0]
+
+        self.assertEqual(result_child_span.name, child_span_name)
+        self.assertEqual(result_child_span.span_id, span_id)
+        self.assertEqual(result_child_span.kind, kind)
+        self.assertEqual(result_child_span.parent_span_id, root_span.span_id)
+        self.assertEqual(result_child_span.labels, {})
+        self.assertIsNone(result_child_span.start_time)
+        self.assertIsNone(result_child_span.end_time)
+
+    def test_add_label(self):
+        span_name = 'test_span_name'
+        span = self._make_one(span_name)
+        label_key = 'label_key'
+        label_value = 'label_value'
+        span.add_label(label_key, label_value)
+
+        self.assertEqual(span.labels[label_key], label_value)
+        span.labels.pop(label_key, None)
+
+    def test_start(self):
+        span_name = 'root_span'
+        span = self._make_one(span_name)
+        self.assertIsNone(span.start_time)
+
+        span.start()
+        self.assertIsNotNone(span.start_time)
+
+    def test_finish(self):
+        span_name = 'root_span'
+        span = self._make_one(span_name)
+        self.assertIsNone(span.end_time)
+
+        span.finish()
+        self.assertIsNotNone(span.end_time)
+
+    def test___iter__(self):
+        root_span_name = 'root_span_name'
+        child1_span_name = 'child1_span_name'
+        child2_span_name = 'child2_span_name'
+        child1_child1_span_name = 'child1_child1_span_name'
+
+        root_span = self._make_one(root_span_name)
+        child1_span = self._make_one(child1_span_name)
+        child2_span = self._make_one(child2_span_name)
+        child1_child1_span = self._make_one(child1_child1_span_name)
+
+        child1_span._child_spans.append(child1_child1_span)
+        root_span._child_spans.extend([child1_span, child2_span])
+
+        span_iter_list = list(iter(root_span))
+
+        self.assertEqual(
+            span_iter_list,
+            [child1_child1_span, child1_span, child2_span, root_span])