diff --git a/trace/.coveragerc b/trace/.coveragerc new file mode 100644 index 000000000000..a54b99aa14b7 --- /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/MANIFEST.in b/trace/MANIFEST.in index 5fffff88c833..8a2c2aa5ab99 100644 --- a/trace/MANIFEST.in +++ b/trace/MANIFEST.in @@ -1,5 +1,6 @@ include README.rst LICENSE -global-include *.json +recursive-include tests * +global-include google *.json *.proto graft google global-exclude *.py[co] global-exclude __pycache__ diff --git a/trace/google/cloud/trace/__init__.py b/trace/google/cloud/trace/__init__.py new file mode 100644 index 000000000000..461d41be3b20 --- /dev/null +++ b/trace/google/cloud/trace/__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. + +from google.cloud.trace.client import Client +from google.cloud.trace.trace import Trace +from google.cloud.trace.trace_span import TraceSpan + + +__all__ = ['Client', 'Trace', 'TraceSpan'] diff --git a/trace/google/cloud/trace/_gax.py b/trace/google/cloud/trace/_gax.py new file mode 100644 index 000000000000..b412b54ee856 --- /dev/null +++ b/trace/google/cloud/trace/_gax.py @@ -0,0 +1,213 @@ +# 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. + +"""GAX Wrapper for interacting with the Stackdriver Trace API.""" + +from google.cloud.gapic.trace.v1 import trace_service_client +from google.cloud.proto.devtools.cloudtrace.v1 import trace_pb2 +from google.gax import CallOptions +from google.gax import INITIAL_PAGE +from google.cloud._helpers import make_secure_channel +from google.cloud._http import DEFAULT_USER_AGENT +from google.cloud.iterator import GAXIterator +from google.protobuf.json_format import MessageToDict +from google.protobuf.json_format import ParseDict + + +class _TraceAPI(object): + """Wrapper to help mapping trace-related APIs. + + See + https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools. + cloudtrace.v1 + + :type gax_api: + :class:`~google.cloud.gapic.trace.v1.trace_service_client. + TraceServiceClient` + :param gax_api: API object used to make GAX requests. + + :type client: :class:`~google.cloud.trace.client.Client` + :param client: The client that owns this API object. + """ + def __init__(self, gax_api, client): + self._gax_api = gax_api + self.client = client + + def patch_traces(self, project_id, traces, options=None): + """Sends new traces to Stackdriver Trace or updates existing traces. + + :type project_id: str + :param project_id: ID of the Cloud project where the trace data is + stored. + + :type traces: dict + :param traces: The traces to be patched in the API call. + + :type options: :class:`~google.gax.CallOptions` + :param options: (Optional) Overrides the default settings for this + call, e.g, timeout, retries etc. + """ + traces_pb = _traces_mapping_to_pb(traces) + self._gax_api.patch_traces(project_id, traces_pb, options) + + def get_trace(self, project_id, trace_id, options=None): + """Gets a single trace by its ID. + + :type project_id: str + :param project_id: ID of the Cloud project where the trace data is + stored. + + :type trace_id: str + :param trace_id: ID of the trace to return. + + :type options: :class:`~google.gax.CallOptions` + :param options: (Optional) Overrides the default settings for this + call, e.g, timeout, retries etc. + + :rtype: :dict + :returns: A Trace dict. + """ + trace_pb = self._gax_api.get_trace(project_id, trace_id, options) + trace_mapping = _parse_trace_pb(trace_pb) + return trace_mapping + + def list_traces( + self, + project_id, + view=None, + page_size=None, + start_time=None, + end_time=None, + filter_=None, + order_by=None, + page_token=None): + """Returns of a list of traces that match the specified filter + conditions. + + :type project_id: str + :param project_id: ID of the Cloud project where the trace data is + stored. + + :type view: :class:`google.cloud.gapic.trace.v1.enums. + ListTracesRequest.ViewType` + :param view: (Optional) Type of data returned for traces in the list. + Default is ``MINIMAL``. + + :type page_size: int + :param page_size: (Optional) Maximum number of traces to return. + If not specified or <= 0, the implementation selects + a reasonable value. The implementation may return + fewer traces than the requested page size. + + :type start_time: :class:`google.protobuf.timestamp_pb2.Timestamp` + :param start_time: (Optional) Start of the time interval (inclusive) + during which the trace data was collected from the + application. + + :type end_time: :class:`google.protobuf.timestamp_pb2.Timestamp` + :param end_time: (Optional) End of the time interval (inclusive) + during which the trace data was collected from the + application. + + :type filter_: str + :param filter_: (Optional) An optional filter for the request. + + :type order_by: str + :param order_by: (Optional) Field used to sort the returned traces. + + :type page_token: str + :param page_token: opaque marker for the next "page" of entries. If not + passed, the API will return the first page of + entries. + + :rtype: :class:`~google.cloud.iterator.Iterator` + :returns: Traces that match the specified filter conditions. + """ + if page_token is None: + page_token = INITIAL_PAGE + options = CallOptions(page_token=page_token) + page_iter = self._gax_api.list_traces( + project_id=project_id, + view=view, + page_size=page_size, + start_time=start_time, + end_time=end_time, + filter_=filter_, + order_by=order_by, + options=options) + item_to_value = _item_to_mapping + return GAXIterator(self.client, page_iter, item_to_value) + + +def _parse_trace_pb(trace_pb): + """Parse a ``Trace`` protobuf to a dictionary. + + :type trace_pb: :class:`google.cloud.proto.devtools.cloudtrace.v1. + trace_pb2.Trace` + :param trace_pb: A trace protobuf instance. + + :rtype: dict + :returns: The converted trace dict. + """ + try: + return MessageToDict(trace_pb) + except TypeError: + raise + + +def _item_to_mapping(iterator, trace_pb): + """Helper callable function for the GAXIterator + + :type iterator: :class:`~google.cloud.iterator.Iterator` + :param iterator: The iterator that is currently in use. + + :type trace_pb: :class:`google.cloud.proto.devtools.cloudtrace.v1. + trace_pb2.Trace` + :param trace_pb: A trace protobuf instance. + """ + mapping = _parse_trace_pb(trace_pb) + return mapping + + +def make_gax_trace_api(client): + """Create an instance of the GAX Trace API. + + :type client: :class:`~google.cloud.trace.client.Client` + :param client: The client that holds configuration details. + + :rtype: :class:`~google.cloud.trace._gax._TraceAPI` + :returns: A Trace API instance with the proper configurations. + """ + channel = make_secure_channel( + client._credentials, + DEFAULT_USER_AGENT, + trace_service_client.TraceServiceClient.SERVICE_ADDRESS) + generated = trace_service_client.TraceServiceClient( + channel=channel, + lib_name='gccl') + return _TraceAPI(generated, client) + + +def _traces_mapping_to_pb(traces_mapping): + """Convert a trace dict to protobuf. + + :type traces_mapping: dict + :param traces_mapping: A trace mapping. + + :rtype: class:`google.cloud.proto.devtools.cloudtrace.v1.trace_pb2.Traces` + :returns: The converted protobuf type traces. + """ + traces_pb = trace_pb2.Traces() + ParseDict(traces_mapping, traces_pb) + return traces_pb diff --git a/trace/google/cloud/trace/client.py b/trace/google/cloud/trace/client.py new file mode 100644 index 000000000000..bc6e08cbda4d --- /dev/null +++ b/trace/google/cloud/trace/client.py @@ -0,0 +1,186 @@ +# 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. + +"""Client for interacting with the Stackdriver Trace API.""" + +from google.cloud.trace._gax import make_gax_trace_api +from google.cloud.trace.trace import Trace +from google.cloud.client import ClientWithProject +from google.cloud._helpers import _datetime_to_pb_timestamp + + +class Client(ClientWithProject): + """Client to bundle configuration needed for API requests. + + :type project: str + :param project: The project which the client acts on behalf of. + If not passed, falls back to the default inferred from + the environment. + + :type credentials: :class:`~google.auth.credentials.Credentials` + :param credentials: (Optional) The OAuth2 Credentials to use for this + client. If not passed, falls back to the default + inferred from the environment. + """ + _trace_api = None + + def __init__(self, project=None, credentials=None): + super(Client, self).__init__( + project=project, credentials=credentials) + + @property + def trace_api(self): + """Helper for trace-related API calls. + + See + https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools. + cloudtrace.v1 + """ + self._trace_api = make_gax_trace_api(self) + return self._trace_api + + def trace(self, project_id=None, trace_id=None): + """Initialize a new trace instance. + + :type project_id: str + :param project_id: ID of the Cloud project where the trace data + is stored. + + :type trace_id: str + :param trace_id: ID of the trace. 32 digits uuid. + + :rtype: :class:`~google.cloud.trace.trace.Trace` + :returns: A Trace instance. + """ + if project_id is None: + project_id = self.project + + return Trace(client=self, project_id=project_id, trace_id=trace_id) + + def patch_traces(self, traces, project_id=None, options=None): + """Sends new traces to Stackdriver Trace or updates existing traces. + + :type traces: dict + :param traces: The traces to be patched in the API call. + + :type project_id: str + :param project_id: (Optional) ID of the Cloud project where the trace + data is stored. + + :type options: :class:`~google.gax.CallOptions` + :param options: (Optional) Overrides the default settings for this + call, e.g, timeout, retries etc. + """ + if project_id is None: + project_id = self.project + + self.trace_api.patch_traces( + project_id=project_id, + traces=traces, + options=options) + + def get_trace(self, trace_id, project_id=None, options=None): + """Gets a single trace by its ID. + + :type project_id: str + :param project_id: ID of the Cloud project where the trace data is + stored. + + :type trace_id: str + :param trace_id: ID of the trace to return. + + :type options: :class:`~google.gax.CallOptions` + :param options: (Optional) Overrides the default settings for this + call, e.g, timeout, retries etc. + + :rtype: dict + :returns: A Trace dict. + """ + if project_id is None: + project_id = self.project + + return self.trace_api.get_trace( + project_id=project_id, + trace_id=trace_id, + options=options) + + def list_traces( + self, + project_id=None, + view=None, + page_size=None, + start_time=None, + end_time=None, + filter_=None, + order_by=None, + page_token=None): + """Returns of a list of traces that match the filter conditions. + + :type project_id: str + :param project_id: (Optional) ID of the Cloud project where the trace + data is stored. + + :type view: :class:`google.cloud.gapic.trace.v1.enums. + ListTracesRequest.ViewType` + :param view: (Optional) Type of data returned for traces in the list. + Default is ``MINIMAL``. + + :type page_size: int + :param page_size: (Optional) Maximum number of traces to return. + If not specified or <= 0, the implementation selects + a reasonable value. The implementation may return + fewer traces than the requested page size. + + :type start_time: :class:`~datetime.datetime` + :param start_time: (Optional) Start of the time interval (inclusive) + during which the trace data was collected from the + application. + + :type end_time: :class:`~datetime.datetime` + :param end_time: (Optional) End of the time interval (inclusive) during + which the trace data was collected from the + application. + + :type filter_: str + :param filter_: (Optional) An optional filter for the request. + + :type order_by: str + :param order_by: (Optional) Field used to sort the returned traces. + + :type page_token: str + :param page_token: opaque marker for the next "page" of entries. If not + passed, the API will return the first page of + entries. + + :rtype: :class:`~google.cloud.iterator.Iterator` + :returns: Traces that match the specified filter conditions. + """ + if project_id is None: + project_id = self.project + + if start_time is not None: + start_time = _datetime_to_pb_timestamp(start_time) + + if end_time is not None: + end_time = _datetime_to_pb_timestamp(end_time) + + return self.trace_api.list_traces( + project_id=project_id, + view=view, + page_size=page_size, + start_time=start_time, + end_time=end_time, + filter_=filter_, + order_by=order_by, + page_token=page_token) diff --git a/trace/google/cloud/trace/trace.py b/trace/google/cloud/trace/trace.py new file mode 100644 index 000000000000..e726c7361f2f --- /dev/null +++ b/trace/google/cloud/trace/trace.py @@ -0,0 +1,125 @@ +# 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. + +"""Trace for interacting with the Stackdriver Trace API.""" + +from google.cloud.trace.trace_span import TraceSpan +from google.cloud.trace.trace_span import format_span_json + +import uuid + + +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 client: :class:`~google.cloud.trace.client.Client` + :param client: A client which holds the credentials and project + configuration for the 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 digits uuid for the trace. + If not given, will generate one automatically. + """ + def __init__(self, client, project_id=None, trace_id=None): + self.client = client + + if project_id is None: + project_id = client.project + + self.project_id = project_id + + if trace_id is None: + trace_id = generate_trace_id() + + self.trace_id = trace_id + + 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 = 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 = [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.client.patch_traces( + project_id=self.project_id, + traces=traces, + options=None) + + +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/google/cloud/trace/trace_span.py b/trace/google/cloud/trace/trace_span.py new file mode 100644 index 000000000000..3ead9243d5cb --- /dev/null +++ b/trace/google/cloud/trace/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. + +"""TraceSpan for sending traces to the Stackdriver Trace API.""" + +from datetime import datetime +from google.cloud.gapic.trace.v1.enums import TraceSpan as Enum + +from itertools import chain +import random + + +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:`~google.cloud.trace.span.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=None, + 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 set_start_time(self): + """Set the start time for a span.""" + self.start_time = datetime.utcnow().isoformat() + 'Z' + + def set_end_time(self): + """Set the end time for a span.""" + self.end_time = datetime.utcnow().isoformat() + 'Z' + + def __iter__(self): + for span in chain(*(map(iter, self.children))): + yield span + yield self + + def __enter__(self): + self.set_start_time() + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.set_end_time() + + +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/nox.py b/trace/nox.py index 0f6bd713afbe..d7376a745130 100644 --- a/trace/nox.py +++ b/trace/nox.py @@ -17,6 +17,9 @@ import nox +LOCAL_DEPS = ('../core/',) + + @nox.session @nox.parametrize('python_version', ['2.7', '3.4', '3.5', '3.6']) def unit_tests(session, python_version): @@ -26,11 +29,34 @@ def unit_tests(session, python_version): session.interpreter = 'python{}'.format(python_version) # Install all test dependencies, then install this package in-place. - session.install('mock', 'pytest', 'pytest-cov') + session.install('mock', 'pytest', 'pytest-cov', *LOCAL_DEPS) session.install('-e', '.') # Run py.test against the unit tests. - session.run('py.test', '--quiet', 'tests/') + session.run( + 'py.test', + '--quiet', + '--cov=google.cloud.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 = 'python2.7' + session.install('flake8', *LOCAL_DEPS) + session.install('.') + session.run('flake8', 'google/cloud/trace') + @nox.session def lint_setup_py(session): @@ -39,3 +65,15 @@ def lint_setup_py(session): 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 = 'python2.7' + session.install('coverage', 'pytest-cov') + session.run('coverage', 'report', '--show-missing', '--fail-under=100') + session.run('coverage', 'erase') diff --git a/trace/setup.py b/trace/setup.py index 886e20393f61..aeeae31756e4 100644 --- a/trace/setup.py +++ b/trace/setup.py @@ -6,11 +6,11 @@ """ from setuptools import setup, find_packages -import sys 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( diff --git a/trace/tests/__init__.py b/trace/tests/__init__.py new file mode 100644 index 000000000000..0fe161d30fc3 --- /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/test__gax.py b/trace/tests/unit/test__gax.py new file mode 100644 index 000000000000..3f950021b85e --- /dev/null +++ b/trace/tests/unit/test__gax.py @@ -0,0 +1,429 @@ +# 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 + +from google.cloud._testing import _GAXBaseAPI + + +class _Base(object): + project = 'PROJECT' + + def _make_one(self, *args, **kw): + return self._get_target_class()(*args, **kw) + + +class Test__TraceAPI(_Base, unittest.TestCase): + + @staticmethod + def _get_target_class(): + from google.cloud.trace._gax import _TraceAPI + + return _TraceAPI + + def test_constructor(self): + gax_api = object() + client = object() + api = self._make_one(gax_api, client) + self.assertIs(api._gax_api, gax_api) + self.assertIs(api.client, client) + + def test_patch_traces(self): + from google.cloud.gapic.trace.v1 import trace_service_client + from google.cloud.proto.devtools.cloudtrace.v1.trace_pb2 import ( + TraceSpan, Trace, Traces) + from google.cloud.trace._gax import _traces_mapping_to_pb + from google.cloud._helpers import _datetime_to_pb_timestamp + + from datetime import datetime + + trace_id = 'test_trace_id' + span_id = 1234 + span_name = 'test_span_name' + start_time = datetime.utcnow() + end_time = datetime.utcnow() + + traces = { + 'traces': [ + { + 'projectId': self.project, + 'traceId': trace_id, + 'spans': [ + { + 'spanId': span_id, + 'name': span_name, + 'startTime': start_time.isoformat() + 'Z', + 'endTime': end_time.isoformat() + 'Z', + }, + ], + }, + ], + } + + traces_pb = _traces_mapping_to_pb(traces) + + gax_api = mock.Mock(spec=trace_service_client.TraceServiceClient) + api = self._make_one(gax_api, None) + api.patch_traces(project_id=self.project, traces=traces) + + gax_api.patch_traces.assert_called_with(self.project, traces_pb, None) + + call_args = gax_api.patch_traces.call_args[0] + self.assertEqual(len(call_args), 3) + traces_called = call_args[1] + self.assertEqual(len(traces_called.traces), 1) + trace = traces_called.traces[0] + + self.assertEqual(len(trace.spans), 1) + span = trace.spans[0] + + self.assertIsInstance(traces_called, Traces) + self.assertEqual(trace.project_id, self.project) + self.assertEqual(trace.trace_id, trace_id) + self.assertIsInstance(trace, Trace) + + self.assertEqual(span.span_id, span_id) + self.assertEqual(span.name, span_name) + self.assertEqual( + span.start_time, + _datetime_to_pb_timestamp(start_time)) + self.assertEqual( + span.end_time, + _datetime_to_pb_timestamp(end_time)) + self.assertIsInstance(span, TraceSpan) + + def test_get_trace(self): + from google.cloud.gapic.trace.v1 import trace_service_client + + trace_id = 'test_trace_id' + + gax_api = mock.Mock(spec=trace_service_client.TraceServiceClient) + api = self._make_one(gax_api, None) + patch = mock.patch('google.cloud.trace._gax._parse_trace_pb', + return_value='fake_pb_result') + + with patch: + api.get_trace(project_id=self.project, trace_id=trace_id) + + gax_api.get_trace.assert_called_with(self.project, trace_id, None) + + def _make_trace_pb( + self, + project, + trace_id, + span_id, + span_name, + start_time, + end_time, + parent_span_id, + labels): + from google.cloud.trace._gax import _traces_mapping_to_pb + + span_kind = 2 + + traces = { + 'traces': [ + { + 'projectId': project, + 'traceId': trace_id, + 'spans': [ + { + 'spanId': span_id, + 'name': span_name, + 'startTime': start_time, + 'endTime': end_time, + 'kind': span_kind, + 'parentSpanId': parent_span_id, + 'labels': labels, + }, + ], + }, + ], + } + + traces_pb = _traces_mapping_to_pb(traces) + trace_pb = traces_pb.traces + return trace_pb + + def test_list_traces_no_paging(self): + from google.cloud._testing import _GAXPageIterator + from google.cloud.gapic.trace.v1 import trace_service_client + from google.cloud.gapic.trace.v1.enums import ListTracesRequest as Enum + from google.gax import INITIAL_PAGE + + from datetime import datetime + + trace_id = 'test_trace_id' + span_id = 1234 + span_name = 'test_span_name' + span_kind = 'RPC_CLIENT' + parent_span_id = 123 + start_time = datetime.utcnow().isoformat() + 'Z' + end_time = datetime.utcnow().isoformat() + 'Z' + labels = { + '/http/status_code': '200', + '/component': 'HTTP load balancer', + } + size = 10 + view_type = Enum.ViewType.COMPLETE + + trace_pb = self._make_trace_pb( + self.project, + trace_id, + span_id, + span_name, + start_time, + end_time, + parent_span_id, + labels) + + response = _GAXPageIterator(trace_pb) + gax_api = mock.Mock(spec=trace_service_client.TraceServiceClient) + gax_api.list_traces.return_value = response + api = self._make_one(gax_api, None) + + iterator = api.list_traces( + project_id=self.project, + view=view_type, + page_size=size) + + traces = list(iterator) + + self.assertEqual(len(traces), 1) + trace = traces[0] + + self.assertEqual(len(trace['spans']), 1) + span = trace['spans'][0] + + self.assertEqual(trace['projectId'], self.project) + self.assertEqual(trace['traceId'], trace_id) + + self.assertEqual(span['spanId'], str(span_id)) + self.assertEqual(span['name'], span_name) + + self.assertEqual( + span['startTime'], start_time) + self.assertEqual( + span['endTime'], end_time) + self.assertEqual(span['kind'], span_kind) + self.assertEqual(span['parentSpanId'], str(parent_span_id)) + self.assertEqual(span['labels'], labels) + + call_args = gax_api.list_traces.call_args[1] + + self.assertEqual(call_args['project_id'], self.project) + self.assertEqual(call_args['view'], view_type) + self.assertEqual(call_args['page_size'], size) + self.assertIsNone(call_args['start_time']) + self.assertIsNone(call_args['end_time']) + self.assertIsNone(call_args['filter_']) + self.assertIsNone(call_args['order_by']) + self.assertEqual(call_args['options'].page_token, INITIAL_PAGE) + + def test_list_traces_with_paging(self): + from google.cloud._testing import _GAXPageIterator + from google.cloud.gapic.trace.v1 import trace_service_client + from google.cloud.gapic.trace.v1.enums import ListTracesRequest as Enum + + from datetime import datetime + + trace_id = 'test_trace_id' + span_id = 1234 + span_name = 'test_span_name' + span_kind = 'RPC_CLIENT' + parent_span_id = 123 + start_time = datetime.utcnow().isoformat() + 'Z' + end_time = datetime.utcnow().isoformat() + 'Z' + labels = { + '/http/status_code': '200', + '/component': 'HTTP load balancer', + } + size = 10 + view_type = Enum.ViewType.COMPLETE + token = 'TOKEN' + + trace_pb = self._make_trace_pb( + self.project, + trace_id, + span_id, + span_name, + start_time, + end_time, + parent_span_id, + labels) + + response = _GAXPageIterator(trace_pb) + gax_api = mock.Mock(spec=trace_service_client.TraceServiceClient) + gax_api.list_traces.return_value = response + api = self._make_one(gax_api, None) + + iterator = api.list_traces( + project_id=self.project, + view=view_type, + page_size=size, + page_token=token) + + traces = list(iterator) + + self.assertEqual(len(traces), 1) + trace = traces[0] + + self.assertEqual(len(trace['spans']), 1) + span = trace['spans'][0] + + self.assertEqual(trace['projectId'], self.project) + self.assertEqual(trace['traceId'], trace_id) + + self.assertEqual(span['spanId'], str(span_id)) + self.assertEqual(span['name'], span_name) + + self.assertEqual( + span['startTime'], start_time) + self.assertEqual( + span['endTime'], end_time) + self.assertEqual(span['kind'], span_kind) + self.assertEqual(span['parentSpanId'], str(parent_span_id)) + self.assertEqual(span['labels'], labels) + + call_args = gax_api.list_traces.call_args[1] + + self.assertEqual(call_args['project_id'], self.project) + self.assertEqual(call_args['view'], view_type) + self.assertEqual(call_args['page_size'], size) + self.assertIsNone(call_args['start_time']) + self.assertIsNone(call_args['end_time']) + self.assertIsNone(call_args['filter_']) + self.assertIsNone(call_args['order_by']) + self.assertEqual(call_args['options'].page_token, token) + + +class Test__parse_trace_pb(unittest.TestCase): + + @staticmethod + def _call_fut(*args, **kwargs): + from google.cloud.trace._gax import _parse_trace_pb + + return _parse_trace_pb(*args, **kwargs) + + def test_registered_type(self): + from google.cloud.proto.devtools.cloudtrace.v1.trace_pb2 import ( + TraceSpan, Trace) + from google.protobuf.timestamp_pb2 import Timestamp + + project = u'PROJECT' + trace_id = u'test_trace_id' + span_id = 1234 + span_name = u'test_span_name' + start_time = '2017-06-24T00:12:50.369990Z' + end_time = '2017-06-24T00:13:39.633255Z' + start_seconds = 1498263170 + start_nanos = 369990000 + end_seconds = 1498263219 + end_nanos = 633255000 + + start_time_pb = Timestamp(seconds=start_seconds, nanos=start_nanos) + end_time_pb = Timestamp(seconds=end_seconds, nanos=end_nanos) + + span_pb = TraceSpan( + span_id=span_id, + name=span_name, + start_time=start_time_pb, + end_time=end_time_pb) + + trace_pb = Trace( + project_id=project, + trace_id=trace_id, + spans=[span_pb]) + + parse_result = self._call_fut(trace_pb) + + expected_result = { + 'projectId': project, + 'traceId': trace_id, + 'spans': [ + { + 'spanId': str(span_id), + 'name': span_name, + 'startTime': start_time, + 'endTime': end_time, + }, + ], + } + + self.assertEqual(parse_result, expected_result) + + @mock.patch('google.cloud.trace._gax.MessageToDict', + side_effect=TypeError) + def test_unregistered_type(self, msg_to_dict_mock): + trace_pb = mock.Mock(spec=['HasField']) + trace_pb.HasField.return_value = False + with self.assertRaises(TypeError): + self._call_fut(trace_pb) + + +class Test_make_gax_trace_api(unittest.TestCase): + + def _call_fut(self, client): + from google.cloud.trace._gax import make_gax_trace_api + + return make_gax_trace_api(client) + + def test_it(self): + from google.cloud.trace._gax import _TraceAPI + from google.cloud._http import DEFAULT_USER_AGENT + + credentials = object() + client = mock.Mock(_credentials=credentials, spec=['_credentials']) + channels = [] + channel_args = [] + generated_api_kwargs = [] + channel_obj = object() + generated = object() + + def make_channel(*args): + channel_args.append(args) + return channel_obj + + def generated_api(channel=None, **kwargs): + channels.append(channel) + generated_api_kwargs.append(kwargs) + return generated + + host = 'foo.apis.invalid' + generated_api.SERVICE_ADDRESS = host + + patch_channel = mock.patch( + 'google.cloud.trace._gax.make_secure_channel', + new=make_channel) + + patch_api = mock.patch( + 'google.cloud.trace._gax.trace_service_client.TraceServiceClient', + new=generated_api) + + with patch_api: + with patch_channel: + trace_api = self._call_fut(client) + + self.assertEqual(channels, [channel_obj]) + self.assertEqual(channel_args, + [(credentials, DEFAULT_USER_AGENT, host)]) + + self.assertEqual(len(generated_api_kwargs), 1) + self.assertEqual(generated_api_kwargs[0]['lib_name'], 'gccl') + + self.assertIsInstance(trace_api, _TraceAPI) + self.assertIs(trace_api._gax_api, generated) + self.assertIs(trace_api.client, client) diff --git a/trace/tests/unit/test_client.py b/trace/tests/unit/test_client.py new file mode 100644 index 000000000000..39193cbec69a --- /dev/null +++ b/trace/tests/unit/test_client.py @@ -0,0 +1,278 @@ +# 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 + + +def _make_credentials(): + import google.auth.credentials + + return mock.Mock(spec=google.auth.credentials.Credentials) + + +class TestClient(unittest.TestCase): + + project = 'PROJECT' + + @staticmethod + def _get_target_class(): + from google.cloud.trace.client import Client + + return Client + + def _make_one(self, *args, **kw): + return self._get_target_class()(*args, **kw) + + def test_constructor(self): + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + self.assertEqual(client.project, self.project) + + def test_trace_api(self): + clients = [] + api_obj = object() + + def make_api(client_obj): + clients.append(client_obj) + return api_obj + + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + + patch = mock.patch( + 'google.cloud.trace.client.make_gax_trace_api', + new=make_api) + + with patch: + api = client.trace_api + + self.assertIs(api, api_obj) + self.assertEqual(clients, [client]) + + def test_trace_default(self): + from google.cloud.trace.trace import Trace + + trace_id = '5e6e73b4131303cb6f5c9dfbaf104e33' + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + result_trace = client.trace(trace_id=trace_id) + + self.assertIsInstance(result_trace, Trace) + self.assertIs(result_trace.client, client) + self.assertEqual(result_trace.project_id, self.project) + self.assertEqual(result_trace.trace_id, trace_id) + + def test_trace_explicit(self): + from google.cloud.trace.trace import Trace + + trace_id = '5e6e73b4131303cb6f5c9dfbaf104e33' + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + result_trace = client.trace(project_id=self.project, trace_id=trace_id) + + self.assertIsInstance(result_trace, Trace) + self.assertIs(result_trace.client, client) + self.assertEqual(result_trace.project_id, self.project) + self.assertEqual(result_trace.trace_id, trace_id) + + def test_patch_traces_default(self): + from google.cloud.trace._gax import _TraceAPI + + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + traces = 'fake_traces_for_test' + + mock_trace_api = mock.Mock(spec=_TraceAPI) + mock_trace_api.patch_traces = mock.Mock() + patch = mock.patch('google.cloud.trace.client.make_gax_trace_api', return_value=mock_trace_api) + + with patch: + client.patch_traces(traces=traces) + + mock_trace_api.patch_traces.assert_called_with( + options=None, + project_id='PROJECT', + traces='fake_traces_for_test') + + def test_patch_traces_explicit(self): + from google.cloud.trace._gax import _TraceAPI + + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + traces = 'fake_traces_for_test' + + mock_trace_api = mock.Mock(spec=_TraceAPI) + mock_trace_api.patch_traces = mock.Mock() + patch = mock.patch('google.cloud.trace.client.make_gax_trace_api', return_value=mock_trace_api) + + with patch: + client.patch_traces( + project_id=self.project, + traces=traces) + + mock_trace_api.patch_traces.assert_called_with( + options=None, + project_id='PROJECT', + traces='fake_traces_for_test') + + def test_get_trace_default(self): + from google.cloud.trace._gax import _TraceAPI + + def get_trace(trace_id, project_id=None, options=None): + _get_trace_called_with = (trace_id, project_id, options) + return _get_trace_called_with + + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + trace_id = '5e6e73b4131303cb6f5c9dfbaf104e33' + + mock_trace_api = mock.Mock(spec=_TraceAPI) + mock_trace_api.get_trace = get_trace + patch = mock.patch('google.cloud.trace.client.make_gax_trace_api', + return_value=mock_trace_api) + + with patch: + get_trace_called_with = client.get_trace(trace_id=trace_id) + + self.assertEqual(get_trace_called_with, + (trace_id, self.project, None)) + + def test_get_trace_explicit(self): + from google.cloud.trace._gax import _TraceAPI + + def get_trace(trace_id, project_id=None, options=None): + _get_trace_called_with = (trace_id, project_id, options) + return _get_trace_called_with + + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + trace_id = '5e6e73b4131303cb6f5c9dfbaf104e33' + + mock_trace_api = mock.Mock(spec=_TraceAPI) + mock_trace_api.get_trace = get_trace + patch = mock.patch('google.cloud.trace.client.make_gax_trace_api', + return_value=mock_trace_api) + + with patch: + get_trace_called_with = client.get_trace( + trace_id=trace_id, + project_id=self.project) + + self.assertEqual(get_trace_called_with, + (trace_id, self.project, None)) + + def test_list_traces_default(self): + from google.cloud.trace._gax import _TraceAPI + + def list_traces( + project_id, + view=None, + page_size=None, + start_time=None, + end_time=None, + filter_=None, + order_by=None, + page_token=None): + _list_traces_called_with = ( + project_id, + view, + page_size, + start_time, + end_time, + filter_, + order_by, + page_token) + return _list_traces_called_with + + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + + mock_trace_api = mock.Mock(spec=_TraceAPI) + mock_trace_api.list_traces = list_traces + patch = mock.patch('google.cloud.trace.client.make_gax_trace_api', + return_value=mock_trace_api) + + with patch: + list_traces_called_with = client.list_traces() + + self.assertEqual(list_traces_called_with, ( + self.project, + None, None, None, None, None, None, None)) + + def test_list_traces_explicit(self): + from google.cloud._helpers import _datetime_to_pb_timestamp + from google.cloud.gapic.trace.v1.enums import ListTracesRequest as Enum + from google.cloud.trace._gax import _TraceAPI + + from datetime import datetime + + def list_traces( + project_id, + view=None, + page_size=None, + start_time=None, + end_time=None, + filter_=None, + order_by=None, + page_token=None): + _list_traces_called_with = ( + project_id, + view, + page_size, + start_time, + end_time, + filter_, + order_by, + page_token) + return _list_traces_called_with + + credentials = _make_credentials() + client = self._make_one(project=self.project, credentials=credentials) + + mock_trace_api = mock.Mock(spec=_TraceAPI) + mock_trace_api.list_traces = list_traces + patch = mock.patch('google.cloud.trace.client.make_gax_trace_api', + return_value=mock_trace_api) + + view = Enum.ViewType.COMPLETE + page_size = 10 + start_time = datetime.utcnow() + end_time = datetime.utcnow() + filter_ = '+span:span1' + order_by = 'traceId' + page_token = 'TOKEN' + + + with patch: + list_traces_called_with = client.list_traces( + project_id=self.project, + view=view, + page_size=page_size, + start_time=start_time, + end_time=end_time, + filter_=filter_, + order_by=order_by, + page_token=page_token) + + self.assertEqual(list_traces_called_with, ( + self.project, + view, + page_size, + _datetime_to_pb_timestamp(start_time), + _datetime_to_pb_timestamp(end_time), + filter_, + order_by, + page_token)) diff --git a/trace/tests/unit/test_trace.py b/trace/tests/unit/test_trace.py new file mode 100644 index 000000000000..3b50f1563174 --- /dev/null +++ b/trace/tests/unit/test_trace.py @@ -0,0 +1,216 @@ +# 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 google.cloud.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' + + client = mock.Mock(project=self.project, spec=['project']) + patch = mock.patch( + 'google.cloud.trace.trace.generate_trace_id', + return_value=trace_id) + + with patch: + trace = self._make_one(client) + + self.assertIs(trace.client, client) + self.assertEqual(trace.project_id, self.project) + self.assertEqual(trace.trace_id, trace_id) + + def test_constructor_explicit(self): + trace_id = 'test_trace_id' + + client = mock.Mock(project=self.project, spec=['project']) + trace = self._make_one( + client=client, + project_id=self.project, + trace_id=trace_id) + + self.assertIs(trace.client, client) + self.assertEqual(trace.project_id, self.project) + self.assertEqual(trace.trace_id, trace_id) + + def test_start(self): + client = object() + trace = self._make_one(client=client, project_id=self.project) + trace.start() + + self.assertEqual(trace.spans, []) + + def test_finish_with_valid_span(self): + from google.cloud.gapic.trace.v1.enums import TraceSpan as Enum + from google.cloud.trace.trace_span import TraceSpan + + def patch_traces(traces, project_id=None, options=None): + _patch_traces_called_with = (traces, project_id, options) + return _patch_traces_called_with + + client = mock.Mock(project=self.project, spec=['project']) + client.patch_traces = patch_traces + trace = self._make_one(client=client) + + 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 google.cloud.trace.trace_span import TraceSpan + + span_name = 'test_span_name' + + client = object() + trace = self._make_one(client=client, 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): + client = mock.Mock(project=self.project, spec=['project']) + trace_id = 'test_trace_id' + trace = self._make_one(client=client, trace_id=trace_id) + trace.spans = [] + + trace.send() + + self.assertFalse(client.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 google.cloud.gapic.trace.v1.enums import TraceSpan as Enum + from google.cloud.trace.client import Client + from google.cloud.trace.trace_span import TraceSpan + + client = mock.Mock(spec=Client) + client.project = self.project + trace_id = 'test_trace_id' + trace = self._make_one(client=client, trace_id=trace_id) + 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() + + client.patch_traces.assert_called_with(project_id=self.project, + traces=traces, + options=None) + + 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 000000000000..a56a2e3d8063 --- /dev/null +++ b/trace/tests/unit/test_trace_span.py @@ -0,0 +1,153 @@ +# 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 google.cloud.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 google.cloud.gapic.trace.v1.enums import TraceSpan as Enum + + span_id = 'test_span_id' + span_name = 'test_span_name' + + patch = mock.patch( + 'google.cloud.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.assertIsNone(span.labels) + self.assertIsNone(span.start_time) + self.assertIsNone(span.end_time) + self.assertEqual(span.children, []) + + def test_constructor_explicit(self): + from google.cloud.gapic.trace.v1.enums import TraceSpan as 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 google.cloud.gapic.trace.v1.enums import TraceSpan as 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( + 'google.cloud.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.assertIsNone(result_child_span.labels) + self.assertIsNone(result_child_span.start_time) + self.assertIsNone(result_child_span.end_time) + + def test_set_start_time(self): + span_name = 'root_span' + span = self._make_one(span_name) + self.assertIsNone(span.start_time) + + span.set_start_time() + self.assertIsNotNone(span.start_time) + + def test_set_end_time(self): + span_name = 'root_span' + span = self._make_one(span_name) + self.assertIsNone(span.end_time) + + span.set_end_time() + 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])