From 75e365311f97ecdbb1938772d6a9f29a9a5f0603 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Wed, 13 Nov 2019 23:44:19 +0800 Subject: [PATCH 01/12] Add stackdriver trace exporter TODO: - add GCP/AWS/Azure labels if planned - increase test coverage Test on: - examples/client.py and examples/server.py - https://github.com/open-telemetry/opentelemetry-go/tree/master/example/http-stackdriver --- ext/opentelemetry-ext-stackdriver/README.rst | 47 +++++ .../examples/client.py | 32 ++++ .../examples/server.py | 44 +++++ .../examples/trace.py | 25 +++ ext/opentelemetry-ext-stackdriver/setup.cfg | 48 +++++ ext/opentelemetry-ext-stackdriver/setup.py | 26 +++ .../opentelemetry/ext/stackdriver/__init__.py | 134 ++++++++++++++ .../opentelemetry/ext/stackdriver/utils.py | 174 ++++++++++++++++++ .../opentelemetry/ext/stackdriver/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_stackdriver_exporter.py | 120 ++++++++++++ scripts/coverage.sh | 1 + tox.ini | 3 + 13 files changed, 669 insertions(+) create mode 100644 ext/opentelemetry-ext-stackdriver/README.rst create mode 100644 ext/opentelemetry-ext-stackdriver/examples/client.py create mode 100644 ext/opentelemetry-ext-stackdriver/examples/server.py create mode 100644 ext/opentelemetry-ext-stackdriver/examples/trace.py create mode 100644 ext/opentelemetry-ext-stackdriver/setup.cfg create mode 100644 ext/opentelemetry-ext-stackdriver/setup.py create mode 100644 ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py create mode 100644 ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/utils.py create mode 100644 ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/version.py create mode 100644 ext/opentelemetry-ext-stackdriver/tests/__init__.py create mode 100644 ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py diff --git a/ext/opentelemetry-ext-stackdriver/README.rst b/ext/opentelemetry-ext-stackdriver/README.rst new file mode 100644 index 00000000000..928a51ca92e --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/README.rst @@ -0,0 +1,47 @@ +OpenTelemetry Stackdriver Exporters +===================================== + +This library provides integration with Google Cloud Stackdriver. + +Installation +------------ + +:: + + pip install opentelemetry-ext-stackdriver + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext import stackdriver + from opentelemetry.sdk.trace import Tracer + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + trace.set_preferred_tracer_implementation(lambda T: Tracer()) + tracer = trace.tracer() + + # create a StackdriverSpanExporter + stackdriver_exporter = stackdriver.StackdriverSpanExporter( + project_id='my-helloworld-project', + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(stackdriver_exporter) + + # add to the tracer + tracer.add_span_processor(span_processor) + + with tracer.start_as_current_span('foo'): + print('Hello world!') + + # shutdown the span processor + span_processor.shutdown() + +References +---------- + +* `Stackdriver `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-stackdriver/examples/client.py b/ext/opentelemetry-ext-stackdriver/examples/client.py new file mode 100644 index 00000000000..0b882913166 --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/examples/client.py @@ -0,0 +1,32 @@ +# Copyright 2019, OpenTelemetry Authors +# +# 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 requests + +from opentelemetry import trace +from opentelemetry.ext import http_requests +from opentelemetry.ext.stackdriver import StackdriverSpanExporter +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() +span_processor = SimpleExportSpanProcessor( + StackdriverSpanExporter(project_id="alansandbox") +) +tracer.add_span_processor(span_processor) + +http_requests.enable(tracer) +response = requests.get(url="http://localhost:7777/hello") +span_processor.shutdown() diff --git a/ext/opentelemetry-ext-stackdriver/examples/server.py b/ext/opentelemetry-ext-stackdriver/examples/server.py new file mode 100644 index 00000000000..e4870d9d9d3 --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/examples/server.py @@ -0,0 +1,44 @@ +# Copyright 2019, OpenCensus Authors +# +# 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 flask +import requests + +from opentelemetry import trace +from opentelemetry.ext import http_requests +from opentelemetry.ext.stackdriver import StackdriverSpanExporter +from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +trace.set_preferred_tracer_implementation(lambda T: Tracer()) + +span_processor = BatchExportSpanProcessor(StackdriverSpanExporter()) +http_requests.enable(trace.tracer()) +trace.tracer().add_span_processor(span_processor) + +app = flask.Flask(__name__) +app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) + + +@app.route("/") +def hello(): + with trace.tracer().start_as_current_span("parent"): + requests.get("https://www.wikipedia.org/wiki/Rabbit") + return "hello" + + +if __name__ == "__main__": + app.run(debug=True) + span_processor.shutdown() diff --git a/ext/opentelemetry-ext-stackdriver/examples/trace.py b/ext/opentelemetry-ext-stackdriver/examples/trace.py new file mode 100644 index 00000000000..9e0ef163e19 --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/examples/trace.py @@ -0,0 +1,25 @@ +# Copyright 2019, OpenCensus Authors +# +# 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 opentelemetry import trace +from opentelemetry.ext.stackdriver import StackdriverSpanExporter +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() +tracer.add_span_processor(SimpleExportSpanProcessor(StackdriverSpanExporter())) + +with tracer.start_as_current_span("hello") as span: + print("Hello, World!") diff --git a/ext/opentelemetry-ext-stackdriver/setup.cfg b/ext/opentelemetry-ext-stackdriver/setup.cfg new file mode 100644 index 00000000000..e6765438eb6 --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/setup.cfg @@ -0,0 +1,48 @@ +# Copyright 2019, OpenTelemetry Authors +# +# 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. +# +[metadata] +name = opentelemetry-ext-stackdriver +description = Stackdriver integration for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-stackdriver +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api + opentelemetry-sdk + google-cloud-monitoring + google-cloud-trace + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-stackdriver/setup.py b/ext/opentelemetry-ext-stackdriver/setup.py new file mode 100644 index 00000000000..8d43c44ffde --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# 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 setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "stackdriver", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py new file mode 100644 index 00000000000..e21286e04bb --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py @@ -0,0 +1,134 @@ +# Copyright 2018, OpenCensus Authors +# Copyright 2019, OpenTelemetry Authors +# +# 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. + +"""Stackdriver Span Exporter for OpenTelemetry.""" + +import base64 +import datetime +import logging +import socket + +from google.cloud.trace import trace_service_client +from google.cloud.trace.client import Client +from google.cloud.trace_v2.proto import trace_pb2 + +import opentelemetry.trace as trace_api +from opentelemetry.context import Context +from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult +from opentelemetry.sdk.util import ns_to_iso_str + +from .utils import ( + extract_attributes, + extract_events, + extract_links, + extract_status, + get_truncatable_str, + map_attributes, +) +from .version import __version__ + +logger = logging.getLogger(__name__) + +AGENT = "opentelemetry-python [{}]".format(__version__) + + +class StackdriverSpanExporter(SpanExporter): + """Stackdriver span exporter for OpenTelemetry. + + Args: + client: Stackdriver Trace client. + project_id: project_id to create the Trace client. + """ + + def __init__( + self, client=None, project_id=None, + ): + if client is None: + client = Client(project=project_id) + # initialize a authed client to prevent recorded in span + client.batch_write_spans( + "projects/{}".format(project_id), {"spans": []} + ) + self.client = client + self.project_id = self.client.project + + def export(self, spans: Span): + """Export the spans to Stackdriver. + + Args: + spans: Tuple of spans to export + """ + stackdriver_spans = self.translate_to_stackdriver(spans) + + self.client.batch_write_spans( + "projects/{}".format(self.project_id), {"spans": stackdriver_spans} + ) + + return SpanExportResult.SUCCESS + + def translate_to_stackdriver(self, spans: Span): + """Translate the spans to Stackdriver format. + + Args: + spans: Tuple of spans to convert + """ + + stackdriver_spans = [] + + for span in spans: + ctx = span.get_context() + trace_id = "{:032x}".format(ctx.trace_id) + span_id = "{:016x}".format(ctx.span_id) + + parent_id = None + if isinstance(span.parent, trace_api.Span): + parent_id = "{:016x}".format(span.parent.get_context().span_id) + elif isinstance(span.parent, trace_api.SpanContext): + parent_id = "{:016x}".format(span.parent.span_id) + + span_name = "projects/{}/traces/{}/spans/{}".format( + self.project_id, trace_id, span_id + ) + + span.attributes["g.co/agent"] = AGENT + attr_map = extract_attributes(span.attributes) + formatted_time_events = extract_events(span.events) + + sd_span = { + "name": span_name, + "spanId": span_id, + "parentSpanId": parent_id, + "displayName": get_truncatable_str(span.name), + "attributes": map_attributes(attr_map), + "links": extract_links(span.links), + "status": extract_status(span.status), + } + sd_span["parentSpanId"]: parent_id + sd_span["timeEvents"]: { + "timeEvent": formatted_time_events + } if formatted_time_events else None + sd_span["startTime"] = ( + ns_to_iso_str(span.start_time) if span.start_time else None + ) + sd_span["endTime"] = ( + ns_to_iso_str(span.end_time) if span.end_time else None + ) + + stackdriver_spans.append(sd_span) + + return stackdriver_spans + + def shutdown(self): + pass diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/utils.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/utils.py new file mode 100644 index 00000000000..7de7d815db1 --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/utils.py @@ -0,0 +1,174 @@ +# Copyright 2018, OpenCensus Authors +# Copyright 2019, OpenTelemetry Authors +# +# 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 datetime +import logging + +import opentelemetry.trace as trace_api +from opentelemetry.sdk.util import ns_to_iso_str +from opentelemetry.util import types + +# Max length is 128 bytes for a truncatable string. +MAX_LENGTH = 128 + + +def get_truncatable_str(str_to_convert): + """Truncate a string if exceed limit and record the truncated bytes + count. + """ + truncated, truncated_byte_count = check_str_length( + str_to_convert, MAX_LENGTH + ) + + result = { + "value": truncated, + "truncated_byte_count": truncated_byte_count, + } + return result + + +def check_str_length(str_to_check, limit=MAX_LENGTH): + """Check the length of a string. If exceeds limit, then truncate it. + """ + str_bytes = str_to_check.encode("utf-8") + str_len = len(str_bytes) + truncated_byte_count = 0 + + if str_len > limit: + truncated_byte_count = str_len - limit + str_bytes = str_bytes[:limit] + + result = str(str_bytes.decode("utf-8", errors="ignore")) + + return (result, truncated_byte_count) + + +def extract_status(status: trace_api.Status): + """Convert a Status object to dict.""" + status_json = {"details": None} + + status_json["code"] = status.canonical_code.value + + if status.description is not None: + status_json["message"] = status.description + + return status_json + + +def extract_links(links): + """Convert span.links to set.""" + if not links: + return None + + links = [] + for link in links: + trace_id = link.context.trace_id + span_id = link.context.span_id + links.append( + {trace_id: trace_id, span_id: span_id, type: "CHILD_LINKED_SPAN"} + ) + return set(links) + + +def extract_events(events): + """Convert span.events to dict.""" + if not events: + return None + + logs = [] + + for event in events: + annotation_json = {"description": get_truncatable_str(event.name)} + if event.attributes is not None: + annotation_json["attributes"] = extract_attributes( + event.attributes + ) + + logs.append( + { + "time": ns_to_iso_str(event.timestamp), + "annotation": annotation_json, + } + ) + return logs + + +def extract_attributes(attrs: types.Attributes): + """Convert span.attributes to dict.""" + attributes_json = {} + + for key, value in attrs.items(): + key = check_str_length(key)[0] + value = _format_attribute_value(value) + + if value is not None: + attributes_json[key] = value + + result = {"attributeMap": attributes_json} + + return result + + +def map_attributes(attribute_map): + """Convert the attributes to stackdriver attributes.""" + if attribute_map is None: + return attribute_map + for (key, value) in attribute_map.items(): + if key != "attributeMap": + continue + for attribute_key in list(value.keys()): + if attribute_key in ATTRIBUTE_MAPPING: + new_key = ATTRIBUTE_MAPPING.get(attribute_key) + value[new_key] = value.pop(attribute_key) + return attribute_map + + +def _format_attribute_value(value): + if isinstance(value, bool): + value_type = "bool_value" + elif isinstance(value, int): + value_type = "int_value" + elif isinstance(value, str): + value_type = "string_value" + value = get_truncatable_str(value) + elif isinstance(value, float): + value_type = "double_value" + else: + return None + + return {value_type: value} + + +ATTRIBUTE_MAPPING = { + "component": "/component", + "error.message": "/error/message", + "error.name": "/error/name", + "http.client_city": "/http/client_city", + "http.client_country": "/http/client_country", + "http.client_protocol": "/http/client_protocol", + "http.client_region": "/http/client_region", + "http.host": "/http/host", + "http.method": "/http/method", + "http.redirected_url": "/http/redirected_url", + "http.request_size": "/http/request/size", + "http.response_size": "/http/response/size", + "http.status_code": "/http/status_code", + "http.url": "/http/url", + "http.user_agent": "/http/user_agent", + "pid": "/pid", + "stacktrace": "/stacktrace", + "tid": "/tid", + "grpc.host_port": "/grpc/host_port", + "grpc.method": "/grpc/method", +} diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/version.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/version.py new file mode 100644 index 00000000000..93ef792d051 --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# 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. + +__version__ = "0.3.dev0" diff --git a/ext/opentelemetry-ext-stackdriver/tests/__init__.py b/ext/opentelemetry-ext-stackdriver/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py b/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py new file mode 100644 index 00000000000..1025f610e86 --- /dev/null +++ b/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py @@ -0,0 +1,120 @@ +# Copyright 2017, OpenCensus Authors +# Copyright 2019, OpenTelemetry Authors +# +# 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 +from unittest import mock + +import opentelemetry.ext.stackdriver as sd_exporter +from opentelemetry.sdk.trace import Span +from opentelemetry.trace import SpanContext, SpanKind +from opentelemetry.util.version import __version__ + + +class _Client(object): + def __init__(self, project=None): + if project is None: + project = "PROJECT" + + self.project = project + + def batch_write_spans(self, name, spans): + pass + + +class TestStackdriverSpanExporter(unittest.TestCase): + def test_constructor_default(self): + patch = mock.patch("opentelemetry.ext.stackdriver.Client", new=_Client) + + with patch: + exporter = sd_exporter.StackdriverSpanExporter() + + project_id = "PROJECT" + self.assertEqual(exporter.project_id, project_id) + + def test_constructor_explicit(self): + client = mock.Mock() + project_id = "PROJECT" + client.project = project_id + + exporter = sd_exporter.StackdriverSpanExporter( + client=client, project_id=project_id + ) + + self.assertIs(exporter.client, client) + self.assertEqual(exporter.project_id, project_id) + + def test_export(self): + trace_id = "6e0c63257de34c92bf9efcd03927272e" + span_id = "95bb5edabd45950f" + # start_times = 683647322 * 10 ** 9 # in ns + # durations = 50 * 10 ** 6 + # end_times = start_times + durations + span_datas = [ + Span( + name="span_name", + context=SpanContext( + trace_id=int(trace_id, 16), span_id=int(span_id, 16) + ), + parent=None, + kind=SpanKind.INTERNAL, + ) + ] + + stackdriver_spans = { + "spans": [ + { + "name": "projects/PROJECT/traces/{}/spans/{}".format( + trace_id, span_id + ), + "spanId": span_id, + "parentSpanId": None, + "displayName": { + "value": "span_name", + "truncated_byte_count": 0, + }, + "attributes": { + "attributeMap": { + "g.co/agent": { + "string_value": { + "value": "opentelemetry-python [{}]".format( + __version__ + ), + "truncated_byte_count": 0, + } + } + } + }, + "links": None, + "status": {"details": None, "code": 0}, + "startTime": None, + "endTime": None, + } + ] + } + + client = mock.Mock() + project_id = "PROJECT" + client.project = project_id + + exporter = sd_exporter.StackdriverSpanExporter( + client=client, project_id=project_id + ) + + exporter.export(span_datas) + + name = "projects/{}".format(project_id) + + client.batch_write_spans.assert_called_with(name, stackdriver_spans) + self.assertTrue(client.batch_write_spans.called) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index bddf39a90cd..d93a9fa9f83 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -20,6 +20,7 @@ cov opentelemetry-sdk cov ext/opentelemetry-ext-http-requests cov ext/opentelemetry-ext-jaeger cov ext/opentelemetry-ext-opentracing-shim +cov ext/opentelemetry-ext-stackdriver cov ext/opentelemetry-ext-wsgi cov examples/opentelemetry-example-app diff --git a/tox.ini b/tox.ini index e0e076fe77d..04a33e9a5ca 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,7 @@ changedir = test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests + test-ext-stackdriver: ext/opentelemetry-ext-stackdriver/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-example-app: examples/opentelemetry-example-app/tests test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests @@ -62,6 +63,7 @@ commands_pre = coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim + coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-stackdriver coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi coverage: pip install -e {toxinidir}/examples/opentelemetry-example-app @@ -97,6 +99,7 @@ commands_pre = pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger pip install -e {toxinidir}/ext/opentelemetry-ext-pymongo + pip install -e {toxinidir}/ext/opentelemetry-ext-stackdriver pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/examples/opentelemetry-example-app pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim From 850d12a42da11a2fc636a615ec02203b8b7f7ce4 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Thu, 14 Nov 2019 10:16:46 +0800 Subject: [PATCH 02/12] pat py3.4 --- .../src/opentelemetry/ext/stackdriver/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py index e21286e04bb..60fef551089 100644 --- a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py @@ -115,7 +115,7 @@ def translate_to_stackdriver(self, spans: Span): "links": extract_links(span.links), "status": extract_status(span.status), } - sd_span["parentSpanId"]: parent_id + sd_span["parentSpanId"] = parent_id sd_span["timeEvents"]: { "timeEvent": formatted_time_events } if formatted_time_events else None From 3cbc0f2102615f2d75a50d93f530625940da6349 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Thu, 14 Nov 2019 10:26:46 +0800 Subject: [PATCH 03/12] pat py3.4 --- .../src/opentelemetry/ext/stackdriver/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py index 60fef551089..64da0abd123 100644 --- a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py @@ -116,7 +116,7 @@ def translate_to_stackdriver(self, spans: Span): "status": extract_status(span.status), } sd_span["parentSpanId"] = parent_id - sd_span["timeEvents"]: { + sd_span["timeEvents"] = { "timeEvent": formatted_time_events } if formatted_time_events else None sd_span["startTime"] = ( From 086d5eb50d47050bb1ac5314e9f1f28e0a5764c1 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Thu, 14 Nov 2019 11:16:19 +0800 Subject: [PATCH 04/12] missing item --- .../tests/test_stackdriver_exporter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py b/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py index 1025f610e86..027c8529a73 100644 --- a/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py +++ b/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py @@ -98,6 +98,7 @@ def test_export(self): }, "links": None, "status": {"details": None, "code": 0}, + "timeEvents": None, "startTime": None, "endTime": None, } From 3b437300d95cb45320256bdf3aef4e971a3d05ca Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Thu, 14 Nov 2019 11:23:29 +0800 Subject: [PATCH 05/12] black --- .../src/opentelemetry/ext/stackdriver/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py index 64da0abd123..7dc1787bd81 100644 --- a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py @@ -116,9 +116,11 @@ def translate_to_stackdriver(self, spans: Span): "status": extract_status(span.status), } sd_span["parentSpanId"] = parent_id - sd_span["timeEvents"] = { - "timeEvent": formatted_time_events - } if formatted_time_events else None + sd_span["timeEvents"] = ( + {"timeEvent": formatted_time_events} + if formatted_time_events + else None + ) sd_span["startTime"] = ( ns_to_iso_str(span.start_time) if span.start_time else None ) From 3fb85da05b1abf39bb477d5d7fa60b1d1d03b977 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 23 Nov 2019 19:19:41 +0800 Subject: [PATCH 06/12] fix by review --- ext/opentelemetry-ext-stackdriver/README.rst | 3 --- ext/opentelemetry-ext-stackdriver/examples/client.py | 2 +- ext/opentelemetry-ext-stackdriver/setup.cfg | 4 ++-- .../src/opentelemetry/ext/stackdriver/__init__.py | 7 +++++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ext/opentelemetry-ext-stackdriver/README.rst b/ext/opentelemetry-ext-stackdriver/README.rst index 928a51ca92e..319baa962b1 100644 --- a/ext/opentelemetry-ext-stackdriver/README.rst +++ b/ext/opentelemetry-ext-stackdriver/README.rst @@ -37,9 +37,6 @@ Usage with tracer.start_as_current_span('foo'): print('Hello world!') - # shutdown the span processor - span_processor.shutdown() - References ---------- diff --git a/ext/opentelemetry-ext-stackdriver/examples/client.py b/ext/opentelemetry-ext-stackdriver/examples/client.py index 0b882913166..8bc6d40db5c 100644 --- a/ext/opentelemetry-ext-stackdriver/examples/client.py +++ b/ext/opentelemetry-ext-stackdriver/examples/client.py @@ -23,7 +23,7 @@ trace.set_preferred_tracer_implementation(lambda T: Tracer()) tracer = trace.tracer() span_processor = SimpleExportSpanProcessor( - StackdriverSpanExporter(project_id="alansandbox") + StackdriverSpanExporter(project_id="my-helloworld-project") ) tracer.add_span_processor(span_processor) diff --git a/ext/opentelemetry-ext-stackdriver/setup.cfg b/ext/opentelemetry-ext-stackdriver/setup.cfg index e6765438eb6..d3307eae772 100644 --- a/ext/opentelemetry-ext-stackdriver/setup.cfg +++ b/ext/opentelemetry-ext-stackdriver/setup.cfg @@ -41,8 +41,8 @@ packages=find_namespace: install_requires = opentelemetry-api opentelemetry-sdk - google-cloud-monitoring - google-cloud-trace + google-cloud-monitoring + google-cloud-trace [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py index 7dc1787bd81..77915d6668b 100644 --- a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py @@ -19,6 +19,7 @@ import datetime import logging import socket +import typing from google.cloud.trace import trace_service_client from google.cloud.trace.client import Client @@ -64,7 +65,7 @@ def __init__( self.client = client self.project_id = self.client.project - def export(self, spans: Span): + def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: """Export the spans to Stackdriver. Args: @@ -78,7 +79,9 @@ def export(self, spans: Span): return SpanExportResult.SUCCESS - def translate_to_stackdriver(self, spans: Span): + def translate_to_stackdriver( + self, spans: typing.Sequence[Span] + ) -> typing.List[typing.Dict[str, Any]]: """Translate the spans to Stackdriver format. Args: From caa3847770effd483ee990d5b40c6efc6ca7c8c9 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 23 Nov 2019 19:48:43 +0800 Subject: [PATCH 07/12] enrich exception, remove unused imports --- .../ext/stackdriver/{ => trace}/__init__.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) rename ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/{ => trace}/__init__.py (90%) diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py similarity index 90% rename from ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py rename to ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py index 77915d6668b..bafe6e571e1 100644 --- a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py @@ -1,4 +1,3 @@ -# Copyright 2018, OpenCensus Authors # Copyright 2019, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,10 +14,7 @@ """Stackdriver Span Exporter for OpenTelemetry.""" -import base64 -import datetime import logging -import socket import typing from google.cloud.trace import trace_service_client @@ -68,14 +64,21 @@ def __init__( def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: """Export the spans to Stackdriver. + See: https://cloud.google.com/trace/docs/reference/v2/rest/v2/ + projects.traces/batchWrite + Args: spans: Tuple of spans to export """ stackdriver_spans = self.translate_to_stackdriver(spans) - self.client.batch_write_spans( - "projects/{}".format(self.project_id), {"spans": stackdriver_spans} - ) + try: + self.client.batch_write_spans( + "projects/{}".format(self.project_id), {"spans": stackdriver_spans} + ) + except Exception as ex: + logger.warning("Error while writing to stackdriver: %s", ex) + return SpanExportResult.FAILED_RETRYABLE return SpanExportResult.SUCCESS @@ -118,7 +121,6 @@ def translate_to_stackdriver( "links": extract_links(span.links), "status": extract_status(span.status), } - sd_span["parentSpanId"] = parent_id sd_span["timeEvents"] = ( {"timeEvent": formatted_time_events} if formatted_time_events From f88135be622c6bf411e398e1895069af78ef2d90 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 23 Nov 2019 20:18:14 +0800 Subject: [PATCH 08/12] refine test --- .../ext/stackdriver/trace/__init__.py | 195 +++++++++++++++--- .../opentelemetry/ext/stackdriver/utils.py | 174 ---------------- .../tests/test_stackdriver_exporter.py | 29 +-- 3 files changed, 181 insertions(+), 217 deletions(-) delete mode 100644 ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/utils.py diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py index bafe6e571e1..88e12ec302b 100644 --- a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py @@ -25,20 +25,15 @@ from opentelemetry.context import Context from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult from opentelemetry.sdk.util import ns_to_iso_str +from opentelemetry.util import types -from .utils import ( - extract_attributes, - extract_events, - extract_links, - extract_status, - get_truncatable_str, - map_attributes, -) -from .version import __version__ +from ..version import __version__ logger = logging.getLogger(__name__) AGENT = "opentelemetry-python [{}]".format(__version__) +# Max length is 128 bytes for a truncatable string. +MAX_LENGTH = 128 class StackdriverSpanExporter(SpanExporter): @@ -84,7 +79,7 @@ def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: def translate_to_stackdriver( self, spans: typing.Sequence[Span] - ) -> typing.List[typing.Dict[str, Any]]: + ) -> typing.List[typing.Dict[str, typing.Any]]: """Translate the spans to Stackdriver format. Args: @@ -97,6 +92,9 @@ def translate_to_stackdriver( ctx = span.get_context() trace_id = "{:032x}".format(ctx.trace_id) span_id = "{:016x}".format(ctx.span_id) + span_name = "projects/{}/traces/{}/spans/{}".format( + self.project_id, trace_id, span_id + ) parent_id = None if isinstance(span.parent, trace_api.Span): @@ -104,13 +102,17 @@ def translate_to_stackdriver( elif isinstance(span.parent, trace_api.SpanContext): parent_id = "{:016x}".format(span.parent.span_id) - span_name = "projects/{}/traces/{}/spans/{}".format( - self.project_id, trace_id, span_id - ) + start_time = None + if span.start_time: + start_time = ns_to_iso_str(span.start_time) + end_time = None + if span.end_time: + end_time = ns_to_iso_str(span.end_time) + + time_events = None span.attributes["g.co/agent"] = AGENT attr_map = extract_attributes(span.attributes) - formatted_time_events = extract_events(span.events) sd_span = { "name": span_name, @@ -120,18 +122,10 @@ def translate_to_stackdriver( "attributes": map_attributes(attr_map), "links": extract_links(span.links), "status": extract_status(span.status), + "timeEvents": extract_events(span.events), + "startTime": start_time, + "endTime": end_time, } - sd_span["timeEvents"] = ( - {"timeEvent": formatted_time_events} - if formatted_time_events - else None - ) - sd_span["startTime"] = ( - ns_to_iso_str(span.start_time) if span.start_time else None - ) - sd_span["endTime"] = ( - ns_to_iso_str(span.end_time) if span.end_time else None - ) stackdriver_spans.append(sd_span) @@ -139,3 +133,154 @@ def translate_to_stackdriver( def shutdown(self): pass + + +def get_truncatable_str(str_to_convert): + """Truncate a string if exceed limit and record the truncated bytes + count. + """ + truncated, truncated_byte_count = check_str_length( + str_to_convert, MAX_LENGTH + ) + + result = { + "value": truncated, + "truncated_byte_count": truncated_byte_count, + } + return result + + +def check_str_length(str_to_check, limit=MAX_LENGTH): + """Check the length of a string. If exceeds limit, then truncate it. + """ + str_bytes = str_to_check.encode("utf-8") + str_len = len(str_bytes) + truncated_byte_count = 0 + + if str_len > limit: + truncated_byte_count = str_len - limit + str_bytes = str_bytes[:limit] + + result = str(str_bytes.decode("utf-8", errors="ignore")) + + return (result, truncated_byte_count) + + +def extract_status(status: trace_api.Status): + """Convert a Status object to dict.""" + status_json = {"details": None} + + status_json["code"] = status.canonical_code.value + + if status.description is not None: + status_json["message"] = status.description + + return status_json + + +def extract_links(links): + """Convert span.links to set.""" + if not links: + return None + + links = [] + for link in links: + trace_id = link.context.trace_id + span_id = link.context.span_id + links.append( + {trace_id: trace_id, span_id: span_id, type: "CHILD_LINKED_SPAN"} + ) + return set(links) + + +def extract_events(events): + """Convert span.events to dict.""" + if not events: + return None + + logs = [] + + for event in events: + annotation_json = {"description": get_truncatable_str(event.name)} + if event.attributes is not None: + annotation_json["attributes"] = extract_attributes( + event.attributes + ) + + logs.append( + { + "time": ns_to_iso_str(event.timestamp), + "annotation": annotation_json, + } + ) + return {"timeEvent": logs} + + +def extract_attributes(attrs: types.Attributes): + """Convert span.attributes to dict.""" + attributes_json = {} + + for key, value in attrs.items(): + key = check_str_length(key)[0] + value = _format_attribute_value(value) + + if value is not None: + attributes_json[key] = value + + result = {"attributeMap": attributes_json} + + return result + + +def map_attributes(attribute_map): + """Convert the attributes to stackdriver attributes.""" + if attribute_map is None: + return attribute_map + for (key, value) in attribute_map.items(): + if key != "attributeMap": + continue + for attribute_key in list(value.keys()): + if attribute_key in ATTRIBUTE_MAPPING: + new_key = ATTRIBUTE_MAPPING.get(attribute_key) + value[new_key] = value.pop(attribute_key) + return attribute_map + + +def _format_attribute_value(value): + if isinstance(value, bool): + value_type = "bool_value" + elif isinstance(value, int): + value_type = "int_value" + elif isinstance(value, str): + value_type = "string_value" + value = get_truncatable_str(value) + elif isinstance(value, float): + value_type = "double_value" + else: + return None + + return {value_type: value} + + +ATTRIBUTE_MAPPING = { + "component": "/component", + "error.message": "/error/message", + "error.name": "/error/name", + "http.client_city": "/http/client_city", + "http.client_country": "/http/client_country", + "http.client_protocol": "/http/client_protocol", + "http.client_region": "/http/client_region", + "http.host": "/http/host", + "http.method": "/http/method", + "http.redirected_url": "/http/redirected_url", + "http.request_size": "/http/request/size", + "http.response_size": "/http/response/size", + "http.status_code": "/http/status_code", + "http.url": "/http/url", + "http.user_agent": "/http/user_agent", + "pid": "/pid", + "stacktrace": "/stacktrace", + "tid": "/tid", + "grpc.host_port": "/grpc/host_port", + "grpc.method": "/grpc/method", +} diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/utils.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/utils.py deleted file mode 100644 index 7de7d815db1..00000000000 --- a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/utils.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2018, OpenCensus Authors -# Copyright 2019, OpenTelemetry Authors -# -# 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 datetime -import logging - -import opentelemetry.trace as trace_api -from opentelemetry.sdk.util import ns_to_iso_str -from opentelemetry.util import types - -# Max length is 128 bytes for a truncatable string. -MAX_LENGTH = 128 - - -def get_truncatable_str(str_to_convert): - """Truncate a string if exceed limit and record the truncated bytes - count. - """ - truncated, truncated_byte_count = check_str_length( - str_to_convert, MAX_LENGTH - ) - - result = { - "value": truncated, - "truncated_byte_count": truncated_byte_count, - } - return result - - -def check_str_length(str_to_check, limit=MAX_LENGTH): - """Check the length of a string. If exceeds limit, then truncate it. - """ - str_bytes = str_to_check.encode("utf-8") - str_len = len(str_bytes) - truncated_byte_count = 0 - - if str_len > limit: - truncated_byte_count = str_len - limit - str_bytes = str_bytes[:limit] - - result = str(str_bytes.decode("utf-8", errors="ignore")) - - return (result, truncated_byte_count) - - -def extract_status(status: trace_api.Status): - """Convert a Status object to dict.""" - status_json = {"details": None} - - status_json["code"] = status.canonical_code.value - - if status.description is not None: - status_json["message"] = status.description - - return status_json - - -def extract_links(links): - """Convert span.links to set.""" - if not links: - return None - - links = [] - for link in links: - trace_id = link.context.trace_id - span_id = link.context.span_id - links.append( - {trace_id: trace_id, span_id: span_id, type: "CHILD_LINKED_SPAN"} - ) - return set(links) - - -def extract_events(events): - """Convert span.events to dict.""" - if not events: - return None - - logs = [] - - for event in events: - annotation_json = {"description": get_truncatable_str(event.name)} - if event.attributes is not None: - annotation_json["attributes"] = extract_attributes( - event.attributes - ) - - logs.append( - { - "time": ns_to_iso_str(event.timestamp), - "annotation": annotation_json, - } - ) - return logs - - -def extract_attributes(attrs: types.Attributes): - """Convert span.attributes to dict.""" - attributes_json = {} - - for key, value in attrs.items(): - key = check_str_length(key)[0] - value = _format_attribute_value(value) - - if value is not None: - attributes_json[key] = value - - result = {"attributeMap": attributes_json} - - return result - - -def map_attributes(attribute_map): - """Convert the attributes to stackdriver attributes.""" - if attribute_map is None: - return attribute_map - for (key, value) in attribute_map.items(): - if key != "attributeMap": - continue - for attribute_key in list(value.keys()): - if attribute_key in ATTRIBUTE_MAPPING: - new_key = ATTRIBUTE_MAPPING.get(attribute_key) - value[new_key] = value.pop(attribute_key) - return attribute_map - - -def _format_attribute_value(value): - if isinstance(value, bool): - value_type = "bool_value" - elif isinstance(value, int): - value_type = "int_value" - elif isinstance(value, str): - value_type = "string_value" - value = get_truncatable_str(value) - elif isinstance(value, float): - value_type = "double_value" - else: - return None - - return {value_type: value} - - -ATTRIBUTE_MAPPING = { - "component": "/component", - "error.message": "/error/message", - "error.name": "/error/name", - "http.client_city": "/http/client_city", - "http.client_country": "/http/client_country", - "http.client_protocol": "/http/client_protocol", - "http.client_region": "/http/client_region", - "http.host": "/http/host", - "http.method": "/http/method", - "http.redirected_url": "/http/redirected_url", - "http.request_size": "/http/request/size", - "http.response_size": "/http/response/size", - "http.status_code": "/http/status_code", - "http.url": "/http/url", - "http.user_agent": "/http/user_agent", - "pid": "/pid", - "stacktrace": "/stacktrace", - "tid": "/tid", - "grpc.host_port": "/grpc/host_port", - "grpc.method": "/grpc/method", -} diff --git a/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py b/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py index 027c8529a73..0af19d0b102 100644 --- a/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py +++ b/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py @@ -16,32 +16,25 @@ import unittest from unittest import mock -import opentelemetry.ext.stackdriver as sd_exporter +import opentelemetry.ext.stackdriver.trace as sd_exporter from opentelemetry.sdk.trace import Span from opentelemetry.trace import SpanContext, SpanKind from opentelemetry.util.version import __version__ -class _Client(object): - def __init__(self, project=None): - if project is None: - project = "PROJECT" - - self.project = project - - def batch_write_spans(self, name, spans): - pass - - class TestStackdriverSpanExporter(unittest.TestCase): - def test_constructor_default(self): - patch = mock.patch("opentelemetry.ext.stackdriver.Client", new=_Client) + def setUp(self): + self.client_patcher = mock.patch( + "opentelemetry.ext.stackdriver.trace.Client" + ) + self.client_patcher.start() - with patch: - exporter = sd_exporter.StackdriverSpanExporter() + def tearDown(self): + self.client_patcher.stop() - project_id = "PROJECT" - self.assertEqual(exporter.project_id, project_id) + def test_constructor_default(self): + exporter = sd_exporter.StackdriverSpanExporter() + self.assertEqual(exporter.project_id, exporter.client.project) def test_constructor_explicit(self): client = mock.Mock() From 54896efe6d7b7203b39026ddd08f11bfbe665442 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 23 Nov 2019 20:29:06 +0800 Subject: [PATCH 09/12] fix example --- ext/opentelemetry-ext-stackdriver/examples/client.py | 2 +- ext/opentelemetry-ext-stackdriver/examples/server.py | 2 +- ext/opentelemetry-ext-stackdriver/examples/trace.py | 2 +- .../src/opentelemetry/ext/stackdriver/trace/__init__.py | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ext/opentelemetry-ext-stackdriver/examples/client.py b/ext/opentelemetry-ext-stackdriver/examples/client.py index 8bc6d40db5c..ee6206d9470 100644 --- a/ext/opentelemetry-ext-stackdriver/examples/client.py +++ b/ext/opentelemetry-ext-stackdriver/examples/client.py @@ -16,7 +16,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests -from opentelemetry.ext.stackdriver import StackdriverSpanExporter +from opentelemetry.ext.stackdriver.trace import StackdriverSpanExporter from opentelemetry.sdk.trace import Tracer from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor diff --git a/ext/opentelemetry-ext-stackdriver/examples/server.py b/ext/opentelemetry-ext-stackdriver/examples/server.py index e4870d9d9d3..c001d94077f 100644 --- a/ext/opentelemetry-ext-stackdriver/examples/server.py +++ b/ext/opentelemetry-ext-stackdriver/examples/server.py @@ -17,7 +17,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests -from opentelemetry.ext.stackdriver import StackdriverSpanExporter +from opentelemetry.ext.stackdriver.trace import StackdriverSpanExporter from opentelemetry.ext.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.trace import Tracer from opentelemetry.sdk.trace.export import BatchExportSpanProcessor diff --git a/ext/opentelemetry-ext-stackdriver/examples/trace.py b/ext/opentelemetry-ext-stackdriver/examples/trace.py index 9e0ef163e19..29216c740d1 100644 --- a/ext/opentelemetry-ext-stackdriver/examples/trace.py +++ b/ext/opentelemetry-ext-stackdriver/examples/trace.py @@ -13,7 +13,7 @@ # limitations under the License. from opentelemetry import trace -from opentelemetry.ext.stackdriver import StackdriverSpanExporter +from opentelemetry.ext.stackdriver.trace import StackdriverSpanExporter from opentelemetry.sdk.trace import Tracer from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py index 88e12ec302b..6885eb16bc6 100644 --- a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py @@ -69,7 +69,8 @@ def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: try: self.client.batch_write_spans( - "projects/{}".format(self.project_id), {"spans": stackdriver_spans} + "projects/{}".format(self.project_id), + {"spans": stackdriver_spans}, ) except Exception as ex: logger.warning("Error while writing to stackdriver: %s", ex) @@ -109,8 +110,6 @@ def translate_to_stackdriver( if span.end_time: end_time = ns_to_iso_str(span.end_time) - time_events = None - span.attributes["g.co/agent"] = AGENT attr_map = extract_attributes(span.attributes) From 0dff8bc7be403c7b11cb0dae0d3c42386bda9faf Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 23 Nov 2019 21:11:43 +0800 Subject: [PATCH 10/12] fix README --- ext/opentelemetry-ext-stackdriver/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-stackdriver/README.rst b/ext/opentelemetry-ext-stackdriver/README.rst index 319baa962b1..fcf2a08e014 100644 --- a/ext/opentelemetry-ext-stackdriver/README.rst +++ b/ext/opentelemetry-ext-stackdriver/README.rst @@ -24,7 +24,7 @@ Usage tracer = trace.tracer() # create a StackdriverSpanExporter - stackdriver_exporter = stackdriver.StackdriverSpanExporter( + stackdriver_exporter = stackdriver.trace.StackdriverSpanExporter( project_id='my-helloworld-project', ) From 6b9b68ce721cc7e7014d8ae33c8aa9410706db7a Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 23 Nov 2019 21:11:57 +0800 Subject: [PATCH 11/12] remove span initialization in grpc --- .../src/opentelemetry/ext/stackdriver/trace/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py index 6885eb16bc6..6995cbe8633 100644 --- a/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py +++ b/ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/trace/__init__.py @@ -49,10 +49,6 @@ def __init__( ): if client is None: client = Client(project=project_id) - # initialize a authed client to prevent recorded in span - client.batch_write_spans( - "projects/{}".format(project_id), {"spans": []} - ) self.client = client self.project_id = self.client.project From e1b07e75d2fe7748c8b276de3d91c7b2ce8056ad Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Mon, 9 Dec 2019 14:36:39 +0800 Subject: [PATCH 12/12] remove OC header, file has changed a lot --- ext/opentelemetry-ext-stackdriver/examples/server.py | 2 +- ext/opentelemetry-ext-stackdriver/examples/trace.py | 2 +- .../tests/test_stackdriver_exporter.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ext/opentelemetry-ext-stackdriver/examples/server.py b/ext/opentelemetry-ext-stackdriver/examples/server.py index c001d94077f..8cd74a653e3 100644 --- a/ext/opentelemetry-ext-stackdriver/examples/server.py +++ b/ext/opentelemetry-ext-stackdriver/examples/server.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenCensus Authors +# Copyright 2019, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-stackdriver/examples/trace.py b/ext/opentelemetry-ext-stackdriver/examples/trace.py index 29216c740d1..1cdb41ecc0e 100644 --- a/ext/opentelemetry-ext-stackdriver/examples/trace.py +++ b/ext/opentelemetry-ext-stackdriver/examples/trace.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenCensus Authors +# Copyright 2019, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py b/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py index 0af19d0b102..d86ef4986a0 100644 --- a/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py +++ b/ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py @@ -1,4 +1,3 @@ -# Copyright 2017, OpenCensus Authors # Copyright 2019, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License");