diff --git a/trace/.coveragerc b/trace/.coveragerc new file mode 100644 index 000000000..a54b99aa1 --- /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 000000000..6b3e02537 --- /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 000000000..b8c64ba36 --- /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 000000000..5286f31be --- /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 000000000..a3489e476 --- /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 000000000..a26c2d707 --- /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 000000000..7c07b241f --- /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 000000000..65d2174a0 --- /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 000000000..b1e2c7987 --- /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 000000000..43bc0caaa --- /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 000000000..794433eb9 --- /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 000000000..9dcea8cd7 --- /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 000000000..bd417ec00 --- /dev/null +++ b/trace/requirements.txt @@ -0,0 +1 @@ +google-gax>=0.15.7, <0.16dev diff --git a/trace/setup.cfg b/trace/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /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 000000000..850a340c3 --- /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') diff --git a/trace/tests/__init__.py b/trace/tests/__init__.py new file mode 100644 index 000000000..0fe161d30 --- /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 000000000..890809f70 --- /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 000000000..b243a82c3 --- /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 000000000..3df6eacf7 --- /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 000000000..6e4541cfa --- /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 000000000..324bc4d5f --- /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])