diff --git a/ext/opentelemetry-ext-azure-monitor/README.rst b/ext/opentelemetry-ext-azure-monitor/README.rst index d8c3e16074..976d9a531e 100644 --- a/ext/opentelemetry-ext-azure-monitor/README.rst +++ b/ext/opentelemetry-ext-azure-monitor/README.rst @@ -3,6 +3,13 @@ OpenTelemetry Azure Monitor Exporters This library provides integration with Microsoft Azure Monitor. +Installation +------------ + +:: + + pip install opentelemetry-ext-azure-monitor + References ---------- diff --git a/ext/opentelemetry-ext-azure-monitor/examples/client.py b/ext/opentelemetry-ext-azure-monitor/examples/client.py new file mode 100644 index 0000000000..ff954788e6 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/examples/client.py @@ -0,0 +1,30 @@ +# 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 requests + +from opentelemetry import trace +from opentelemetry.ext import http_requests +from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter +from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() +http_requests.enable(tracer) +span_processor = BatchExportSpanProcessor(AzureMonitorSpanExporter()) +tracer.add_span_processor(span_processor) + +response = requests.get(url="http://127.0.0.1:5000/") +span_processor.shutdown() diff --git a/ext/opentelemetry-ext-azure-monitor/examples/server.py b/ext/opentelemetry-ext-azure-monitor/examples/server.py new file mode 100644 index 0000000000..54727ef737 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/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.azure_monitor import AzureMonitorSpanExporter +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()) + +http_requests.enable(trace.tracer()) +span_processor = BatchExportSpanProcessor(AzureMonitorSpanExporter()) +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_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-azure-monitor/examples/trace.py b/ext/opentelemetry-ext-azure-monitor/examples/trace.py index 49b38c051d..8e8f887aa1 100644 --- a/ext/opentelemetry-ext-azure-monitor/examples/trace.py +++ b/ext/opentelemetry-ext-azure-monitor/examples/trace.py @@ -1,3 +1,17 @@ +# 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.azure_monitor import AzureMonitorSpanExporter from opentelemetry.sdk.trace import Tracer diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py new file mode 100644 index 0000000000..ccdf5eef8d --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/protocol.py @@ -0,0 +1,201 @@ +# 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. + + +class BaseObject(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for key in kwargs: + self[key] = kwargs[key] + + def __repr__(self): + tmp = {} + current = self + while True: + for item in current.items(): + if item[0] not in tmp: + tmp[item[0]] = item[1] + if ( + current._default # noqa pylint: disable=protected-access + == current + ): + break + current = current._default # noqa pylint: disable=protected-access + return repr(tmp) + + def __setattr__(self, name, value): + self[name] = value + + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError( + "'{}' object has no attribute {}".format( + type(self).__name__, name + ) + ) + + def __getitem__(self, key): + if self._default is self: + return super().__getitem__(key) + if key in self: + return super().__getitem__(key) + return self._default[key] + + +BaseObject._default = BaseObject() # noqa pylint: disable=protected-access + + +class Data(BaseObject): + _default = BaseObject(baseData=None, baseType=None) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.baseData = self.baseData # noqa pylint: disable=invalid-name + self.baseType = self.baseType # noqa pylint: disable=invalid-name + + +class DataPoint(BaseObject): + _default = BaseObject( + ns="", + name="", + kind=None, + value=0.0, + count=None, + min=None, + max=None, + stdDev=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.name = self.name + self.value = self.value + + +class Envelope(BaseObject): + _default = BaseObject( + ver=1, + name="", + time="", + sampleRate=None, + seq=None, + iKey=None, + flags=None, + tags=None, + data=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.name = self.name + self.time = self.time + + +class Event(BaseObject): + _default = BaseObject(ver=2, name="", properties=None, measurements=None) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.name = self.name + + +class ExceptionData(BaseObject): + _default = BaseObject( + ver=2, + exceptions=[], + severityLevel=None, + problemId=None, + properties=None, + measurements=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.exceptions = self.exceptions + + +class Message(BaseObject): + _default = BaseObject( + ver=2, + message="", + severityLevel=None, + properties=None, + measurements=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.message = self.message + + +class MetricData(BaseObject): + _default = BaseObject(ver=2, metrics=[], properties=None) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.metrics = self.metrics + + +class RemoteDependency(BaseObject): + _default = BaseObject( + ver=2, + name="", + id="", + resultCode="", + duration="", + success=True, + data=None, + type=None, + target=None, + properties=None, + measurements=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.name = self.name + self.resultCode = self.resultCode # noqa pylint: disable=invalid-name + self.duration = self.duration + + +class Request(BaseObject): + _default = BaseObject( + ver=2, + id="", + duration="", + responseCode="", + success=True, + source=None, + name=None, + url=None, + properties=None, + measurements=None, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ver = self.ver + self.id = self.id # noqa pylint: disable=invalid-name + self.duration = self.duration + self.responseCode = ( # noqa pylint: disable=invalid-name + self.responseCode + ) + self.success = self.success diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py index a65cdd92a1..16f9252fd0 100644 --- a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/trace.py @@ -12,14 +12,162 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +import logging +from urllib.parse import urlparse + +import requests + +from opentelemetry.ext.azure_monitor import protocol, util from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.sdk.util import ns_to_iso_str +from opentelemetry.trace import Span, SpanKind + +logger = logging.getLogger(__name__) class AzureMonitorSpanExporter(SpanExporter): - def __init__(self): - pass + def __init__(self, **options): + self.options = util.Options(**options) + if not self.options.instrumentation_key: + raise ValueError("The instrumentation_key is not provided.") def export(self, spans): - for span in spans: - print(span) # TODO: add actual implementation here - return SpanExportResult.SUCCESS + envelopes = tuple(map(self.span_to_envelope, spans)) + + try: + response = requests.post( + url=self.options.endpoint, + data=json.dumps(envelopes), + headers={ + "Accept": "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + timeout=self.options.timeout, + ) + except requests.RequestException as ex: + logger.warning("Transient client side error %s.", ex) + return SpanExportResult.FAILED_RETRYABLE + + text = "N/A" + data = None # noqa pylint: disable=unused-variable + try: + text = response.text + except Exception as ex: # noqa pylint: disable=broad-except + logger.warning("Error while reading response body %s.", ex) + else: + try: + data = json.loads(text) # noqa pylint: disable=unused-variable + except Exception: # noqa pylint: disable=broad-except + pass + + if response.status_code == 200: + logger.info("Transmission succeeded: %s.", text) + return SpanExportResult.SUCCESS + + if response.status_code in ( + 206, # Partial Content + 429, # Too Many Requests + 500, # Internal Server Error + 503, # Service Unavailable + ): + return SpanExportResult.FAILED_RETRYABLE + + return SpanExportResult.FAILED_NOT_RETRYABLE + + @staticmethod + def ns_to_duration(nanoseconds): + value = (nanoseconds + 500000) // 1000000 # duration in milliseconds + value, microseconds = divmod(value, 1000) + value, seconds = divmod(value, 60) + value, minutes = divmod(value, 60) + days, hours = divmod(value, 24) + return "{:d}.{:02d}:{:02d}:{:02d}.{:03d}".format( + days, hours, minutes, seconds, microseconds + ) + + def span_to_envelope(self, span): # noqa pylint: disable=too-many-branches + envelope = protocol.Envelope( + iKey=self.options.instrumentation_key, + tags=dict(util.azure_monitor_context), + time=ns_to_iso_str(span.start_time), + ) + envelope.tags["ai.operation.id"] = "{:032x}".format( + span.context.trace_id + ) + parent = span.parent + if isinstance(parent, Span): + parent = parent.context + if parent: + envelope.tags[ + "ai.operation.parentId" + ] = "|{:032x}.{:016x}.".format(parent.trace_id, parent.span_id) + if span.kind in (SpanKind.CONSUMER, SpanKind.SERVER): + envelope.name = "Microsoft.ApplicationInsights.Request" + data = protocol.Request( + id="|{:032x}.{:016x}.".format( + span.context.trace_id, span.context.span_id + ), + duration=self.ns_to_duration(span.end_time - span.start_time), + responseCode="0", + success=False, + properties={}, + ) + envelope.data = protocol.Data( + baseData=data, baseType="RequestData" + ) + if "http.method" in span.attributes: + data.name = span.attributes["http.method"] + if "http.route" in span.attributes: + data.name = data.name + " " + span.attributes["http.route"] + envelope.tags["ai.operation.name"] = data.name + if "http.url" in span.attributes: + data.url = span.attributes["http.url"] + if "http.status_code" in span.attributes: + status_code = span.attributes["http.status_code"] + data.responseCode = str(status_code) + data.success = 200 <= status_code < 400 + else: + envelope.name = "Microsoft.ApplicationInsights.RemoteDependency" + data = protocol.RemoteDependency( + name=span.name, + id="|{:032x}.{:016x}.".format( + span.context.trace_id, span.context.span_id + ), + resultCode="0", # TODO + duration=self.ns_to_duration(span.end_time - span.start_time), + success=True, # TODO + properties={}, + ) + envelope.data = protocol.Data( + baseData=data, baseType="RemoteDependencyData" + ) + if span.kind in (SpanKind.CLIENT, SpanKind.PRODUCER): + data.type = "HTTP" # TODO + if "http.url" in span.attributes: + url = span.attributes["http.url"] + # TODO: error handling, probably put scheme as well + data.name = urlparse(url).netloc + if "http.status_code" in span.attributes: + data.resultCode = str(span.attributes["http.status_code"]) + else: # SpanKind.INTERNAL + data.type = "InProc" + for key in span.attributes: + data.properties[key] = span.attributes[key] + if span.links: + links = [] + for link in span.links: + links.append( + { + "operation_Id": "{:032x}".format( + link.context.trace_id + ), + "id": "|{:032x}.{:016x}.".format( + link.context.trace_id, link.context.span_id + ), + } + ) + data.properties["_MS.links"] = json.dumps(links) + print(data.properties["_MS.links"]) + # TODO: tracestate, tags + return envelope diff --git a/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py new file mode 100644 index 0000000000..f97dbd3e33 --- /dev/null +++ b/ext/opentelemetry-ext-azure-monitor/src/opentelemetry/ext/azure_monitor/util.py @@ -0,0 +1,42 @@ +# 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 locale +import os +import platform +import sys + +from opentelemetry.ext.azure_monitor.protocol import BaseObject +from opentelemetry.ext.azure_monitor.version import __version__ as ext_version +from opentelemetry.sdk.version import __version__ as opentelemetry_version + +azure_monitor_context = { + "ai.cloud.role": os.path.basename(sys.argv[0]) or "Python Application", + "ai.cloud.roleInstance": platform.node(), + "ai.device.id": platform.node(), + "ai.device.locale": locale.getdefaultlocale()[0], + "ai.device.osVersion": platform.version(), + "ai.device.type": "Other", + "ai.internal.sdkVersion": "py{}:ot{}:ext{}".format( + platform.python_version(), opentelemetry_version, ext_version + ), +} + + +class Options(BaseObject): + _default = BaseObject( + endpoint="https://dc.services.visualstudio.com/v2/track", + instrumentation_key=os.getenv("APPINSIGHTS_INSTRUMENTATIONKEY", None), + timeout=10.0, # networking timeout in seconds + )