Merge pull request #280 from jeremydvoss/vendor-instrumentations

Vendor instrumentations
This commit is contained in:
Jeremy Voss 2023-06-12 12:09:06 -07:00 коммит произвёл GitHub
Родитель 7bca8a11f5 1e84508e94
Коммит 992d12dce9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
70 изменённых файлов: 5809 добавлений и 50 удалений

Просмотреть файл

@ -25,3 +25,4 @@ exclude =
target
__pycache__
*/build/lib/*
azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/*

Просмотреть файл

@ -14,5 +14,5 @@ profile=black
; docs: https://github.com/timothycrosley/isort#multi-line-output-modes
multi_line_output=3
skip=target
skip_glob=**/gen/*,.venv*/*,venv*/*,**/proto/*,.tox/*
skip_glob=**/gen/*,.venv*/*,venv*/*,**/proto/*,.tox/*, azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/*
known_third_party=opentelemetry,psutil,pytest,redis,redis_opentracing

Просмотреть файл

@ -2,6 +2,8 @@
## Unreleased
- Vendor Instrumentations
([#280](https://github.com/microsoft/ApplicationInsights-Python/pull/280))
- Update samples
([#281](https://github.com/microsoft/ApplicationInsights-Python/pull/281))
- Fixed spelling

Просмотреть файл

@ -14,6 +14,12 @@ from azure.monitor.opentelemetry._constants import (
SAMPLING_RATIO_ARG,
)
from azure.monitor.opentelemetry._types import ConfigurationValue
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.dependencies import (
get_dependency_conflicts,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
from azure.monitor.opentelemetry.exporter import (
ApplicationInsightsSampler,
AzureMonitorLogExporter,
@ -22,10 +28,6 @@ from azure.monitor.opentelemetry.exporter import (
)
from azure.monitor.opentelemetry.util.configurations import _get_configurations
from opentelemetry._logs import get_logger_provider, set_logger_provider
from opentelemetry.instrumentation.dependencies import (
get_dist_dependency_conflicts,
)
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.metrics import set_meter_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
@ -39,15 +41,15 @@ from pkg_resources import iter_entry_points
_logger = getLogger(__name__)
_SUPPORTED_INSTRUMENTED_LIBRARIES = (
"django",
"fastapi",
"flask",
"psycopg2",
"requests",
"urllib",
"urllib3",
)
_SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP = {
"django": ("django >= 1.10",),
"fastapi": ("fastapi ~= 0.58",),
"flask": ("flask >= 1.0, < 3.0",),
"psycopg2": ("psycopg2 >= 2.7.3.1",),
"requests": ("requests ~= 2.0",),
"urllib": tuple(),
"urllib3": ("urllib3 >= 1.0.0, < 2.0.0",),
}
def configure_azure_monitor(**kwargs) -> None:
@ -129,13 +131,18 @@ def _setup_metrics(configurations: Dict[str, ConfigurationValue]):
def _setup_instrumentations():
# use pkg_resources for now until https://github.com/open-telemetry/opentelemetry-python/pull/3168 is merged
for entry_point in iter_entry_points("opentelemetry_instrumentor"):
for entry_point in iter_entry_points(
"azure_monitor_opentelemetry_instrumentor"
):
lib_name = entry_point.name
if lib_name not in _SUPPORTED_INSTRUMENTED_LIBRARIES:
if lib_name not in _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP:
continue
try:
# Check if dependent libraries/version are installed
conflict = get_dist_dependency_conflicts(entry_point.dist)
instruments = _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP[
lib_name
]
conflict = get_dependency_conflicts(instruments)
if conflict:
_logger.debug(
"Skipping instrumentation %s: %s",

Просмотреть файл

@ -0,0 +1,5 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License in the project root for
# license information.
# --------------------------------------------------------------------------

Просмотреть файл

@ -0,0 +1,5 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License in the project root for
# license information.
# --------------------------------------------------------------------------

Просмотреть файл

@ -0,0 +1,5 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License in the project root for
# license information.
# --------------------------------------------------------------------------

Просмотреть файл

@ -0,0 +1,5 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License in the project root for
# license information.
# --------------------------------------------------------------------------

Просмотреть файл

@ -0,0 +1,486 @@
# Copyright The 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.
# pylint: disable=too-many-locals
import typing
import urllib
from functools import wraps
from timeit import default_timer
from typing import Tuple
from asgiref.compatibility import guarantee_single_callable
from opentelemetry import context, trace
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.asgi.version import (
__version__,
) # noqa
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.propagators import (
get_global_response_propagator,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_start_internal_or_server_span,
http_status_to_status_code,
)
from opentelemetry.metrics import get_meter
from opentelemetry.propagators.textmap import Getter, Setter
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span, set_span_in_context
from opentelemetry.trace.status import Status, StatusCode
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
SanitizeValue,
_parse_active_request_count_attrs,
_parse_duration_attrs,
get_custom_headers,
normalise_request_header_name,
normalise_response_header_name,
remove_url_credentials,
)
_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]]
class ASGIGetter(Getter[dict]):
def get(
self, carrier: dict, key: str
) -> typing.Optional[typing.List[str]]:
"""Getter implementation to retrieve a HTTP header value from the ASGI
scope.
Args:
carrier: ASGI scope object
key: header name in scope
Returns:
A list with a single string with the header value if it exists,
else None.
"""
headers = carrier.get("headers")
if not headers:
return None
# ASGI header keys are in lower case
key = key.lower()
decoded = [
_value.decode("utf8")
for (_key, _value) in headers
if _key.decode("utf8").lower() == key
]
if not decoded:
return None
return decoded
def keys(self, carrier: dict) -> typing.List[str]:
headers = carrier.get("headers") or []
return [_key.decode("utf8") for (_key, _value) in headers]
asgi_getter = ASGIGetter()
class ASGISetter(Setter[dict]):
def set(
self, carrier: dict, key: str, value: str
) -> None: # pylint: disable=no-self-use
"""Sets response header values on an ASGI scope according to `the spec <https://asgi.readthedocs.io/en/latest/specs/www.html#response-start-send-event>`_.
Args:
carrier: ASGI scope object
key: response header name to set
value: response header value
Returns:
None
"""
headers = carrier.get("headers")
if not headers:
headers = []
carrier["headers"] = headers
headers.append([key.lower().encode(), value.encode()])
asgi_setter = ASGISetter()
def collect_request_attributes(scope):
"""Collects HTTP request attributes from the ASGI scope and returns a
dictionary to be used as span creation attributes."""
server_host, port, http_url = get_host_port_url_tuple(scope)
query_string = scope.get("query_string")
if query_string and http_url:
if isinstance(query_string, bytes):
query_string = query_string.decode("utf8")
http_url += "?" + urllib.parse.unquote(query_string)
result = {
SpanAttributes.HTTP_SCHEME: scope.get("scheme"),
SpanAttributes.HTTP_HOST: server_host,
SpanAttributes.NET_HOST_PORT: port,
SpanAttributes.HTTP_FLAVOR: scope.get("http_version"),
SpanAttributes.HTTP_TARGET: scope.get("path"),
SpanAttributes.HTTP_URL: remove_url_credentials(http_url),
}
http_method = scope.get("method")
if http_method:
result[SpanAttributes.HTTP_METHOD] = http_method
http_host_value_list = asgi_getter.get(scope, "host")
if http_host_value_list:
result[SpanAttributes.HTTP_SERVER_NAME] = ",".join(
http_host_value_list
)
http_user_agent = asgi_getter.get(scope, "user-agent")
if http_user_agent:
result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent[0]
if "client" in scope and scope["client"] is not None:
result[SpanAttributes.NET_PEER_IP] = scope.get("client")[0]
result[SpanAttributes.NET_PEER_PORT] = scope.get("client")[1]
# remove None values
result = {k: v for k, v in result.items() if v is not None}
return result
def collect_custom_request_headers_attributes(scope):
"""returns custom HTTP request headers to be added into SERVER span as span attributes
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
"""
sanitize = SanitizeValue(
get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
)
)
# Decode headers before processing.
headers = {
_key.decode("utf8"): _value.decode("utf8")
for (_key, _value) in scope.get("headers")
}
return sanitize.sanitize_header_values(
headers,
get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
),
normalise_request_header_name,
)
def collect_custom_response_headers_attributes(message):
"""returns custom HTTP response headers to be added into SERVER span as span attributes
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
"""
sanitize = SanitizeValue(
get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
)
)
# Decode headers before processing.
headers = {
_key.decode("utf8"): _value.decode("utf8")
for (_key, _value) in message.get("headers")
}
return sanitize.sanitize_header_values(
headers,
get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
),
normalise_response_header_name,
)
def get_host_port_url_tuple(scope):
"""Returns (host, port, full_url) tuple."""
server = scope.get("server") or ["0.0.0.0", 80]
port = server[1]
server_host = server[0] + (":" + str(port) if str(port) != "80" else "")
full_path = scope.get("root_path", "") + scope.get("path", "")
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
return server_host, port, http_url
def set_status_code(span, status_code):
"""Adds HTTP response attributes to span using the status_code argument."""
if not span.is_recording():
return
try:
status_code = int(status_code)
except ValueError:
span.set_status(
Status(
StatusCode.ERROR,
"Non-integer HTTP status: " + repr(status_code),
)
)
else:
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
span.set_status(
Status(http_status_to_status_code(status_code, server_span=True))
)
def get_default_span_details(scope: dict) -> Tuple[str, dict]:
"""Default implementation for get_default_span_details
Args:
scope: the ASGI scope dictionary
Returns:
a tuple of the span name, and any attributes to attach to the span.
"""
span_name = (
scope.get("path", "").strip()
or f"HTTP {scope.get('method', '').strip()}"
)
return span_name, {}
def _collect_target_attribute(
scope: typing.Dict[str, typing.Any]
) -> typing.Optional[str]:
"""
Returns the target path as defined by the Semantic Conventions.
This value is suitable to use in metrics as it should replace concrete
values with a parameterized name. Example: /api/users/{user_id}
Refer to the specification
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#parameterized-attributes
Note: this function requires specific code for each framework, as there's no
standard attribute to use.
"""
# FastAPI
root_path = scope.get("root_path", "")
route = scope.get("route")
path_format = getattr(route, "path_format", None)
if path_format:
return f"{root_path}{path_format}"
return None
class OpenTelemetryMiddleware:
"""The ASGI application middleware.
This class is an ASGI middleware that starts and annotates spans for any
requests it is invoked with.
Args:
app: The ASGI application callable to forward requests to.
default_span_details: Callback which should return a string and a tuple, representing the desired default span name and a
dictionary with any additional span attributes to set.
Optional: Defaults to get_default_span_details.
server_request_hook: Optional callback which is called with the server span and ASGI
scope object for every incoming request.
client_request_hook: Optional callback which is called with the internal span and an ASGI
scope which is sent as a dictionary for when the method receive is called.
client_response_hook: Optional callback which is called with the internal span and an ASGI
event which is sent as a dictionary for when the method send is called.
tracer_provider: The optional tracer provider to use. If omitted
the current globally configured one is used.
"""
# pylint: disable=too-many-branches
def __init__(
self,
app,
excluded_urls=None,
default_span_details=None,
server_request_hook: _ServerRequestHookT = None,
client_request_hook: _ClientRequestHookT = None,
client_response_hook: _ClientResponseHookT = None,
tracer_provider=None,
meter_provider=None,
meter=None,
):
self.app = guarantee_single_callable(app)
self.tracer = trace.get_tracer(__name__, __version__, tracer_provider)
self.meter = (
get_meter(__name__, __version__, meter_provider)
if meter is None
else meter
)
self.duration_histogram = self.meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="measures the duration of the inbound HTTP request",
)
self.active_requests_counter = self.meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
description="measures the number of concurrent HTTP requests that are currently in-flight",
)
self.excluded_urls = excluded_urls
self.default_span_details = (
default_span_details or get_default_span_details
)
self.server_request_hook = server_request_hook
self.client_request_hook = client_request_hook
self.client_response_hook = client_response_hook
async def __call__(self, scope, receive, send):
"""The ASGI application
Args:
scope: An ASGI environment.
receive: An awaitable callable yielding dictionaries
send: An awaitable callable taking a single dictionary as argument.
"""
if scope["type"] not in ("http", "websocket"):
return await self.app(scope, receive, send)
_, _, url = get_host_port_url_tuple(scope)
if self.excluded_urls and self.excluded_urls.url_disabled(url):
return await self.app(scope, receive, send)
span_name, additional_attributes = self.default_span_details(scope)
span, token = _start_internal_or_server_span(
tracer=self.tracer,
span_name=span_name,
start_time=None,
context_carrier=scope,
context_getter=asgi_getter,
)
attributes = collect_request_attributes(scope)
attributes.update(additional_attributes)
active_requests_count_attrs = _parse_active_request_count_attrs(
attributes
)
duration_attrs = _parse_duration_attrs(attributes)
if scope["type"] == "http":
self.active_requests_counter.add(1, active_requests_count_attrs)
try:
with trace.use_span(span, end_on_exit=True) as current_span:
if current_span.is_recording():
for key, value in attributes.items():
current_span.set_attribute(key, value)
if current_span.kind == trace.SpanKind.SERVER:
custom_attributes = (
collect_custom_request_headers_attributes(scope)
)
if len(custom_attributes) > 0:
current_span.set_attributes(custom_attributes)
if callable(self.server_request_hook):
self.server_request_hook(current_span, scope)
otel_receive = self._get_otel_receive(
span_name, scope, receive
)
otel_send = self._get_otel_send(
current_span,
span_name,
scope,
send,
duration_attrs,
)
start = default_timer()
await self.app(scope, otel_receive, otel_send)
finally:
if scope["type"] == "http":
target = _collect_target_attribute(scope)
if target:
duration_attrs[SpanAttributes.HTTP_TARGET] = target
duration = max(round((default_timer() - start) * 1000), 0)
self.duration_histogram.record(duration, duration_attrs)
self.active_requests_counter.add(
-1, active_requests_count_attrs
)
if token:
context.detach(token)
# pylint: enable=too-many-branches
def _get_otel_receive(self, server_span_name, scope, receive):
@wraps(receive)
async def otel_receive():
with self.tracer.start_as_current_span(
" ".join((server_span_name, scope["type"], "receive"))
) as receive_span:
if callable(self.client_request_hook):
self.client_request_hook(receive_span, scope)
message = await receive()
if receive_span.is_recording():
if message["type"] == "websocket.receive":
set_status_code(receive_span, 200)
receive_span.set_attribute("type", message["type"])
return message
return otel_receive
def _get_otel_send(
self, server_span, server_span_name, scope, send, duration_attrs
):
@wraps(send)
async def otel_send(message):
with self.tracer.start_as_current_span(
" ".join((server_span_name, scope["type"], "send"))
) as send_span:
if callable(self.client_response_hook):
self.client_response_hook(send_span, message)
if send_span.is_recording():
if message["type"] == "http.response.start":
status_code = message["status"]
duration_attrs[
SpanAttributes.HTTP_STATUS_CODE
] = status_code
set_status_code(server_span, status_code)
set_status_code(send_span, status_code)
elif message["type"] == "websocket.send":
set_status_code(server_span, 200)
set_status_code(send_span, 200)
send_span.set_attribute("type", message["type"])
if (
server_span.is_recording()
and server_span.kind == trace.SpanKind.SERVER
and "headers" in message
):
custom_response_attributes = (
collect_custom_response_headers_attributes(message)
)
if len(custom_response_attributes) > 0:
server_span.set_attributes(
custom_response_attributes
)
propagator = get_global_response_propagator()
if propagator:
propagator.inject(
message,
context=set_span_in_context(
server_span, trace.context_api.Context()
),
setter=asgi_setter,
)
await send(message)
return otel_send

Просмотреть файл

@ -0,0 +1,16 @@
# Copyright The 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.
_instruments = ("asgiref ~= 3.0",)

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -0,0 +1,117 @@
#!/usr/bin/env python3
# Copyright The 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.
from argparse import REMAINDER, ArgumentParser
from logging import getLogger
from os import environ, execl, getcwd
from os.path import abspath, dirname, pathsep
from re import sub
from shutil import which
from pkg_resources import iter_entry_points
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.version import (
__version__,
)
_logger = getLogger(__name__)
def run() -> None:
parser = ArgumentParser(
description="""
opentelemetry-instrument automatically instruments a Python
program and its dependencies and then runs the program.
""",
epilog="""
Optional arguments (except for --help and --version) for opentelemetry-instrument
directly correspond with OpenTelemetry environment variables. The
corresponding optional argument is formed by removing the OTEL_ or
OTEL_PYTHON_ prefix from the environment variable and lower casing the
rest. For example, the optional argument --attribute_value_length_limit
corresponds with the environment variable
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT.
These optional arguments will override the current value of the
corresponding environment variable during the execution of the command.
""",
)
argument_otel_environment_variable = {}
for entry_point in iter_entry_points(
"opentelemetry_environment_variables"
):
environment_variable_module = entry_point.load()
for attribute in dir(environment_variable_module):
if attribute.startswith("OTEL_"):
argument = sub(r"OTEL_(PYTHON_)?", "", attribute).lower()
parser.add_argument(
f"--{argument}",
required=False,
)
argument_otel_environment_variable[argument] = attribute
parser.add_argument(
"--version",
help="print version information",
action="version",
version="%(prog)s " + __version__,
)
parser.add_argument("command", help="Your Python application.")
parser.add_argument(
"command_args",
help="Arguments for your application.",
nargs=REMAINDER,
)
args = parser.parse_args()
for argument, otel_environment_variable in (
argument_otel_environment_variable
).items():
value = getattr(args, argument)
if value is not None:
environ[otel_environment_variable] = value
python_path = environ.get("PYTHONPATH")
if not python_path:
python_path = []
else:
python_path = python_path.split(pathsep)
cwd_path = getcwd()
# This is being added to support applications that are being run from their
# own executable, like Django.
# FIXME investigate if there is another way to achieve this
if cwd_path not in python_path:
python_path.insert(0, cwd_path)
filedir_path = dirname(abspath(__file__))
python_path = [path for path in python_path if path != filedir_path]
python_path.insert(0, filedir_path)
environ["PYTHONPATH"] = pathsep.join(python_path)
executable = which(args.command)
execl(executable, executable, *args.command_args)

Просмотреть файл

@ -0,0 +1,134 @@
# Copyright The 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.
from logging import getLogger
from os import environ
from os.path import abspath, dirname, pathsep
from pkg_resources import iter_entry_points
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.dependencies import (
get_dist_dependency_conflicts,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.distro import (
BaseDistro,
DefaultDistro,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.environment_variables import (
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_python_path_without_directory,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.version import (
__version__,
)
logger = getLogger(__name__)
def _load_distros() -> BaseDistro:
for entry_point in iter_entry_points("opentelemetry_distro"):
try:
distro = entry_point.load()()
if not isinstance(distro, BaseDistro):
logger.debug(
"%s is not an OpenTelemetry Distro. Skipping",
entry_point.name,
)
continue
logger.debug(
"Distribution %s will be configured", entry_point.name
)
return distro
except Exception as exc: # pylint: disable=broad-except
logger.exception(
"Distribution %s configuration failed", entry_point.name
)
raise exc
return DefaultDistro()
def _load_instrumentors(distro):
package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, [])
if isinstance(package_to_exclude, str):
package_to_exclude = package_to_exclude.split(",")
# to handle users entering "requests , flask" or "requests, flask" with spaces
package_to_exclude = [x.strip() for x in package_to_exclude]
for entry_point in iter_entry_points("opentelemetry_pre_instrument"):
entry_point.load()()
for entry_point in iter_entry_points("opentelemetry_instrumentor"):
if entry_point.name in package_to_exclude:
logger.debug(
"Instrumentation skipped for library %s", entry_point.name
)
continue
try:
conflict = get_dist_dependency_conflicts(entry_point.dist)
if conflict:
logger.debug(
"Skipping instrumentation %s: %s",
entry_point.name,
conflict,
)
continue
# tell instrumentation to not run dep checks again as we already did it above
distro.load_instrumentor(entry_point, skip_dep_check=True)
logger.debug("Instrumented %s", entry_point.name)
except Exception as exc: # pylint: disable=broad-except
logger.exception("Instrumenting of %s failed", entry_point.name)
raise exc
for entry_point in iter_entry_points("opentelemetry_post_instrument"):
entry_point.load()()
def _load_configurators():
configured = None
for entry_point in iter_entry_points("opentelemetry_configurator"):
if configured is not None:
logger.warning(
"Configuration of %s not loaded, %s already loaded",
entry_point.name,
configured,
)
continue
try:
entry_point.load()().configure(auto_instrumentation_version=__version__) # type: ignore
configured = entry_point.name
except Exception as exc: # pylint: disable=broad-except
logger.exception("Configuration of %s failed", entry_point.name)
raise exc
def initialize():
# prevents auto-instrumentation of subprocesses if code execs another python process
environ["PYTHONPATH"] = _python_path_without_directory(
environ["PYTHONPATH"], dirname(abspath(__file__)), pathsep
)
try:
distro = _load_distros()
distro.configure()
_load_configurators()
_load_instrumentors(distro)
except Exception: # pylint: disable=broad-except
logger.exception("Failed to auto initialize opentelemetry")
initialize()

Просмотреть файл

@ -0,0 +1,163 @@
#!/usr/bin/env python3
# Copyright The 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 argparse
import logging
import subprocess
import sys
import pkg_resources
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.bootstrap_gen import (
default_instrumentations,
libraries,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.version import (
__version__,
)
logger = logging.getLogger(__name__)
def _syscall(func):
def wrapper(package=None):
try:
if package:
return func(package)
return func()
except subprocess.SubprocessError as exp:
cmd = getattr(exp, "cmd", None)
if cmd:
msg = f'Error calling system command "{" ".join(cmd)}"'
if package:
msg = f'{msg} for package "{package}"'
raise RuntimeError(msg)
return wrapper
@_syscall
def _sys_pip_install(package):
# explicit upgrade strategy to override potential pip config
subprocess.check_call(
[
sys.executable,
"-m",
"pip",
"install",
"-U",
"--upgrade-strategy",
"only-if-needed",
package,
]
)
def _pip_check():
"""Ensures none of the instrumentations have dependency conflicts.
Clean check reported as:
'No broken requirements found.'
Dependency conflicts are reported as:
'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.'
To not be too restrictive, we'll only check for relevant packages.
"""
with subprocess.Popen(
[sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE
) as check_pipe:
pip_check = check_pipe.communicate()[0].decode()
pip_check_lower = pip_check.lower()
for package_tup in libraries.values():
for package in package_tup:
if package.lower() in pip_check_lower:
raise RuntimeError(f"Dependency conflict found: {pip_check}")
def _is_installed(req):
if req in sys.modules:
return True
try:
pkg_resources.get_distribution(req)
except pkg_resources.DistributionNotFound:
return False
except pkg_resources.VersionConflict as exc:
logger.warning(
"instrumentation for package %s is available but version %s is installed. Skipping.",
exc.req,
exc.dist.as_requirement(), # pylint: disable=no-member
)
return False
return True
def _find_installed_libraries():
libs = default_instrumentations[:]
libs.extend(
[
v["instrumentation"]
for _, v in libraries.items()
if _is_installed(v["library"])
]
)
return libs
def _run_requirements():
logger.setLevel(logging.ERROR)
print("\n".join(_find_installed_libraries()), end="")
def _run_install():
for lib in _find_installed_libraries():
_sys_pip_install(lib)
_pip_check()
def run() -> None:
action_install = "install"
action_requirements = "requirements"
parser = argparse.ArgumentParser(
description="""
opentelemetry-bootstrap detects installed libraries and automatically
installs the relevant instrumentation packages for them.
"""
)
parser.add_argument(
"--version",
help="print version information",
action="version",
version="%(prog)s " + __version__,
)
parser.add_argument(
"-a",
"--action",
choices=[action_install, action_requirements],
default=action_requirements,
help="""
install - uses pip to install the new requirements using to the
currently active site-package.
requirements - prints out the new requirements to stdout. Action can
be piped and appended to a requirements.txt file.
""",
)
args = parser.parse_args()
cmd = {
action_install: _run_install,
action_requirements: _run_requirements,
}[args.action]
cmd()

Просмотреть файл

@ -0,0 +1,175 @@
# Copyright The 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.
# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES.
# RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE.
libraries = {
"aio_pika": {
"library": "aio_pika >= 7.2.0, < 10.0.0",
"instrumentation": "opentelemetry-instrumentation-aio-pika==0.38b0",
},
"aiohttp": {
"library": "aiohttp ~= 3.0",
"instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.38b0",
},
"aiopg": {
"library": "aiopg >= 0.13.0, < 2.0.0",
"instrumentation": "opentelemetry-instrumentation-aiopg==0.38b0",
},
"asgiref": {
"library": "asgiref ~= 3.0",
"instrumentation": "opentelemetry-instrumentation-asgi==0.38b0",
},
"asyncpg": {
"library": "asyncpg >= 0.12.0",
"instrumentation": "opentelemetry-instrumentation-asyncpg==0.38b0",
},
"boto": {
"library": "boto~=2.0",
"instrumentation": "opentelemetry-instrumentation-boto==0.38b0",
},
"boto3": {
"library": "boto3 ~= 1.0",
"instrumentation": "opentelemetry-instrumentation-boto3sqs==0.38b0",
},
"botocore": {
"library": "botocore ~= 1.0",
"instrumentation": "opentelemetry-instrumentation-botocore==0.38b0",
},
"celery": {
"library": "celery >= 4.0, < 6.0",
"instrumentation": "opentelemetry-instrumentation-celery==0.38b0",
},
"confluent-kafka": {
"library": "confluent-kafka >= 1.8.2, < 2.0.0",
"instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.38b0",
},
"django": {
"library": "django >= 1.10",
"instrumentation": "opentelemetry-instrumentation-django==0.38b0",
},
"elasticsearch": {
"library": "elasticsearch >= 2.0",
"instrumentation": "opentelemetry-instrumentation-elasticsearch==0.38b0",
},
"falcon": {
"library": "falcon >= 1.4.1, < 4.0.0",
"instrumentation": "opentelemetry-instrumentation-falcon==0.38b0",
},
"fastapi": {
"library": "fastapi ~= 0.58",
"instrumentation": "opentelemetry-instrumentation-fastapi==0.38b0",
},
"flask": {
"library": "flask >= 1.0, < 3.0",
"instrumentation": "opentelemetry-instrumentation-flask==0.38b0",
},
"grpcio": {
"library": "grpcio ~= 1.27",
"instrumentation": "opentelemetry-instrumentation-grpc==0.38b0",
},
"httpx": {
"library": "httpx >= 0.18.0, <= 0.23.0",
"instrumentation": "opentelemetry-instrumentation-httpx==0.38b0",
},
"jinja2": {
"library": "jinja2 >= 2.7, < 4.0",
"instrumentation": "opentelemetry-instrumentation-jinja2==0.38b0",
},
"kafka-python": {
"library": "kafka-python >= 2.0",
"instrumentation": "opentelemetry-instrumentation-kafka-python==0.38b0",
},
"mysql-connector-python": {
"library": "mysql-connector-python ~= 8.0",
"instrumentation": "opentelemetry-instrumentation-mysql==0.38b0",
},
"pika": {
"library": "pika >= 0.12.0",
"instrumentation": "opentelemetry-instrumentation-pika==0.38b0",
},
"psycopg2": {
"library": "psycopg2 >= 2.7.3.1",
"instrumentation": "opentelemetry-instrumentation-psycopg2==0.38b0",
},
"pymemcache": {
"library": "pymemcache >= 1.3.5, < 4",
"instrumentation": "opentelemetry-instrumentation-pymemcache==0.38b0",
},
"pymongo": {
"library": "pymongo >= 3.1, < 5.0",
"instrumentation": "opentelemetry-instrumentation-pymongo==0.38b0",
},
"PyMySQL": {
"library": "PyMySQL < 2",
"instrumentation": "opentelemetry-instrumentation-pymysql==0.38b0",
},
"pyramid": {
"library": "pyramid >= 1.7",
"instrumentation": "opentelemetry-instrumentation-pyramid==0.38b0",
},
"redis": {
"library": "redis >= 2.6",
"instrumentation": "opentelemetry-instrumentation-redis==0.38b0",
},
"remoulade": {
"library": "remoulade >= 0.50",
"instrumentation": "opentelemetry-instrumentation-remoulade==0.38b0",
},
"requests": {
"library": "requests ~= 2.0",
"instrumentation": "opentelemetry-instrumentation-requests==0.38b0",
},
"scikit-learn": {
"library": "scikit-learn ~= 0.24.0",
"instrumentation": "opentelemetry-instrumentation-sklearn==0.38b0",
},
"sqlalchemy": {
"library": "sqlalchemy",
"instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.38b0",
},
"starlette": {
"library": "starlette ~= 0.13.0",
"instrumentation": "opentelemetry-instrumentation-starlette==0.38b0",
},
"psutil": {
"library": "psutil >= 5",
"instrumentation": "opentelemetry-instrumentation-system-metrics==0.38b0",
},
"tornado": {
"library": "tornado >= 5.1.1",
"instrumentation": "opentelemetry-instrumentation-tornado==0.38b0",
},
"tortoise-orm": {
"library": "tortoise-orm >= 0.17.0",
"instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.38b0",
},
"pydantic": {
"library": "pydantic >= 1.10.2",
"instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.38b0",
},
"urllib3": {
"library": "urllib3 >= 1.0.0, < 2.0.0",
"instrumentation": "opentelemetry-instrumentation-urllib3==0.38b0",
},
}
default_instrumentations = [
"opentelemetry-instrumentation-aws-lambda==0.38b0",
"opentelemetry-instrumentation-dbapi==0.38b0",
"opentelemetry-instrumentation-logging==0.38b0",
"opentelemetry-instrumentation-sqlite3==0.38b0",
"opentelemetry-instrumentation-urllib==0.38b0",
"opentelemetry-instrumentation-wsgi==0.38b0",
]

Просмотреть файл

@ -0,0 +1,473 @@
# Copyright The 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 functools
import logging
import re
import typing
import wrapt
from opentelemetry import trace as trace_api
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.dbapi.version import (
__version__,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.sqlcommenter_utils import (
_add_sql_comment,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_get_opentelemetry_values,
unwrap,
)
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
_logger = logging.getLogger(__name__)
def trace_integration(
connect_module: typing.Callable[..., typing.Any],
connect_method_name: str,
database_system: str,
connection_attributes: typing.Dict = None,
tracer_provider: typing.Optional[TracerProvider] = None,
capture_parameters: bool = False,
enable_commenter: bool = False,
db_api_integration_factory=None,
):
"""Integrate with DB API library.
https://www.python.org/dev/peps/pep-0249/
Args:
connect_module: Module name where connect method is available.
connect_method_name: The connect method name.
database_system: An identifier for the database management system (DBMS)
product being used.
connection_attributes: Attribute names for database, port, host and
user in Connection object.
tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to
use. If omitted the current configured one is used.
capture_parameters: Configure if db.statement.parameters should be captured.
enable_commenter: Flag to enable/disable sqlcommenter.
db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the
default one is used.
"""
wrap_connect(
__name__,
connect_module,
connect_method_name,
database_system,
connection_attributes,
version=__version__,
tracer_provider=tracer_provider,
capture_parameters=capture_parameters,
enable_commenter=enable_commenter,
db_api_integration_factory=db_api_integration_factory,
)
def wrap_connect(
name: str,
connect_module: typing.Callable[..., typing.Any],
connect_method_name: str,
database_system: str,
connection_attributes: typing.Dict = None,
version: str = "",
tracer_provider: typing.Optional[TracerProvider] = None,
capture_parameters: bool = False,
enable_commenter: bool = False,
db_api_integration_factory=None,
commenter_options: dict = None,
):
"""Integrate with DB API library.
https://www.python.org/dev/peps/pep-0249/
Args:
connect_module: Module name where connect method is available.
connect_method_name: The connect method name.
database_system: An identifier for the database management system (DBMS)
product being used.
connection_attributes: Attribute names for database, port, host and
user in Connection object.
tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to
use. If omitted the current configured one is used.
capture_parameters: Configure if db.statement.parameters should be captured.
enable_commenter: Flag to enable/disable sqlcommenter.
db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the
default one is used.
commenter_options: Configurations for tags to be appended at the sql query.
"""
db_api_integration_factory = (
db_api_integration_factory or DatabaseApiIntegration
)
# pylint: disable=unused-argument
def wrap_connect_(
wrapped: typing.Callable[..., typing.Any],
instance: typing.Any,
args: typing.Tuple[typing.Any, typing.Any],
kwargs: typing.Dict[typing.Any, typing.Any],
):
db_integration = db_api_integration_factory(
name,
database_system,
connection_attributes=connection_attributes,
version=version,
tracer_provider=tracer_provider,
capture_parameters=capture_parameters,
enable_commenter=enable_commenter,
commenter_options=commenter_options,
connect_module=connect_module,
)
return db_integration.wrapped_connection(wrapped, args, kwargs)
try:
wrapt.wrap_function_wrapper(
connect_module, connect_method_name, wrap_connect_
)
except Exception as ex: # pylint: disable=broad-except
_logger.warning("Failed to integrate with DB API. %s", str(ex))
def unwrap_connect(
connect_module: typing.Callable[..., typing.Any], connect_method_name: str
):
"""Disable integration with DB API library.
https://www.python.org/dev/peps/pep-0249/
Args:
connect_module: Module name where the connect method is available.
connect_method_name: The connect method name.
"""
unwrap(connect_module, connect_method_name)
def instrument_connection(
name: str,
connection,
database_system: str,
connection_attributes: typing.Dict = None,
version: str = "",
tracer_provider: typing.Optional[TracerProvider] = None,
capture_parameters: bool = False,
enable_commenter: bool = False,
commenter_options: dict = None,
):
"""Enable instrumentation in a database connection.
Args:
connection: The connection to instrument.
database_system: An identifier for the database management system (DBMS)
product being used.
connection_attributes: Attribute names for database, port, host and
user in a connection object.
tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to
use. If omitted the current configured one is used.
capture_parameters: Configure if db.statement.parameters should be captured.
enable_commenter: Flag to enable/disable sqlcommenter.
commenter_options: Configurations for tags to be appended at the sql query.
Returns:
An instrumented connection.
"""
if isinstance(connection, wrapt.ObjectProxy):
_logger.warning("Connection already instrumented")
return connection
db_integration = DatabaseApiIntegration(
name,
database_system,
connection_attributes=connection_attributes,
version=version,
tracer_provider=tracer_provider,
capture_parameters=capture_parameters,
enable_commenter=enable_commenter,
commenter_options=commenter_options,
)
db_integration.get_connection_attributes(connection)
return get_traced_connection_proxy(connection, db_integration)
def uninstrument_connection(connection):
"""Disable instrumentation in a database connection.
Args:
connection: The connection to uninstrument.
Returns:
An uninstrumented connection.
"""
if isinstance(connection, wrapt.ObjectProxy):
return connection.__wrapped__
_logger.warning("Connection is not instrumented")
return connection
class DatabaseApiIntegration:
def __init__(
self,
name: str,
database_system: str,
connection_attributes=None,
version: str = "",
tracer_provider: typing.Optional[TracerProvider] = None,
capture_parameters: bool = False,
enable_commenter: bool = False,
commenter_options: dict = None,
connect_module: typing.Callable[..., typing.Any] = None,
):
self.connection_attributes = connection_attributes
if self.connection_attributes is None:
self.connection_attributes = {
"database": "database",
"port": "port",
"host": "host",
"user": "user",
}
self._name = name
self._version = version
self._tracer = get_tracer(
self._name,
instrumenting_library_version=self._version,
tracer_provider=tracer_provider,
)
self.capture_parameters = capture_parameters
self.enable_commenter = enable_commenter
self.commenter_options = commenter_options
self.database_system = database_system
self.connection_props = {}
self.span_attributes = {}
self.name = ""
self.database = ""
self.connect_module = connect_module
def wrapped_connection(
self,
connect_method: typing.Callable[..., typing.Any],
args: typing.Tuple[typing.Any, typing.Any],
kwargs: typing.Dict[typing.Any, typing.Any],
):
"""Add object proxy to connection object."""
connection = connect_method(*args, **kwargs)
self.get_connection_attributes(connection)
return get_traced_connection_proxy(connection, self)
def get_connection_attributes(self, connection):
# Populate span fields using connection
for key, value in self.connection_attributes.items():
# Allow attributes nested in connection object
attribute = functools.reduce(
lambda attribute, attribute_value: getattr(
attribute, attribute_value, None
),
value.split("."),
connection,
)
if attribute:
self.connection_props[key] = attribute
self.name = self.database_system
self.database = self.connection_props.get("database", "")
if self.database:
# PyMySQL encodes names with utf-8
if hasattr(self.database, "decode"):
self.database = self.database.decode(errors="ignore")
self.name += "." + self.database
user = self.connection_props.get("user")
# PyMySQL encodes this data
if user and isinstance(user, bytes):
user = user.decode()
if user is not None:
self.span_attributes[SpanAttributes.DB_USER] = str(user)
host = self.connection_props.get("host")
if host is not None:
self.span_attributes[SpanAttributes.NET_PEER_NAME] = host
port = self.connection_props.get("port")
if port is not None:
self.span_attributes[SpanAttributes.NET_PEER_PORT] = port
def get_traced_connection_proxy(
connection, db_api_integration, *args, **kwargs
):
# pylint: disable=abstract-method
class TracedConnectionProxy(wrapt.ObjectProxy):
# pylint: disable=unused-argument
def __init__(self, connection, *args, **kwargs):
wrapt.ObjectProxy.__init__(self, connection)
def __getattribute__(self, name):
if object.__getattribute__(self, name):
return object.__getattribute__(self, name)
return object.__getattribute__(
object.__getattribute__(self, "_connection"), name
)
def cursor(self, *args, **kwargs):
return get_traced_cursor_proxy(
self.__wrapped__.cursor(*args, **kwargs), db_api_integration
)
def __enter__(self):
self.__wrapped__.__enter__()
return self
def __exit__(self, *args, **kwargs):
self.__wrapped__.__exit__(*args, **kwargs)
return TracedConnectionProxy(connection, *args, **kwargs)
class CursorTracer:
def __init__(self, db_api_integration: DatabaseApiIntegration) -> None:
self._db_api_integration = db_api_integration
self._commenter_enabled = self._db_api_integration.enable_commenter
self._commenter_options = (
self._db_api_integration.commenter_options
if self._db_api_integration.commenter_options
else {}
)
self._connect_module = self._db_api_integration.connect_module
self._leading_comment_remover = re.compile(r"^/\*.*?\*/")
def _populate_span(
self,
span: trace_api.Span,
cursor,
*args: typing.Tuple[typing.Any, typing.Any],
):
if not span.is_recording():
return
statement = self.get_statement(cursor, args)
span.set_attribute(
SpanAttributes.DB_SYSTEM, self._db_api_integration.database_system
)
span.set_attribute(
SpanAttributes.DB_NAME, self._db_api_integration.database
)
span.set_attribute(SpanAttributes.DB_STATEMENT, statement)
for (
attribute_key,
attribute_value,
) in self._db_api_integration.span_attributes.items():
span.set_attribute(attribute_key, attribute_value)
if self._db_api_integration.capture_parameters and len(args) > 1:
span.set_attribute("db.statement.parameters", str(args[1]))
def get_operation_name(self, cursor, args): # pylint: disable=no-self-use
if args and isinstance(args[0], str):
# Strip leading comments so we get the operation name.
return self._leading_comment_remover.sub("", args[0]).split()[0]
return ""
def get_statement(self, cursor, args): # pylint: disable=no-self-use
if not args:
return ""
statement = args[0]
if isinstance(statement, bytes):
return statement.decode("utf8", "replace")
return statement
def traced_execution(
self,
cursor,
query_method: typing.Callable[..., typing.Any],
*args: typing.Tuple[typing.Any, typing.Any],
**kwargs: typing.Dict[typing.Any, typing.Any],
):
name = self.get_operation_name(cursor, args)
if not name:
name = (
self._db_api_integration.database
if self._db_api_integration.database
else self._db_api_integration.name
)
with self._db_api_integration._tracer.start_as_current_span(
name, kind=SpanKind.CLIENT
) as span:
self._populate_span(span, cursor, *args)
if args and self._commenter_enabled:
try:
args_list = list(args)
commenter_data = dict(
# Psycopg2/framework information
db_driver=f"psycopg2:{self._connect_module.__version__.split(' ')[0]}",
dbapi_threadsafety=self._connect_module.threadsafety,
dbapi_level=self._connect_module.apilevel,
libpq_version=self._connect_module.__libpq_version__,
driver_paramstyle=self._connect_module.paramstyle,
)
if self._commenter_options.get(
"opentelemetry_values", True
):
commenter_data.update(**_get_opentelemetry_values())
# Filter down to just the requested attributes.
commenter_data = {
k: v
for k, v in commenter_data.items()
if self._commenter_options.get(k, True)
}
statement = _add_sql_comment(
args_list[0], **commenter_data
)
args_list[0] = statement
args = tuple(args_list)
except Exception as exc: # pylint: disable=broad-except
_logger.exception(
"Exception while generating sql comment: %s", exc
)
return query_method(*args, **kwargs)
def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs):
_cursor_tracer = CursorTracer(db_api_integration)
# pylint: disable=abstract-method
class TracedCursorProxy(wrapt.ObjectProxy):
# pylint: disable=unused-argument
def __init__(self, cursor, *args, **kwargs):
wrapt.ObjectProxy.__init__(self, cursor)
def execute(self, *args, **kwargs):
return _cursor_tracer.traced_execution(
self.__wrapped__, self.__wrapped__.execute, *args, **kwargs
)
def executemany(self, *args, **kwargs):
return _cursor_tracer.traced_execution(
self.__wrapped__, self.__wrapped__.executemany, *args, **kwargs
)
def callproc(self, *args, **kwargs):
return _cursor_tracer.traced_execution(
self.__wrapped__, self.__wrapped__.callproc, *args, **kwargs
)
def __enter__(self):
self.__wrapped__.__enter__()
return self
def __exit__(self, *args, **kwargs):
self.__wrapped__.__exit__(*args, **kwargs)
return TracedCursorProxy(cursor, *args, **kwargs)

Просмотреть файл

@ -0,0 +1,16 @@
# Copyright The 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.
_instruments = tuple()

Просмотреть файл

@ -0,0 +1,17 @@
# Copyright The 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.38b0"
_instruments = tuple()

Просмотреть файл

@ -0,0 +1,62 @@
from logging import getLogger
from typing import Collection, Optional
from pkg_resources import (
Distribution,
DistributionNotFound,
RequirementParseError,
VersionConflict,
get_distribution,
)
logger = getLogger(__name__)
class DependencyConflict:
required: str = None
found: Optional[str] = None
def __init__(self, required, found=None):
self.required = required
self.found = found
def __str__(self):
return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"'
def get_dist_dependency_conflicts(
dist: Distribution,
) -> Optional[DependencyConflict]:
main_deps = dist.requires()
instrumentation_deps = []
for dep in dist.requires(("instruments",)):
if dep not in main_deps:
# we set marker to none so string representation of the dependency looks like
# requests ~= 1.0
# instead of
# requests ~= 1.0; extra = "instruments"
# which does not work with `get_distribution()`
dep.marker = None
instrumentation_deps.append(str(dep))
return get_dependency_conflicts(instrumentation_deps)
def get_dependency_conflicts(
deps: Collection[str],
) -> Optional[DependencyConflict]:
for dep in deps:
try:
get_distribution(dep)
except VersionConflict as exc:
return DependencyConflict(dep, exc.dist)
except DistributionNotFound:
return DependencyConflict(dep)
except RequirementParseError as exc:
logger.warning(
'error parsing dependency, reporting as a conflict: "%s" - %s',
dep,
exc,
)
return DependencyConflict(dep)
return None

Просмотреть файл

@ -0,0 +1,72 @@
# Copyright The 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.
# type: ignore
"""
OpenTelemetry Base Distribution (Distro)
"""
from abc import ABC, abstractmethod
from logging import getLogger
from pkg_resources import EntryPoint
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
_LOG = getLogger(__name__)
class BaseDistro(ABC):
"""An ABC for distro"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = object.__new__(cls, *args, **kwargs)
return cls._instance
@abstractmethod
def _configure(self, **kwargs):
"""Configure the distribution"""
def configure(self, **kwargs):
"""Configure the distribution"""
self._configure(**kwargs)
def load_instrumentor( # pylint: disable=no-self-use
self, entry_point: EntryPoint, **kwargs
):
"""Takes a collection of instrumentation entry points
and activates them by instantiating and calling instrument()
on each one.
Distros can override this method to customize the behavior by
inspecting each entry point and configuring them in special ways,
passing additional arguments, load a replacement/fork instead,
skip loading entirely, etc.
"""
instrumentor: BaseInstrumentor = entry_point.load()
instrumentor().instrument(**kwargs)
class DefaultDistro(BaseDistro):
def _configure(self, **kwargs):
pass
__all__ = ["BaseDistro", "DefaultDistro"]

Просмотреть файл

@ -0,0 +1,167 @@
# Copyright The 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.
from logging import getLogger
from os import environ
from typing import Collection
from django import VERSION as django_version
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.django.environment_variables import (
OTEL_PYTHON_DJANGO_INSTRUMENT,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.django.middleware.otel_middleware import (
_DjangoMiddleware,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.django.package import (
_instruments,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.django.version import (
__version__,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
from opentelemetry.metrics import get_meter
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.trace import get_tracer
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http import (
get_excluded_urls,
parse_excluded_urls,
)
DJANGO_2_0 = django_version >= (2, 0)
_excluded_urls_from_env = get_excluded_urls("DJANGO")
_logger = getLogger(__name__)
def _get_django_middleware_setting() -> str:
# In Django versions 1.x, setting MIDDLEWARE_CLASSES can be used as a legacy
# alternative to MIDDLEWARE. This is the case when `settings.MIDDLEWARE` has
# its default value (`None`).
if not DJANGO_2_0 and getattr(settings, "MIDDLEWARE", None) is None:
return "MIDDLEWARE_CLASSES"
return "MIDDLEWARE"
class DjangoInstrumentor(BaseInstrumentor):
"""An instrumentor for Django
See `BaseInstrumentor`
"""
_opentelemetry_middleware = ".".join(
[_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__]
)
_sql_commenter_middleware = "azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter"
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments
def _instrument(self, **kwargs):
# FIXME this is probably a pattern that will show up in the rest of the
# ext. Find a better way of implementing this.
if environ.get(OTEL_PYTHON_DJANGO_INSTRUMENT) == "False":
return
tracer_provider = kwargs.get("tracer_provider")
meter_provider = kwargs.get("meter_provider")
_excluded_urls = kwargs.get("excluded_urls")
tracer = get_tracer(
__name__,
__version__,
tracer_provider=tracer_provider,
)
meter = get_meter(__name__, __version__, meter_provider=meter_provider)
_DjangoMiddleware._tracer = tracer
_DjangoMiddleware._meter = meter
_DjangoMiddleware._excluded_urls = (
_excluded_urls_from_env
if _excluded_urls is None
else parse_excluded_urls(_excluded_urls)
)
_DjangoMiddleware._otel_request_hook = kwargs.pop("request_hook", None)
_DjangoMiddleware._otel_response_hook = kwargs.pop(
"response_hook", None
)
_DjangoMiddleware._duration_histogram = meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="measures the duration of the inbound http request",
)
_DjangoMiddleware._active_request_counter = meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
description="measures the number of concurrent HTTP requests those are currently in flight",
)
# This can not be solved, but is an inherent problem of this approach:
# the order of middleware entries matters, and here you have no control
# on that:
# https://docs.djangoproject.com/en/3.0/topics/http/middleware/#activating-middleware
# https://docs.djangoproject.com/en/3.0/ref/middleware/#middleware-ordering
_middleware_setting = _get_django_middleware_setting()
settings_middleware = []
try:
settings_middleware = getattr(settings, _middleware_setting, [])
except ImproperlyConfigured as exception:
_logger.debug(
"DJANGO_SETTINGS_MODULE environment variable not configured. Defaulting to empty settings: %s",
exception,
)
settings.configure()
settings_middleware = getattr(settings, _middleware_setting, [])
except ModuleNotFoundError as exception:
_logger.debug(
"DJANGO_SETTINGS_MODULE points to a non-existent module. Defaulting to empty settings: %s",
exception,
)
settings.configure()
settings_middleware = getattr(settings, _middleware_setting, [])
# Django allows to specify middlewares as a tuple, so we convert this tuple to a
# list, otherwise we wouldn't be able to call append/remove
if isinstance(settings_middleware, tuple):
settings_middleware = list(settings_middleware)
is_sql_commentor_enabled = kwargs.pop("is_sql_commentor_enabled", None)
if is_sql_commentor_enabled:
settings_middleware.insert(0, self._sql_commenter_middleware)
settings_middleware.insert(0, self._opentelemetry_middleware)
setattr(settings, _middleware_setting, settings_middleware)
def _uninstrument(self, **kwargs):
_middleware_setting = _get_django_middleware_setting()
settings_middleware = getattr(settings, _middleware_setting, None)
# FIXME This is starting to smell like trouble. We have 2 mechanisms
# that may make this condition be True, one implemented in
# BaseInstrumentor and another one implemented in _instrument. Both
# stop _instrument from running and thus, settings_middleware not being
# set.
if settings_middleware is None or (
self._opentelemetry_middleware not in settings_middleware
):
return
settings_middleware.remove(self._opentelemetry_middleware)
setattr(settings, _middleware_setting, settings_middleware)

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.
OTEL_PYTHON_DJANGO_INSTRUMENT = "OTEL_PYTHON_DJANGO_INSTRUMENT"

Просмотреть файл

@ -0,0 +1,400 @@
# Copyright The 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 types
from logging import getLogger
from time import time
from timeit import default_timer
from typing import Callable
from django import VERSION as django_version
from django.http import HttpRequest, HttpResponse
from opentelemetry.context import detach
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.propagators import (
get_global_response_propagator,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_start_internal_or_server_span,
extract_attributes_from_object,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.wsgi import (
add_response_attributes,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.wsgi import (
collect_custom_request_headers_attributes as wsgi_collect_custom_request_headers_attributes,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.wsgi import (
collect_custom_response_headers_attributes as wsgi_collect_custom_response_headers_attributes,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.wsgi import (
collect_request_attributes as wsgi_collect_request_attributes,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.wsgi import (
wsgi_getter,
)
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span, SpanKind, use_span
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http import (
_parse_active_request_count_attrs,
_parse_duration_attrs,
get_excluded_urls,
get_traced_request_attrs,
)
try:
from django.core.urlresolvers import ( # pylint: disable=no-name-in-module
Resolver404,
resolve,
)
except ImportError:
from django.urls import Resolver404, resolve
DJANGO_2_0 = django_version >= (2, 0)
DJANGO_3_0 = django_version >= (3, 0)
if DJANGO_2_0:
# Since Django 2.0, only `settings.MIDDLEWARE` is supported, so new-style
# middlewares can be used.
class MiddlewareMixin:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
self.process_request(request)
response = self.get_response(request)
return self.process_response(request, response)
else:
# Django versions 1.x can use `settings.MIDDLEWARE_CLASSES` and expect
# old-style middlewares, which are created by inheriting from
# `deprecation.MiddlewareMixin` since its creation in Django 1.10 and 1.11,
# or from `object` for older versions.
try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
MiddlewareMixin = object
if DJANGO_3_0:
from django.core.handlers.asgi import ASGIRequest
else:
ASGIRequest = None
# try/except block exclusive for optional ASGI imports.
try:
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.asgi import (
asgi_getter,
asgi_setter,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.asgi import (
collect_custom_request_headers_attributes as asgi_collect_custom_request_attributes,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.asgi import (
collect_custom_response_headers_attributes as asgi_collect_custom_response_attributes,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.asgi import (
collect_request_attributes as asgi_collect_request_attributes,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.asgi import (
set_status_code,
)
_is_asgi_supported = True
except ImportError:
asgi_getter = None
asgi_collect_request_attributes = None
set_status_code = None
_is_asgi_supported = False
_logger = getLogger(__name__)
_attributes_by_preference = [
[
SpanAttributes.HTTP_SCHEME,
SpanAttributes.HTTP_HOST,
SpanAttributes.HTTP_TARGET,
],
[
SpanAttributes.HTTP_SCHEME,
SpanAttributes.HTTP_SERVER_NAME,
SpanAttributes.NET_HOST_PORT,
SpanAttributes.HTTP_TARGET,
],
[
SpanAttributes.HTTP_SCHEME,
SpanAttributes.NET_HOST_NAME,
SpanAttributes.NET_HOST_PORT,
SpanAttributes.HTTP_TARGET,
],
[SpanAttributes.HTTP_URL],
]
def _is_asgi_request(request: HttpRequest) -> bool:
return ASGIRequest is not None and isinstance(request, ASGIRequest)
class _DjangoMiddleware(MiddlewareMixin):
"""Django Middleware for OpenTelemetry"""
_environ_activation_key = (
"opentelemetry-instrumentor-django.activation_key"
)
_environ_token = "opentelemetry-instrumentor-django.token"
_environ_span_key = "opentelemetry-instrumentor-django.span_key"
_environ_exception_key = "opentelemetry-instrumentor-django.exception_key"
_environ_active_request_attr_key = (
"opentelemetry-instrumentor-django.active_request_attr_key"
)
_environ_duration_attr_key = (
"opentelemetry-instrumentor-django.duration_attr_key"
)
_environ_timer_key = "opentelemetry-instrumentor-django.timer_key"
_traced_request_attrs = get_traced_request_attrs("DJANGO")
_excluded_urls = get_excluded_urls("DJANGO")
_tracer = None
_meter = None
_duration_histogram = None
_active_request_counter = None
_otel_request_hook: Callable[[Span, HttpRequest], None] = None
_otel_response_hook: Callable[
[Span, HttpRequest, HttpResponse], None
] = None
@staticmethod
def _get_span_name(request):
try:
if getattr(request, "resolver_match"):
match = request.resolver_match
else:
match = resolve(request.path)
if hasattr(match, "route"):
return match.route
# Instead of using `view_name`, better to use `_func_name` as some applications can use similar
# view names in different modules
if hasattr(match, "_func_name"):
return match._func_name # pylint: disable=protected-access
# Fallback for safety as `_func_name` private field
return match.view_name
except Resolver404:
return f"HTTP {request.method}"
# pylint: disable=too-many-locals
def process_request(self, request):
# request.META is a dictionary containing all available HTTP headers
# Read more about request.META here:
# https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
return
is_asgi_request = _is_asgi_request(request)
if not _is_asgi_supported and is_asgi_request:
return
# pylint:disable=W0212
request._otel_start_time = time()
request_meta = request.META
if is_asgi_request:
carrier = request.scope
carrier_getter = asgi_getter
collect_request_attributes = asgi_collect_request_attributes
else:
carrier = request_meta
carrier_getter = wsgi_getter
collect_request_attributes = wsgi_collect_request_attributes
span, token = _start_internal_or_server_span(
tracer=self._tracer,
span_name=self._get_span_name(request),
start_time=request_meta.get(
"opentelemetry-instrumentor-django.starttime_key"
),
context_carrier=carrier,
context_getter=carrier_getter,
)
attributes = collect_request_attributes(carrier)
active_requests_count_attrs = _parse_active_request_count_attrs(
attributes
)
duration_attrs = _parse_duration_attrs(attributes)
request.META[
self._environ_active_request_attr_key
] = active_requests_count_attrs
request.META[self._environ_duration_attr_key] = duration_attrs
self._active_request_counter.add(1, active_requests_count_attrs)
if span.is_recording():
attributes = extract_attributes_from_object(
request, self._traced_request_attrs, attributes
)
if is_asgi_request:
# ASGI requests include extra attributes in request.scope.headers.
attributes = extract_attributes_from_object(
types.SimpleNamespace(
**{
name.decode("latin1"): value.decode("latin1")
for name, value in request.scope.get("headers", [])
}
),
self._traced_request_attrs,
attributes,
)
if span.is_recording() and span.kind == SpanKind.SERVER:
attributes.update(
asgi_collect_custom_request_attributes(carrier)
)
else:
if span.is_recording() and span.kind == SpanKind.SERVER:
custom_attributes = (
wsgi_collect_custom_request_headers_attributes(carrier)
)
if len(custom_attributes) > 0:
span.set_attributes(custom_attributes)
for key, value in attributes.items():
span.set_attribute(key, value)
activation = use_span(span, end_on_exit=True)
activation.__enter__() # pylint: disable=E1101
request_start_time = default_timer()
request.META[self._environ_timer_key] = request_start_time
request.META[self._environ_activation_key] = activation
request.META[self._environ_span_key] = span
if token:
request.META[self._environ_token] = token
if _DjangoMiddleware._otel_request_hook:
_DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable
span, request
)
# pylint: disable=unused-argument
def process_view(self, request, view_func, *args, **kwargs):
# Process view is executed before the view function, here we get the
# route template from request.resolver_match. It is not set yet in process_request
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
return
if (
self._environ_activation_key in request.META.keys()
and self._environ_span_key in request.META.keys()
):
span = request.META[self._environ_span_key]
if span.is_recording():
match = getattr(request, "resolver_match", None)
if match:
route = getattr(match, "route", None)
if route:
span.set_attribute(SpanAttributes.HTTP_ROUTE, route)
def process_exception(self, request, exception):
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
return
if self._environ_activation_key in request.META.keys():
request.META[self._environ_exception_key] = exception
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
def process_response(self, request, response):
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
return response
is_asgi_request = _is_asgi_request(request)
if not _is_asgi_supported and is_asgi_request:
return response
activation = request.META.pop(self._environ_activation_key, None)
span = request.META.pop(self._environ_span_key, None)
active_requests_count_attrs = request.META.pop(
self._environ_active_request_attr_key, None
)
duration_attrs = request.META.pop(
self._environ_duration_attr_key, None
)
if duration_attrs:
duration_attrs[
SpanAttributes.HTTP_STATUS_CODE
] = response.status_code
request_start_time = request.META.pop(self._environ_timer_key, None)
if activation and span:
if is_asgi_request:
set_status_code(span, response.status_code)
if span.is_recording() and span.kind == SpanKind.SERVER:
custom_headers = {}
for key, value in response.items():
asgi_setter.set(custom_headers, key, value)
custom_res_attributes = (
asgi_collect_custom_response_attributes(custom_headers)
)
for key, value in custom_res_attributes.items():
span.set_attribute(key, value)
else:
add_response_attributes(
span,
f"{response.status_code} {response.reason_phrase}",
response.items(),
)
if span.is_recording() and span.kind == SpanKind.SERVER:
custom_attributes = (
wsgi_collect_custom_response_headers_attributes(
response.items()
)
)
if len(custom_attributes) > 0:
span.set_attributes(custom_attributes)
propagator = get_global_response_propagator()
if propagator:
propagator.inject(response)
# record any exceptions raised while processing the request
exception = request.META.pop(self._environ_exception_key, None)
if _DjangoMiddleware._otel_response_hook:
_DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable
span, request, response
)
if exception:
activation.__exit__(
type(exception),
exception,
getattr(exception, "__traceback__", None),
)
else:
activation.__exit__(None, None, None)
if request_start_time is not None:
duration = max(
round((default_timer() - request_start_time) * 1000), 0
)
self._duration_histogram.record(duration, duration_attrs)
self._active_request_counter.add(-1, active_requests_count_attrs)
if request.META.get(self._environ_token, None) is not None:
detach(request.META.get(self._environ_token))
request.META.pop(self._environ_token)
return response

Просмотреть файл

@ -0,0 +1,123 @@
#!/usr/bin/python
#
# Copyright The 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.
from contextlib import ExitStack
from logging import getLogger
from typing import Any, Type, TypeVar
# pylint: disable=no-name-in-module
from django import conf, get_version
from django.db import connections
from django.db.backends.utils import CursorDebugWrapper
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.sqlcommenter_utils import (
_add_sql_comment,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_get_opentelemetry_values,
)
from opentelemetry.trace.propagation.tracecontext import (
TraceContextTextMapPropagator,
)
_propagator = TraceContextTextMapPropagator()
_django_version = get_version()
_logger = getLogger(__name__)
T = TypeVar("T") # pylint: disable-msg=invalid-name
class SqlCommenter:
"""
Middleware to append a comment to each database query with details about
the framework and the execution context.
"""
def __init__(self, get_response) -> None:
self.get_response = get_response
def __call__(self, request) -> Any:
with ExitStack() as stack:
for db_alias in connections:
stack.enter_context(
connections[db_alias].execute_wrapper(
_QueryWrapper(request)
)
)
return self.get_response(request)
class _QueryWrapper:
def __init__(self, request) -> None:
self.request = request
def __call__(self, execute: Type[T], sql, params, many, context) -> T:
# pylint: disable-msg=too-many-locals
with_framework = getattr(
conf.settings, "SQLCOMMENTER_WITH_FRAMEWORK", True
)
with_controller = getattr(
conf.settings, "SQLCOMMENTER_WITH_CONTROLLER", True
)
with_route = getattr(conf.settings, "SQLCOMMENTER_WITH_ROUTE", True)
with_app_name = getattr(
conf.settings, "SQLCOMMENTER_WITH_APP_NAME", True
)
with_opentelemetry = getattr(
conf.settings, "SQLCOMMENTER_WITH_OPENTELEMETRY", True
)
with_db_driver = getattr(
conf.settings, "SQLCOMMENTER_WITH_DB_DRIVER", True
)
db_driver = context["connection"].settings_dict.get("ENGINE", "")
resolver_match = self.request.resolver_match
sql = _add_sql_comment(
sql,
# Information about the controller.
controller=resolver_match.view_name
if resolver_match and with_controller
else None,
# route is the pattern that matched a request with a controller i.e. the regex
# See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.route
# getattr() because the attribute doesn't exist in Django < 2.2.
route=getattr(resolver_match, "route", None)
if resolver_match and with_route
else None,
# app_name is the application namespace for the URL pattern that matches the URL.
# See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.app_name
app_name=(resolver_match.app_name or None)
if resolver_match and with_app_name
else None,
# Framework centric information.
framework=f"django:{_django_version}" if with_framework else None,
# Information about the database and driver.
db_driver=db_driver if with_db_driver else None,
**_get_opentelemetry_values() if with_opentelemetry else {},
)
# TODO: MySQL truncates logs > 1024B so prepend comments
# instead of statements, if the engine is MySQL.
# See:
# * https://github.com/basecamp/marginalia/issues/61
# * https://github.com/basecamp/marginalia/pull/80
# Add the query to the query log if debugging.
if isinstance(context["cursor"], CursorDebugWrapper):
context["connection"].queries_log.append(sql)
return execute(sql, params, many, context)

Просмотреть файл

@ -0,0 +1,17 @@
# Copyright The 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.
_instruments = ("django >= 1.10",)
_supports_metrics = True

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -0,0 +1,18 @@
# Copyright The 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.
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"
"""
.. envvar:: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS
"""

Просмотреть файл

@ -0,0 +1,194 @@
# Copyright The 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 logging
import typing
from typing import Collection
import fastapi
from starlette.routing import Match
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.asgi import (
OpenTelemetryMiddleware,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.fastapi.package import (
_instruments,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.fastapi.version import (
__version__,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
from opentelemetry.metrics import get_meter
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http import (
get_excluded_urls,
parse_excluded_urls,
)
_excluded_urls_from_env = get_excluded_urls("FASTAPI")
_logger = logging.getLogger(__name__)
_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]]
class FastAPIInstrumentor(BaseInstrumentor):
"""An instrumentor for FastAPI
See `BaseInstrumentor`
"""
_original_fastapi = None
@staticmethod
def instrument_app(
app: fastapi.FastAPI,
server_request_hook: _ServerRequestHookT = None,
client_request_hook: _ClientRequestHookT = None,
client_response_hook: _ClientResponseHookT = None,
tracer_provider=None,
meter_provider=None,
excluded_urls=None,
):
"""Instrument an uninstrumented FastAPI application."""
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
app._is_instrumented_by_opentelemetry = False
if not getattr(app, "_is_instrumented_by_opentelemetry", False):
if excluded_urls is None:
excluded_urls = _excluded_urls_from_env
else:
excluded_urls = parse_excluded_urls(excluded_urls)
meter = get_meter(__name__, __version__, meter_provider)
app.add_middleware(
OpenTelemetryMiddleware,
excluded_urls=excluded_urls,
default_span_details=_get_route_details,
server_request_hook=server_request_hook,
client_request_hook=client_request_hook,
client_response_hook=client_response_hook,
tracer_provider=tracer_provider,
meter=meter,
)
app._is_instrumented_by_opentelemetry = True
if app not in _InstrumentedFastAPI._instrumented_fastapi_apps:
_InstrumentedFastAPI._instrumented_fastapi_apps.add(app)
else:
_logger.warning(
"Attempting to instrument FastAPI app while already instrumented"
)
@staticmethod
def uninstrument_app(app: fastapi.FastAPI):
app.user_middleware = [
x
for x in app.user_middleware
if x.cls is not OpenTelemetryMiddleware
]
app.middleware_stack = app.build_middleware_stack()
app._is_instrumented_by_opentelemetry = False
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments
def _instrument(self, **kwargs):
self._original_fastapi = fastapi.FastAPI
_InstrumentedFastAPI._tracer_provider = kwargs.get("tracer_provider")
_InstrumentedFastAPI._server_request_hook = kwargs.get(
"server_request_hook"
)
_InstrumentedFastAPI._client_request_hook = kwargs.get(
"client_request_hook"
)
_InstrumentedFastAPI._client_response_hook = kwargs.get(
"client_response_hook"
)
_excluded_urls = kwargs.get("excluded_urls")
_InstrumentedFastAPI._excluded_urls = (
_excluded_urls_from_env
if _excluded_urls is None
else parse_excluded_urls(_excluded_urls)
)
_InstrumentedFastAPI._meter_provider = kwargs.get("meter_provider")
fastapi.FastAPI = _InstrumentedFastAPI
def _uninstrument(self, **kwargs):
for instance in _InstrumentedFastAPI._instrumented_fastapi_apps:
self.uninstrument_app(instance)
_InstrumentedFastAPI._instrumented_fastapi_apps.clear()
fastapi.FastAPI = self._original_fastapi
class _InstrumentedFastAPI(fastapi.FastAPI):
_tracer_provider = None
_meter_provider = None
_excluded_urls = None
_server_request_hook: _ServerRequestHookT = None
_client_request_hook: _ClientRequestHookT = None
_client_response_hook: _ClientResponseHookT = None
_instrumented_fastapi_apps = set()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
meter = get_meter(
__name__, __version__, _InstrumentedFastAPI._meter_provider
)
self.add_middleware(
OpenTelemetryMiddleware,
excluded_urls=_InstrumentedFastAPI._excluded_urls,
default_span_details=_get_route_details,
server_request_hook=_InstrumentedFastAPI._server_request_hook,
client_request_hook=_InstrumentedFastAPI._client_request_hook,
client_response_hook=_InstrumentedFastAPI._client_response_hook,
tracer_provider=_InstrumentedFastAPI._tracer_provider,
meter=meter,
)
self._is_instrumented_by_opentelemetry = True
_InstrumentedFastAPI._instrumented_fastapi_apps.add(self)
def __del__(self):
if self in _InstrumentedFastAPI._instrumented_fastapi_apps:
_InstrumentedFastAPI._instrumented_fastapi_apps.remove(self)
def _get_route_details(scope):
"""Callback to retrieve the fastapi route being served.
TODO: there is currently no way to retrieve http.route from
a starlette application from scope.
See: https://github.com/encode/starlette/pull/804
"""
app = scope["app"]
route = None
for starlette_route in app.routes:
match, _ = starlette_route.matches(scope)
if match == Match.FULL:
route = starlette_route.path
break
if match == Match.PARTIAL:
route = starlette_route.path
# method only exists for http, if websocket
# leave it blank.
span_name = route or scope.get("method", "")
attributes = {}
if route:
attributes[SpanAttributes.HTTP_ROUTE] = route
return span_name, attributes

Просмотреть файл

@ -0,0 +1,18 @@
# Copyright The 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.
_instruments = ("fastapi ~= 0.58",)
_supports_metrics = True

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -0,0 +1,431 @@
# Copyright The 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.
# Note: This package is not named "flask" because of
# https://github.com/PyCQA/pylint/issues/2648
from logging import getLogger
from threading import get_ident
from time import time_ns
from timeit import default_timer
from typing import Collection
import flask
import azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.wsgi as otel_wsgi
from opentelemetry import context, trace
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.flask.package import (
_instruments,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.flask.version import (
__version__,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.propagators import (
get_global_response_propagator,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_start_internal_or_server_span,
)
from opentelemetry.metrics import get_meter
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.trace import SpanAttributes
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http import (
get_excluded_urls,
parse_excluded_urls,
)
_logger = getLogger(__name__)
_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
_ENVIRON_THREAD_ID_KEY = "opentelemetry-flask.thread_id_key"
_ENVIRON_TOKEN = "opentelemetry-flask.token"
_excluded_urls_from_env = get_excluded_urls("FLASK")
def get_default_span_name():
try:
span_name = flask.request.url_rule.rule
except AttributeError:
span_name = otel_wsgi.get_default_span_name(flask.request.environ)
return span_name
def _rewrapped_app(
wsgi_app,
active_requests_counter,
duration_histogram,
response_hook=None,
excluded_urls=None,
):
def _wrapped_app(wrapped_app_environ, start_response):
# We want to measure the time for route matching, etc.
# In theory, we could start the span here and use
# update_name later but that API is "highly discouraged" so
# we better avoid it.
wrapped_app_environ[_ENVIRON_STARTTIME_KEY] = time_ns()
start = default_timer()
attributes = otel_wsgi.collect_request_attributes(wrapped_app_environ)
active_requests_count_attrs = (
otel_wsgi._parse_active_request_count_attrs(attributes)
)
duration_attrs = otel_wsgi._parse_duration_attrs(attributes)
active_requests_counter.add(1, active_requests_count_attrs)
def _start_response(status, response_headers, *args, **kwargs):
if flask.request and (
excluded_urls is None
or not excluded_urls.url_disabled(flask.request.url)
):
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
propagator = get_global_response_propagator()
if propagator:
propagator.inject(
response_headers,
setter=otel_wsgi.default_response_propagation_setter,
)
if span:
otel_wsgi.add_response_attributes(
span, status, response_headers
)
status_code = otel_wsgi._parse_status_code(status)
if status_code is not None:
duration_attrs[
SpanAttributes.HTTP_STATUS_CODE
] = status_code
if (
span.is_recording()
and span.kind == trace.SpanKind.SERVER
):
custom_attributes = otel_wsgi.collect_custom_response_headers_attributes(
response_headers
)
if len(custom_attributes) > 0:
span.set_attributes(custom_attributes)
else:
_logger.warning(
"Flask environ's OpenTelemetry span "
"missing at _start_response(%s)",
status,
)
if response_hook is not None:
response_hook(span, status, response_headers)
return start_response(status, response_headers, *args, **kwargs)
result = wsgi_app(wrapped_app_environ, _start_response)
duration = max(round((default_timer() - start) * 1000), 0)
duration_histogram.record(duration, duration_attrs)
active_requests_counter.add(-1, active_requests_count_attrs)
return result
return _wrapped_app
def _wrapped_before_request(
request_hook=None,
tracer=None,
excluded_urls=None,
enable_commenter=True,
commenter_options=None,
):
def _before_request():
if excluded_urls and excluded_urls.url_disabled(flask.request.url):
return
flask_request_environ = flask.request.environ
span_name = get_default_span_name()
span, token = _start_internal_or_server_span(
tracer=tracer,
span_name=span_name,
start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY),
context_carrier=flask_request_environ,
context_getter=otel_wsgi.wsgi_getter,
)
if request_hook:
request_hook(span, flask_request_environ)
if span.is_recording():
attributes = otel_wsgi.collect_request_attributes(
flask_request_environ
)
if flask.request.url_rule:
# For 404 that result from no route found, etc, we
# don't have a url_rule.
attributes[
SpanAttributes.HTTP_ROUTE
] = flask.request.url_rule.rule
for key, value in attributes.items():
span.set_attribute(key, value)
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
custom_attributes = (
otel_wsgi.collect_custom_request_headers_attributes(
flask_request_environ
)
)
if len(custom_attributes) > 0:
span.set_attributes(custom_attributes)
activation = trace.use_span(span, end_on_exit=True)
activation.__enter__() # pylint: disable=E1101
flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation
flask_request_environ[_ENVIRON_THREAD_ID_KEY] = get_ident()
flask_request_environ[_ENVIRON_SPAN_KEY] = span
flask_request_environ[_ENVIRON_TOKEN] = token
if enable_commenter:
current_context = context.get_current()
flask_info = {}
# https://flask.palletsprojects.com/en/1.1.x/api/#flask.has_request_context
if flask and flask.request:
if commenter_options.get("framework", True):
flask_info["framework"] = f"flask:{flask.__version__}"
if (
commenter_options.get("controller", True)
and flask.request.endpoint
):
flask_info["controller"] = flask.request.endpoint
if (
commenter_options.get("route", True)
and flask.request.url_rule
and flask.request.url_rule.rule
):
flask_info["route"] = flask.request.url_rule.rule
sqlcommenter_context = context.set_value(
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", flask_info, current_context
)
context.attach(sqlcommenter_context)
return _before_request
def _wrapped_teardown_request(
excluded_urls=None,
):
def _teardown_request(exc):
# pylint: disable=E1101
if excluded_urls and excluded_urls.url_disabled(flask.request.url):
return
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
thread_id = flask.request.environ.get(_ENVIRON_THREAD_ID_KEY)
if not activation or thread_id != get_ident():
# This request didn't start a span, maybe because it was created in
# a way that doesn't run `before_request`, like when it is created
# with `app.test_request_context`.
#
# Similarly, check the thread_id against the current thread to ensure
# tear down only happens on the original thread. This situation can
# arise if the original thread handling the request spawn children
# threads and then uses something like copy_current_request_context
# to copy the request context.
return
if exc is None:
activation.__exit__(None, None, None)
else:
activation.__exit__(
type(exc), exc, getattr(exc, "__traceback__", None)
)
if flask.request.environ.get(_ENVIRON_TOKEN, None):
context.detach(flask.request.environ.get(_ENVIRON_TOKEN))
return _teardown_request
class _InstrumentedFlask(flask.Flask):
_excluded_urls = None
_tracer_provider = None
_request_hook = None
_response_hook = None
_enable_commenter = True
_commenter_options = None
_meter_provider = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._original_wsgi_app = self.wsgi_app
self._is_instrumented_by_opentelemetry = True
meter = get_meter(
__name__, __version__, _InstrumentedFlask._meter_provider
)
duration_histogram = meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="measures the duration of the inbound HTTP request",
)
active_requests_counter = meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
description="measures the number of concurrent HTTP requests that are currently in-flight",
)
self.wsgi_app = _rewrapped_app(
self.wsgi_app,
active_requests_counter,
duration_histogram,
_InstrumentedFlask._response_hook,
excluded_urls=_InstrumentedFlask._excluded_urls,
)
tracer = trace.get_tracer(
__name__, __version__, _InstrumentedFlask._tracer_provider
)
_before_request = _wrapped_before_request(
_InstrumentedFlask._request_hook,
tracer,
excluded_urls=_InstrumentedFlask._excluded_urls,
enable_commenter=_InstrumentedFlask._enable_commenter,
commenter_options=_InstrumentedFlask._commenter_options,
)
self._before_request = _before_request
self.before_request(_before_request)
_teardown_request = _wrapped_teardown_request(
excluded_urls=_InstrumentedFlask._excluded_urls,
)
self.teardown_request(_teardown_request)
class FlaskInstrumentor(BaseInstrumentor):
# pylint: disable=protected-access,attribute-defined-outside-init
"""An instrumentor for flask.Flask
See `BaseInstrumentor`
"""
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments
def _instrument(self, **kwargs):
self._original_flask = flask.Flask
request_hook = kwargs.get("request_hook")
response_hook = kwargs.get("response_hook")
if callable(request_hook):
_InstrumentedFlask._request_hook = request_hook
if callable(response_hook):
_InstrumentedFlask._response_hook = response_hook
tracer_provider = kwargs.get("tracer_provider")
_InstrumentedFlask._tracer_provider = tracer_provider
excluded_urls = kwargs.get("excluded_urls")
_InstrumentedFlask._excluded_urls = (
_excluded_urls_from_env
if excluded_urls is None
else parse_excluded_urls(excluded_urls)
)
enable_commenter = kwargs.get("enable_commenter", True)
_InstrumentedFlask._enable_commenter = enable_commenter
commenter_options = kwargs.get("commenter_options", {})
_InstrumentedFlask._commenter_options = commenter_options
meter_provider = kwargs.get("meter_provider")
_InstrumentedFlask._meter_provider = meter_provider
flask.Flask = _InstrumentedFlask
def _uninstrument(self, **kwargs):
flask.Flask = self._original_flask
@staticmethod
def instrument_app(
app,
request_hook=None,
response_hook=None,
tracer_provider=None,
excluded_urls=None,
enable_commenter=True,
commenter_options=None,
meter_provider=None,
):
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
app._is_instrumented_by_opentelemetry = False
if not app._is_instrumented_by_opentelemetry:
excluded_urls = (
parse_excluded_urls(excluded_urls)
if excluded_urls is not None
else _excluded_urls_from_env
)
meter = get_meter(__name__, __version__, meter_provider)
duration_histogram = meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="measures the duration of the inbound HTTP request",
)
active_requests_counter = meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
description="measures the number of concurrent HTTP requests that are currently in-flight",
)
app._original_wsgi_app = app.wsgi_app
app.wsgi_app = _rewrapped_app(
app.wsgi_app,
active_requests_counter,
duration_histogram,
response_hook,
excluded_urls=excluded_urls,
)
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
_before_request = _wrapped_before_request(
request_hook,
tracer,
excluded_urls=excluded_urls,
enable_commenter=enable_commenter,
commenter_options=commenter_options
if commenter_options
else {},
)
app._before_request = _before_request
app.before_request(_before_request)
_teardown_request = _wrapped_teardown_request(
excluded_urls=excluded_urls,
)
app._teardown_request = _teardown_request
app.teardown_request(_teardown_request)
app._is_instrumented_by_opentelemetry = True
else:
_logger.warning(
"Attempting to instrument Flask app while already instrumented"
)
@staticmethod
def uninstrument_app(app):
if hasattr(app, "_original_wsgi_app"):
app.wsgi_app = app._original_wsgi_app
# FIXME add support for other Flask blueprints that are not None
app.before_request_funcs[None].remove(app._before_request)
app.teardown_request_funcs[None].remove(app._teardown_request)
del app._original_wsgi_app
app._is_instrumented_by_opentelemetry = False
else:
_logger.warning(
"Attempting to uninstrument Flask "
"app while already uninstrumented"
)

Просмотреть файл

@ -0,0 +1,18 @@
# Copyright The 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.
_instruments = ("flask >= 1.0, < 3.0",)
_supports_metrics = True

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -0,0 +1,131 @@
# Copyright The 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.
# type: ignore
"""
OpenTelemetry Base Instrumentor
"""
from abc import ABC, abstractmethod
from logging import getLogger
from typing import Collection, Optional
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.dependencies import (
DependencyConflict,
get_dependency_conflicts,
)
_LOG = getLogger(__name__)
class BaseInstrumentor(ABC):
"""An ABC for instrumentors
Child classes of this ABC should instrument specific third
party libraries or frameworks either by using the
``opentelemetry-instrument`` command or by calling their methods
directly.
Since every third party library or framework is different and has different
instrumentation needs, more methods can be added to the child classes as
needed to provide practical instrumentation to the end user.
"""
_instance = None
_is_instrumented_by_opentelemetry = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = object.__new__(cls)
return cls._instance
@property
def is_instrumented_by_opentelemetry(self):
return self._is_instrumented_by_opentelemetry
@abstractmethod
def instrumentation_dependencies(self) -> Collection[str]:
"""Return a list of python packages with versions that the will be instrumented.
The format should be the same as used in requirements.txt or pyproject.toml.
For example, if an instrumentation instruments requests 1.x, this method should look
like:
def instrumentation_dependencies(self) -> Collection[str]:
return ['requests ~= 1.0']
This will ensure that the instrumentation will only be used when the specified library
is present in the environment.
"""
def _instrument(self, **kwargs):
"""Instrument the library"""
@abstractmethod
def _uninstrument(self, **kwargs):
"""Uninstrument the library"""
def _check_dependency_conflicts(self) -> Optional[DependencyConflict]:
dependencies = self.instrumentation_dependencies()
return get_dependency_conflicts(dependencies)
def instrument(self, **kwargs):
"""Instrument the library
This method will be called without any optional arguments by the
``opentelemetry-instrument`` command.
This means that calling this method directly without passing any
optional values should do the very same thing that the
``opentelemetry-instrument`` command does.
"""
if self._is_instrumented_by_opentelemetry:
_LOG.warning("Attempting to instrument while already instrumented")
return None
# check if instrumentor has any missing or conflicting dependencies
skip_dep_check = kwargs.pop("skip_dep_check", False)
if not skip_dep_check:
conflict = self._check_dependency_conflicts()
if conflict:
_LOG.error(conflict)
return None
result = self._instrument( # pylint: disable=assignment-from-no-return
**kwargs
)
self._is_instrumented_by_opentelemetry = True
return result
def uninstrument(self, **kwargs):
"""Uninstrument the library
See ``BaseInstrumentor.instrument`` for more information regarding the
usage of ``kwargs``.
"""
if self._is_instrumented_by_opentelemetry:
result = self._uninstrument(**kwargs)
self._is_instrumented_by_opentelemetry = False
return result
_LOG.warning("Attempting to uninstrument while already uninstrumented")
return None
__all__ = ["BaseInstrumentor"]

Просмотреть файл

@ -0,0 +1,124 @@
# Copyright The 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.
"""
This module implements experimental propagators to inject trace context
into response carriers. This is useful for server side frameworks that start traces
when server requests and want to share the trace context with the client so the
client can add its spans to the same trace.
This is part of an upcoming W3C spec and will eventually make it to the Otel spec.
https://w3c.github.io/trace-context/#trace-context-http-response-headers-format
"""
import typing
from abc import ABC, abstractmethod
from opentelemetry import trace
from opentelemetry.context.context import Context
from opentelemetry.propagators import textmap
from opentelemetry.trace import format_span_id, format_trace_id
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"
_RESPONSE_PROPAGATOR = None
def get_global_response_propagator():
return _RESPONSE_PROPAGATOR
def set_global_response_propagator(propagator):
global _RESPONSE_PROPAGATOR # pylint:disable=global-statement
_RESPONSE_PROPAGATOR = propagator
class Setter(ABC):
@abstractmethod
def set(self, carrier, key, value):
"""Inject the provided key value pair in carrier."""
class DictHeaderSetter(Setter):
def set(self, carrier, key, value): # pylint: disable=no-self-use
old_value = carrier.get(key, "")
if old_value:
value = f"{old_value}, {value}"
carrier[key] = value
class FuncSetter(Setter):
"""FuncSetter coverts a function into a valid Setter. Any function that can
set values in a carrier can be converted into a Setter by using FuncSetter.
This is useful when injecting trace context into non-dict objects such
HTTP Response objects for different framework.
For example, it can be used to create a setter for Falcon response object as:
setter = FuncSetter(falcon.api.Response.append_header)
and then used with the propagator as:
propagator.inject(falcon_response, setter=setter)
This would essentially make the propagator call `falcon_response.append_header(key, value)`
"""
def __init__(self, func):
self._func = func
def set(self, carrier, key, value):
self._func(carrier, key, value)
default_setter = DictHeaderSetter()
class ResponsePropagator(ABC):
@abstractmethod
def inject(
self,
carrier: textmap.CarrierT,
context: typing.Optional[Context] = None,
setter: textmap.Setter = default_setter,
) -> None:
"""Injects SpanContext into the HTTP response carrier."""
class TraceResponsePropagator(ResponsePropagator):
"""Experimental propagator that injects tracecontext into HTTP responses."""
def inject(
self,
carrier: textmap.CarrierT,
context: typing.Optional[Context] = None,
setter: textmap.Setter = default_setter,
) -> None:
"""Injects SpanContext into the HTTP response carrier."""
span = trace.get_current_span(context)
span_context = span.get_span_context()
if span_context == trace.INVALID_SPAN_CONTEXT:
return
header_name = "traceresponse"
setter.set(
carrier,
header_name,
f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}",
)
setter.set(
carrier,
_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
header_name,
)

Просмотреть файл

@ -0,0 +1,183 @@
# Copyright The 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 logging
import typing
from typing import Collection
import psycopg2
from psycopg2.extensions import (
cursor as pg_cursor, # pylint: disable=no-name-in-module
)
from psycopg2.sql import Composed # pylint: disable=no-name-in-module
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation import (
dbapi,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.psycopg2.package import (
_instruments,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.psycopg2.version import (
__version__,
)
_logger = logging.getLogger(__name__)
_OTEL_CURSOR_FACTORY_KEY = "_otel_orig_cursor_factory"
class Psycopg2Instrumentor(BaseInstrumentor):
_CONNECTION_ATTRIBUTES = {
"database": "info.dbname",
"port": "info.port",
"host": "info.host",
"user": "info.user",
}
_DATABASE_SYSTEM = "postgresql"
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments
def _instrument(self, **kwargs):
"""Integrate with PostgreSQL Psycopg library.
Psycopg: http://initd.org/psycopg/
"""
tracer_provider = kwargs.get("tracer_provider")
enable_sqlcommenter = kwargs.get("enable_commenter", False)
commenter_options = kwargs.get("commenter_options", {})
dbapi.wrap_connect(
__name__,
psycopg2,
"connect",
self._DATABASE_SYSTEM,
self._CONNECTION_ATTRIBUTES,
version=__version__,
tracer_provider=tracer_provider,
db_api_integration_factory=DatabaseApiIntegration,
enable_commenter=enable_sqlcommenter,
commenter_options=commenter_options,
)
def _uninstrument(self, **kwargs):
""" "Disable Psycopg2 instrumentation"""
dbapi.unwrap_connect(psycopg2, "connect")
# TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
@staticmethod
def instrument_connection(connection, tracer_provider=None):
if not hasattr(connection, "_is_instrumented_by_opentelemetry"):
connection._is_instrumented_by_opentelemetry = False
if not connection._is_instrumented_by_opentelemetry:
setattr(
connection, _OTEL_CURSOR_FACTORY_KEY, connection.cursor_factory
)
connection.cursor_factory = _new_cursor_factory(
tracer_provider=tracer_provider
)
connection._is_instrumented_by_opentelemetry = True
else:
_logger.warning(
"Attempting to instrument Psycopg connection while already instrumented"
)
return connection
# TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
@staticmethod
def uninstrument_connection(connection):
connection.cursor_factory = getattr(
connection, _OTEL_CURSOR_FACTORY_KEY, None
)
return connection
# TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
class DatabaseApiIntegration(dbapi.DatabaseApiIntegration):
def wrapped_connection(
self,
connect_method: typing.Callable[..., typing.Any],
args: typing.Tuple[typing.Any, typing.Any],
kwargs: typing.Dict[typing.Any, typing.Any],
):
"""Add object proxy to connection object."""
base_cursor_factory = kwargs.pop("cursor_factory", None)
new_factory_kwargs = {"db_api": self}
if base_cursor_factory:
new_factory_kwargs["base_factory"] = base_cursor_factory
kwargs["cursor_factory"] = _new_cursor_factory(**new_factory_kwargs)
connection = connect_method(*args, **kwargs)
self.get_connection_attributes(connection)
return connection
class CursorTracer(dbapi.CursorTracer):
def get_operation_name(self, cursor, args):
if not args:
return ""
statement = args[0]
if isinstance(statement, Composed):
statement = statement.as_string(cursor)
if isinstance(statement, str):
# Strip leading comments so we get the operation name.
return self._leading_comment_remover.sub("", statement).split()[0]
return ""
def get_statement(self, cursor, args):
if not args:
return ""
statement = args[0]
if isinstance(statement, Composed):
statement = statement.as_string(cursor)
return statement
def _new_cursor_factory(db_api=None, base_factory=None, tracer_provider=None):
if not db_api:
db_api = DatabaseApiIntegration(
__name__,
Psycopg2Instrumentor._DATABASE_SYSTEM,
connection_attributes=Psycopg2Instrumentor._CONNECTION_ATTRIBUTES,
version=__version__,
tracer_provider=tracer_provider,
)
base_factory = base_factory or pg_cursor
_cursor_tracer = CursorTracer(db_api)
class TracedCursorFactory(base_factory):
def execute(self, *args, **kwargs):
return _cursor_tracer.traced_execution(
self, super().execute, *args, **kwargs
)
def executemany(self, *args, **kwargs):
return _cursor_tracer.traced_execution(
self, super().executemany, *args, **kwargs
)
def callproc(self, *args, **kwargs):
return _cursor_tracer.traced_execution(
self, super().callproc, *args, **kwargs
)
return TracedCursorFactory

Просмотреть файл

@ -0,0 +1,16 @@
# Copyright The 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.
_instruments = ("psycopg2 >= 2.7.3.1",)

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -0,0 +1,273 @@
# Copyright The 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 functools
import types
from timeit import default_timer
from typing import Callable, Collection, Iterable, Optional
from urllib.parse import urlparse
from requests.models import PreparedRequest, Response
from requests.sessions import Session
from requests.structures import CaseInsensitiveDict
from opentelemetry import context
# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined.
from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.requests.package import (
_instruments,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.requests.version import (
__version__,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_SUPPRESS_INSTRUMENTATION_KEY,
http_status_to_status_code,
)
from opentelemetry.metrics import Histogram, get_meter
from opentelemetry.propagate import inject
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import SpanKind, Tracer, get_tracer
from opentelemetry.trace.span import Span
from opentelemetry.trace.status import Status
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http import (
get_excluded_urls,
parse_excluded_urls,
remove_url_credentials,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http.httplib import (
set_ip_on_next_http_connection,
)
_excluded_urls_from_env = get_excluded_urls("REQUESTS")
_RequestHookT = Optional[Callable[[Span, PreparedRequest], None]]
_ResponseHookT = Optional[Callable[[Span, PreparedRequest], None]]
# pylint: disable=unused-argument
# pylint: disable=R0915
def _instrument(
tracer: Tracer,
duration_histogram: Histogram,
request_hook: _RequestHookT = None,
response_hook: _ResponseHookT = None,
excluded_urls: Iterable[str] = None,
):
"""Enables tracing of all requests calls that go through
:code:`requests.session.Session.request` (this includes
:code:`requests.get`, etc.)."""
# Since
# https://github.com/psf/requests/commit/d72d1162142d1bf8b1b5711c664fbbd674f349d1
# (v0.7.0, Oct 23, 2011), get, post, etc are implemented via request which
# again, is implemented via Session.request (`Session` was named `session`
# before v1.0.0, Dec 17, 2012, see
# https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120)
wrapped_send = Session.send
# pylint: disable-msg=too-many-locals,too-many-branches
@functools.wraps(wrapped_send)
def instrumented_send(self, request, **kwargs):
if excluded_urls and excluded_urls.url_disabled(request.url):
return wrapped_send(self, request, **kwargs)
def get_or_create_headers():
request.headers = (
request.headers
if request.headers is not None
else CaseInsensitiveDict()
)
return request.headers
if context.get_value(
_SUPPRESS_INSTRUMENTATION_KEY
) or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY):
return wrapped_send(self, request, **kwargs)
# See
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-client
method = request.method.upper()
span_name = get_default_span_name(method)
url = remove_url_credentials(request.url)
span_attributes = {
SpanAttributes.HTTP_METHOD: method,
SpanAttributes.HTTP_URL: url,
}
metric_labels = {
SpanAttributes.HTTP_METHOD: method,
}
try:
parsed_url = urlparse(url)
metric_labels[SpanAttributes.HTTP_SCHEME] = parsed_url.scheme
if parsed_url.hostname:
metric_labels[SpanAttributes.HTTP_HOST] = parsed_url.hostname
metric_labels[
SpanAttributes.NET_PEER_NAME
] = parsed_url.hostname
if parsed_url.port:
metric_labels[SpanAttributes.NET_PEER_PORT] = parsed_url.port
except ValueError:
pass
with tracer.start_as_current_span(
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
) as span, set_ip_on_next_http_connection(span):
exception = None
if callable(request_hook):
request_hook(span, request)
headers = get_or_create_headers()
inject(headers)
token = context.attach(
context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True)
)
start_time = default_timer()
try:
result = wrapped_send(self, request, **kwargs) # *** PROCEED
except Exception as exc: # pylint: disable=W0703
exception = exc
result = getattr(exc, "response", None)
finally:
elapsed_time = max(
round((default_timer() - start_time) * 1000), 0
)
context.detach(token)
if isinstance(result, Response):
if span.is_recording():
span.set_attribute(
SpanAttributes.HTTP_STATUS_CODE, result.status_code
)
span.set_status(
Status(http_status_to_status_code(result.status_code))
)
metric_labels[
SpanAttributes.HTTP_STATUS_CODE
] = result.status_code
if result.raw is not None:
version = getattr(result.raw, "version", None)
if version:
metric_labels[SpanAttributes.HTTP_FLAVOR] = (
"1.1" if version == 11 else "1.0"
)
if callable(response_hook):
response_hook(span, request, result)
duration_histogram.record(elapsed_time, attributes=metric_labels)
if exception is not None:
raise exception.with_traceback(exception.__traceback__)
return result
instrumented_send.opentelemetry_instrumentation_requests_applied = True
Session.send = instrumented_send
def _uninstrument():
"""Disables instrumentation of :code:`requests` through this module.
Note that this only works if no other module also patches requests."""
_uninstrument_from(Session)
def _uninstrument_from(instr_root, restore_as_bound_func=False):
for instr_func_name in ("request", "send"):
instr_func = getattr(instr_root, instr_func_name)
if not getattr(
instr_func,
"opentelemetry_instrumentation_requests_applied",
False,
):
continue
original = instr_func.__wrapped__ # pylint:disable=no-member
if restore_as_bound_func:
original = types.MethodType(original, instr_root)
setattr(instr_root, instr_func_name, original)
def get_default_span_name(method):
"""Default implementation for name_callback, returns HTTP {method_name}."""
return f"HTTP {method.strip()}"
class RequestsInstrumentor(BaseInstrumentor):
"""An instrumentor for requests
See `BaseInstrumentor`
"""
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments
def _instrument(self, **kwargs):
"""Instruments requests module
Args:
**kwargs: Optional arguments
``tracer_provider``: a TracerProvider, defaults to global
``request_hook``: An optional callback that is invoked right after a span is created.
``response_hook``: An optional callback which is invoked right before the span is finished processing a response.
``excluded_urls``: A string containing a comma-delimited
list of regexes used to exclude URLs from tracking
"""
tracer_provider = kwargs.get("tracer_provider")
tracer = get_tracer(__name__, __version__, tracer_provider)
excluded_urls = kwargs.get("excluded_urls")
meter_provider = kwargs.get("meter_provider")
meter = get_meter(
__name__,
__version__,
meter_provider,
)
duration_histogram = meter.create_histogram(
name=MetricInstruments.HTTP_CLIENT_DURATION,
unit="ms",
description="measures the duration of the outbound HTTP request",
)
_instrument(
tracer,
duration_histogram,
request_hook=kwargs.get("request_hook"),
response_hook=kwargs.get("response_hook"),
excluded_urls=_excluded_urls_from_env
if excluded_urls is None
else parse_excluded_urls(excluded_urls),
)
def _uninstrument(self, **kwargs):
_uninstrument()
@staticmethod
def uninstrument_session(session):
"""Disables instrumentation on the session object."""
_uninstrument_from(session, restore_as_bound_func=True)

Просмотреть файл

@ -0,0 +1,18 @@
# Copyright The 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.
_instruments = ("requests ~= 2.0",)
_supports_metrics = True

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -0,0 +1,68 @@
# Copyright The 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.
from opentelemetry import context
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_url_quote,
)
def _add_sql_comment(sql, **meta) -> str:
"""
Appends comments to the sql statement and returns it
"""
meta.update(**_add_framework_tags())
comment = _generate_sql_comment(**meta)
sql = sql.rstrip()
if sql[-1] == ";":
sql = sql[:-1] + comment + ";"
else:
sql = sql + comment
return sql
def _generate_sql_comment(**meta) -> str:
"""
Return a SQL comment with comma delimited key=value pairs created from
**meta kwargs.
"""
key_value_delimiter = ","
if not meta: # No entries added.
return ""
# Sort the keywords to ensure that caching works and that testing is
# deterministic. It eases visual inspection as well.
return (
" /*"
+ key_value_delimiter.join(
f"{_url_quote(key)}={_url_quote(value)!r}"
for key, value in sorted(meta.items())
if value is not None
)
+ "*/"
)
def _add_framework_tags() -> dict:
"""
Returns orm related tags if any set by the context
"""
sqlcommenter_framework_values = (
context.get_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES")
if context.get_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES")
else {}
)
return sqlcommenter_framework_values

Просмотреть файл

@ -0,0 +1,265 @@
# Copyright The 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 functools
import types
import typing
from http import client
from timeit import default_timer
from typing import Collection, Dict
from urllib.request import ( # pylint: disable=no-name-in-module,import-error
OpenerDirector,
Request,
)
from opentelemetry import context
# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined.
from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.urllib.package import (
_instruments,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.urllib.version import (
__version__,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_SUPPRESS_INSTRUMENTATION_KEY,
http_status_to_status_code,
)
from opentelemetry.metrics import Histogram, get_meter
from opentelemetry.propagate import inject
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span, SpanKind, get_tracer
from opentelemetry.trace.status import Status
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http import (
remove_url_credentials,
)
_RequestHookT = typing.Optional[typing.Callable[[Span, Request], None]]
_ResponseHookT = typing.Optional[
typing.Callable[[Span, Request, client.HTTPResponse], None]
]
class URLLibInstrumentor(BaseInstrumentor):
"""An instrumentor for urllib
See `BaseInstrumentor`
"""
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments
def _instrument(self, **kwargs):
"""Instruments urllib module
Args:
**kwargs: Optional arguments
``tracer_provider``: a TracerProvider, defaults to global
``request_hook``: An optional callback invoked that is invoked right after a span is created.
``response_hook``: An optional callback which is invoked right before the span is finished processing a response
"""
tracer_provider = kwargs.get("tracer_provider")
tracer = get_tracer(__name__, __version__, tracer_provider)
meter_provider = kwargs.get("meter_provider")
meter = get_meter(__name__, __version__, meter_provider)
histograms = _create_client_histograms(meter)
_instrument(
tracer,
histograms,
request_hook=kwargs.get("request_hook"),
response_hook=kwargs.get("response_hook"),
)
def _uninstrument(self, **kwargs):
_uninstrument()
def uninstrument_opener(
self, opener: OpenerDirector
): # pylint: disable=no-self-use
"""uninstrument_opener a specific instance of urllib.request.OpenerDirector"""
_uninstrument_from(opener, restore_as_bound_func=True)
def _instrument(
tracer,
histograms: Dict[str, Histogram],
request_hook: _RequestHookT = None,
response_hook: _ResponseHookT = None,
):
"""Enables tracing of all requests calls that go through
:code:`urllib.Client._make_request`"""
opener_open = OpenerDirector.open
@functools.wraps(opener_open)
def instrumented_open(opener, fullurl, data=None, timeout=None):
if isinstance(fullurl, str):
request_ = Request(fullurl, data)
else:
request_ = fullurl
def get_or_create_headers():
return getattr(request_, "headers", {})
def call_wrapped():
return opener_open(opener, request_, data=data, timeout=timeout)
return _instrumented_open_call(
opener, request_, call_wrapped, get_or_create_headers
)
def _instrumented_open_call(
_, request, call_wrapped, get_or_create_headers
): # pylint: disable=too-many-locals
if context.get_value(
_SUPPRESS_INSTRUMENTATION_KEY
) or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY):
return call_wrapped()
method = request.get_method().upper()
url = request.full_url
span_name = f"HTTP {method}".strip()
url = remove_url_credentials(url)
labels = {
SpanAttributes.HTTP_METHOD: method,
SpanAttributes.HTTP_URL: url,
}
with tracer.start_as_current_span(
span_name, kind=SpanKind.CLIENT, attributes=labels
) as span:
exception = None
if callable(request_hook):
request_hook(span, request)
headers = get_or_create_headers()
inject(headers)
token = context.attach(
context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True)
)
try:
start_time = default_timer()
result = call_wrapped() # *** PROCEED
except Exception as exc: # pylint: disable=W0703
exception = exc
result = getattr(exc, "file", None)
finally:
elapsed_time = round((default_timer() - start_time) * 1000)
context.detach(token)
if result is not None:
code_ = result.getcode()
labels[SpanAttributes.HTTP_STATUS_CODE] = str(code_)
if span.is_recording() and code_ is not None:
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, code_)
span.set_status(Status(http_status_to_status_code(code_)))
ver_ = str(getattr(result, "version", ""))
if ver_:
labels[
SpanAttributes.HTTP_FLAVOR
] = f"{ver_[:1]}.{ver_[:-1]}"
_record_histograms(
histograms, labels, request, result, elapsed_time
)
if callable(response_hook):
response_hook(span, request, result)
if exception is not None:
raise exception.with_traceback(exception.__traceback__)
return result
instrumented_open.opentelemetry_instrumentation_urllib_applied = True
OpenerDirector.open = instrumented_open
def _uninstrument():
"""Disables instrumentation of :code:`urllib` through this module.
Note that this only works if no other module also patches urllib."""
_uninstrument_from(OpenerDirector)
def _uninstrument_from(instr_root, restore_as_bound_func=False):
instr_func_name = "open"
instr_func = getattr(instr_root, instr_func_name)
if not getattr(
instr_func,
"opentelemetry_instrumentation_urllib_applied",
False,
):
return
original = instr_func.__wrapped__ # pylint:disable=no-member
if restore_as_bound_func:
original = types.MethodType(original, instr_root)
setattr(instr_root, instr_func_name, original)
def _create_client_histograms(meter) -> Dict[str, Histogram]:
histograms = {
MetricInstruments.HTTP_CLIENT_DURATION: meter.create_histogram(
name=MetricInstruments.HTTP_CLIENT_DURATION,
unit="ms",
description="measures the duration outbound HTTP requests",
),
MetricInstruments.HTTP_CLIENT_REQUEST_SIZE: meter.create_histogram(
name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE,
unit="By",
description="measures the size of HTTP request messages (compressed)",
),
MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE: meter.create_histogram(
name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE,
unit="By",
description="measures the size of HTTP response messages (compressed)",
),
}
return histograms
def _record_histograms(
histograms, metric_attributes, request, response, elapsed_time
):
histograms[MetricInstruments.HTTP_CLIENT_DURATION].record(
elapsed_time, attributes=metric_attributes
)
data = getattr(request, "data", None)
request_size = 0 if data is None else len(data)
histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE].record(
request_size, attributes=metric_attributes
)
if response is not None:
response_size = int(response.headers.get("Content-Length", 0))
histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE].record(
response_size, attributes=metric_attributes
)

Просмотреть файл

@ -0,0 +1,18 @@
# Copyright The 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.
_instruments = tuple()
_supports_metrics = True

Просмотреть файл

@ -0,0 +1,17 @@
# Copyright The 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.38b0"
_instruments = tuple()

Просмотреть файл

@ -0,0 +1,312 @@
# Copyright The 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 collections.abc
import contextlib
import io
import typing
from timeit import default_timer
from typing import Collection
import urllib3.connectionpool
import wrapt
from opentelemetry import context
# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined.
from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.urllib3.package import (
_instruments,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.urllib3.version import (
__version__,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_SUPPRESS_INSTRUMENTATION_KEY,
http_status_to_status_code,
unwrap,
)
from opentelemetry.metrics import Histogram, get_meter
from opentelemetry.propagate import inject
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer
from opentelemetry.trace.status import Status
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http.httplib import (
set_ip_on_next_http_connection,
)
_UrlFilterT = typing.Optional[typing.Callable[[str], str]]
_RequestHookT = typing.Optional[
typing.Callable[
[
Span,
urllib3.connectionpool.HTTPConnectionPool,
typing.Dict,
typing.Optional[str],
],
None,
]
]
_ResponseHookT = typing.Optional[
typing.Callable[
[
Span,
urllib3.connectionpool.HTTPConnectionPool,
urllib3.response.HTTPResponse,
],
None,
]
]
_URL_OPEN_ARG_TO_INDEX_MAPPING = {
"method": 0,
"url": 1,
"body": 2,
}
class URLLib3Instrumentor(BaseInstrumentor):
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments
def _instrument(self, **kwargs):
"""Instruments the urllib3 module
Args:
**kwargs: Optional arguments
``tracer_provider``: a TracerProvider, defaults to global.
``request_hook``: An optional callback that is invoked right after a span is created.
``response_hook``: An optional callback which is invoked right before the span is finished processing a response.
``url_filter``: A callback to process the requested URL prior
to adding it as a span attribute.
"""
tracer_provider = kwargs.get("tracer_provider")
tracer = get_tracer(__name__, __version__, tracer_provider)
meter_provider = kwargs.get("meter_provider")
meter = get_meter(__name__, __version__, meter_provider)
duration_histogram = meter.create_histogram(
name=MetricInstruments.HTTP_CLIENT_DURATION,
unit="ms",
description="measures the duration outbound HTTP requests",
)
request_size_histogram = meter.create_histogram(
name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE,
unit="By",
description="measures the size of HTTP request messages (compressed)",
)
response_size_histogram = meter.create_histogram(
name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE,
unit="By",
description="measures the size of HTTP response messages (compressed)",
)
_instrument(
tracer,
duration_histogram,
request_size_histogram,
response_size_histogram,
request_hook=kwargs.get("request_hook"),
response_hook=kwargs.get("response_hook"),
url_filter=kwargs.get("url_filter"),
)
def _uninstrument(self, **kwargs):
_uninstrument()
def _instrument(
tracer: Tracer,
duration_histogram: Histogram,
request_size_histogram: Histogram,
response_size_histogram: Histogram,
request_hook: _RequestHookT = None,
response_hook: _ResponseHookT = None,
url_filter: _UrlFilterT = None,
):
def instrumented_urlopen(wrapped, instance, args, kwargs):
if _is_instrumentation_suppressed():
return wrapped(*args, **kwargs)
method = _get_url_open_arg("method", args, kwargs).upper()
url = _get_url(instance, args, kwargs, url_filter)
headers = _prepare_headers(kwargs)
body = _get_url_open_arg("body", args, kwargs)
span_name = f"HTTP {method.strip()}"
span_attributes = {
SpanAttributes.HTTP_METHOD: method,
SpanAttributes.HTTP_URL: url,
}
with tracer.start_as_current_span(
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
) as span, set_ip_on_next_http_connection(span):
if callable(request_hook):
request_hook(span, instance, headers, body)
inject(headers)
with _suppress_further_instrumentation():
start_time = default_timer()
response = wrapped(*args, **kwargs)
elapsed_time = round((default_timer() - start_time) * 1000)
_apply_response(span, response)
if callable(response_hook):
response_hook(span, instance, response)
request_size = _get_body_size(body)
response_size = int(response.headers.get("Content-Length", 0))
metric_attributes = _create_metric_attributes(
instance, response, method
)
duration_histogram.record(
elapsed_time, attributes=metric_attributes
)
if request_size is not None:
request_size_histogram.record(
request_size, attributes=metric_attributes
)
response_size_histogram.record(
response_size, attributes=metric_attributes
)
return response
wrapt.wrap_function_wrapper(
urllib3.connectionpool.HTTPConnectionPool,
"urlopen",
instrumented_urlopen,
)
def _get_url_open_arg(name: str, args: typing.List, kwargs: typing.Mapping):
arg_idx = _URL_OPEN_ARG_TO_INDEX_MAPPING.get(name)
if arg_idx is not None:
try:
return args[arg_idx]
except IndexError:
pass
return kwargs.get(name)
def _get_url(
instance: urllib3.connectionpool.HTTPConnectionPool,
args: typing.List,
kwargs: typing.Mapping,
url_filter: _UrlFilterT,
) -> str:
url_or_path = _get_url_open_arg("url", args, kwargs)
if not url_or_path.startswith("/"):
url = url_or_path
else:
url = instance.scheme + "://" + instance.host
if _should_append_port(instance.scheme, instance.port):
url += ":" + str(instance.port)
url += url_or_path
if url_filter:
return url_filter(url)
return url
def _get_body_size(body: object) -> typing.Optional[int]:
if body is None:
return 0
if isinstance(body, collections.abc.Sized):
return len(body)
if isinstance(body, io.BytesIO):
return body.getbuffer().nbytes
return None
def _should_append_port(scheme: str, port: typing.Optional[int]) -> bool:
if not port:
return False
if scheme == "http" and port == 80:
return False
if scheme == "https" and port == 443:
return False
return True
def _prepare_headers(urlopen_kwargs: typing.Dict) -> typing.Dict:
headers = urlopen_kwargs.get("headers")
# avoid modifying original headers on inject
headers = headers.copy() if headers is not None else {}
urlopen_kwargs["headers"] = headers
return headers
def _apply_response(span: Span, response: urllib3.response.HTTPResponse):
if not span.is_recording():
return
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status)
span.set_status(Status(http_status_to_status_code(response.status)))
def _is_instrumentation_suppressed() -> bool:
return bool(
context.get_value(_SUPPRESS_INSTRUMENTATION_KEY)
or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)
)
def _create_metric_attributes(
instance: urllib3.connectionpool.HTTPConnectionPool,
response: urllib3.response.HTTPResponse,
method: str,
) -> dict:
metric_attributes = {
SpanAttributes.HTTP_METHOD: method,
SpanAttributes.HTTP_HOST: instance.host,
SpanAttributes.HTTP_SCHEME: instance.scheme,
SpanAttributes.HTTP_STATUS_CODE: response.status,
SpanAttributes.NET_PEER_NAME: instance.host,
SpanAttributes.NET_PEER_PORT: instance.port,
}
version = getattr(response, "version")
if version:
metric_attributes[SpanAttributes.HTTP_FLAVOR] = (
"1.1" if version == 11 else "1.0"
)
return metric_attributes
@contextlib.contextmanager
def _suppress_further_instrumentation():
token = context.attach(
context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True)
)
try:
yield
finally:
context.detach(token)
def _uninstrument():
unwrap(urllib3.connectionpool.HTTPConnectionPool, "urlopen")

Просмотреть файл

@ -0,0 +1,18 @@
# Copyright The 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.
_instruments = ("urllib3 >= 1.0.0, < 2.0.0",)
_supports_metrics = True

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -0,0 +1,154 @@
# Copyright The 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 urllib.parse
from re import escape, sub
from typing import Dict, Sequence
from wrapt import ObjectProxy
from opentelemetry import context, trace
# pylint: disable=unused-import
# pylint: disable=E0611
from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401
from opentelemetry.propagate import extract
from opentelemetry.trace import StatusCode
from opentelemetry.trace.propagation.tracecontext import (
TraceContextTextMapPropagator,
)
propagator = TraceContextTextMapPropagator()
def extract_attributes_from_object(
obj: any, attributes: Sequence[str], existing: Dict[str, str] = None
) -> Dict[str, str]:
extracted = {}
if existing:
extracted.update(existing)
for attr in attributes:
value = getattr(obj, attr, None)
if value is not None:
extracted[attr] = str(value)
return extracted
def http_status_to_status_code(
status: int,
allow_redirect: bool = True,
server_span: bool = False,
) -> StatusCode:
"""Converts an HTTP status code to an OpenTelemetry canonical status code
Args:
status (int): HTTP status code
"""
# See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status
if not isinstance(status, int):
return StatusCode.UNSET
if status < 100:
return StatusCode.ERROR
if status <= 299:
return StatusCode.UNSET
if status <= 399 and allow_redirect:
return StatusCode.UNSET
if status <= 499 and server_span:
return StatusCode.UNSET
return StatusCode.ERROR
def unwrap(obj, attr: str):
"""Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it
Args:
obj: Object that holds a reference to the wrapped function
attr (str): Name of the wrapped function
"""
func = getattr(obj, attr, None)
if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"):
setattr(obj, attr, func.__wrapped__)
def _start_internal_or_server_span(
tracer,
span_name,
start_time,
context_carrier,
context_getter,
attributes=None,
):
"""Returns internal or server span along with the token which can be used by caller to reset context
Args:
tracer : tracer in use by given instrumentation library
name (string): name of the span
start_time : start time of the span
context_carrier : object which contains values that are
used to construct a Context. This object
must be paired with an appropriate getter
which understands how to extract a value from it.
context_getter : an object which contains a get function that can retrieve zero
or more values from the carrier and a keys function that can get all the keys
from carrier.
"""
token = ctx = span_kind = None
if trace.get_current_span() is trace.INVALID_SPAN:
ctx = extract(context_carrier, getter=context_getter)
token = context.attach(ctx)
span_kind = trace.SpanKind.SERVER
else:
ctx = context.get_current()
span_kind = trace.SpanKind.INTERNAL
span = tracer.start_span(
name=span_name,
context=ctx,
kind=span_kind,
start_time=start_time,
attributes=attributes,
)
return span, token
def _url_quote(s) -> str: # pylint: disable=invalid-name
if not isinstance(s, (str, bytes)):
return s
quoted = urllib.parse.quote(s)
# Since SQL uses '%' as a keyword, '%' is a by-product of url quoting
# e.g. foo,bar --> foo%2Cbar
# thus in our quoting, we need to escape it too to finally give
# foo,bar --> foo%%2Cbar
return quoted.replace("%", "%%")
def _get_opentelemetry_values() -> dict:
"""
Return the OpenTelemetry Trace and Span IDs if Span ID is set in the
OpenTelemetry execution context.
"""
# Insert the W3C TraceContext generated
_headers = {}
propagator.inject(_headers)
return _headers
def _python_path_without_directory(python_path, directory, path_separator):
return sub(
rf"{escape(directory)}{path_separator}(?!$)",
"",
python_path,
)

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -0,0 +1,406 @@
# Copyright The 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 functools
import typing
import wsgiref.util as wsgiref_util
from timeit import default_timer
from opentelemetry import context, trace
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
_start_internal_or_server_span,
http_status_to_status_code,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.wsgi.version import (
__version__,
)
from opentelemetry.metrics import get_meter
from opentelemetry.propagators.textmap import Getter
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace.status import Status, StatusCode
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
SanitizeValue,
get_custom_headers,
normalise_request_header_name,
normalise_response_header_name,
remove_url_credentials,
)
_HTTP_VERSION_PREFIX = "HTTP/"
_CARRIER_KEY_PREFIX = "HTTP_"
_CARRIER_KEY_PREFIX_LEN = len(_CARRIER_KEY_PREFIX)
# List of recommended attributes
_duration_attrs = [
SpanAttributes.HTTP_METHOD,
SpanAttributes.HTTP_HOST,
SpanAttributes.HTTP_SCHEME,
SpanAttributes.HTTP_STATUS_CODE,
SpanAttributes.HTTP_FLAVOR,
SpanAttributes.HTTP_SERVER_NAME,
SpanAttributes.NET_HOST_NAME,
SpanAttributes.NET_HOST_PORT,
]
_active_requests_count_attrs = [
SpanAttributes.HTTP_METHOD,
SpanAttributes.HTTP_HOST,
SpanAttributes.HTTP_SCHEME,
SpanAttributes.HTTP_FLAVOR,
SpanAttributes.HTTP_SERVER_NAME,
]
class WSGIGetter(Getter[dict]):
def get(
self, carrier: dict, key: str
) -> typing.Optional[typing.List[str]]:
"""Getter implementation to retrieve a HTTP header value from the
PEP3333-conforming WSGI environ
Args:
carrier: WSGI environ object
key: header name in environ object
Returns:
A list with a single string with the header value if it exists,
else None.
"""
environ_key = "HTTP_" + key.upper().replace("-", "_")
value = carrier.get(environ_key)
if value is not None:
return [value]
return None
def keys(self, carrier):
return [
key[_CARRIER_KEY_PREFIX_LEN:].lower().replace("_", "-")
for key in carrier
if key.startswith(_CARRIER_KEY_PREFIX)
]
wsgi_getter = WSGIGetter()
def setifnotnone(dic, key, value):
if value is not None:
dic[key] = value
def collect_request_attributes(environ):
"""Collects HTTP request attributes from the PEP3333-conforming
WSGI environ and returns a dictionary to be used as span creation attributes.
"""
result = {
SpanAttributes.HTTP_METHOD: environ.get("REQUEST_METHOD"),
SpanAttributes.HTTP_SERVER_NAME: environ.get("SERVER_NAME"),
SpanAttributes.HTTP_SCHEME: environ.get("wsgi.url_scheme"),
}
host_port = environ.get("SERVER_PORT")
if host_port is not None and not host_port == "":
result.update({SpanAttributes.NET_HOST_PORT: int(host_port)})
setifnotnone(result, SpanAttributes.HTTP_HOST, environ.get("HTTP_HOST"))
target = environ.get("RAW_URI")
if target is None: # Note: `"" or None is None`
target = environ.get("REQUEST_URI")
if target is not None:
result[SpanAttributes.HTTP_TARGET] = target
else:
result[SpanAttributes.HTTP_URL] = remove_url_credentials(
wsgiref_util.request_uri(environ)
)
remote_addr = environ.get("REMOTE_ADDR")
if remote_addr:
result[SpanAttributes.NET_PEER_IP] = remote_addr
remote_host = environ.get("REMOTE_HOST")
if remote_host and remote_host != remote_addr:
result[SpanAttributes.NET_PEER_NAME] = remote_host
user_agent = environ.get("HTTP_USER_AGENT")
if user_agent is not None and len(user_agent) > 0:
result[SpanAttributes.HTTP_USER_AGENT] = user_agent
setifnotnone(
result, SpanAttributes.NET_PEER_PORT, environ.get("REMOTE_PORT")
)
flavor = environ.get("SERVER_PROTOCOL", "")
if flavor.upper().startswith(_HTTP_VERSION_PREFIX):
flavor = flavor[len(_HTTP_VERSION_PREFIX) :]
if flavor:
result[SpanAttributes.HTTP_FLAVOR] = flavor
return result
def collect_custom_request_headers_attributes(environ):
"""Returns custom HTTP request headers which are configured by the user
from the PEP3333-conforming WSGI environ to be used as span creation attributes as described
in the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
"""
sanitize = SanitizeValue(
get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
)
)
headers = {
key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-"): val
for key, val in environ.items()
if key.startswith(_CARRIER_KEY_PREFIX)
}
return sanitize.sanitize_header_values(
headers,
get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
),
normalise_request_header_name,
)
def collect_custom_response_headers_attributes(response_headers):
"""Returns custom HTTP response headers which are configured by the user from the
PEP3333-conforming WSGI environ as described in the specification
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
"""
sanitize = SanitizeValue(
get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
)
)
response_headers_dict = {}
if response_headers:
response_headers_dict = dict(response_headers)
return sanitize.sanitize_header_values(
response_headers_dict,
get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
),
normalise_response_header_name,
)
def _parse_status_code(resp_status):
status_code, _ = resp_status.split(" ", 1)
try:
return int(status_code)
except ValueError:
return None
def _parse_active_request_count_attrs(req_attrs):
active_requests_count_attrs = {}
for attr_key in _active_requests_count_attrs:
if req_attrs.get(attr_key) is not None:
active_requests_count_attrs[attr_key] = req_attrs[attr_key]
return active_requests_count_attrs
def _parse_duration_attrs(req_attrs):
duration_attrs = {}
for attr_key in _duration_attrs:
if req_attrs.get(attr_key) is not None:
duration_attrs[attr_key] = req_attrs[attr_key]
return duration_attrs
def add_response_attributes(
span, start_response_status, response_headers
): # pylint: disable=unused-argument
"""Adds HTTP response attributes to span using the arguments
passed to a PEP3333-conforming start_response callable.
"""
if not span.is_recording():
return
status_code, _ = start_response_status.split(" ", 1)
try:
status_code = int(status_code)
except ValueError:
span.set_status(
Status(
StatusCode.ERROR,
"Non-integer HTTP status: " + repr(status_code),
)
)
else:
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
span.set_status(
Status(http_status_to_status_code(status_code, server_span=True))
)
def get_default_span_name(environ):
"""Default implementation for name_callback, returns HTTP {METHOD_NAME}."""
return f"HTTP {environ.get('REQUEST_METHOD', '')}".strip()
class OpenTelemetryMiddleware:
"""The WSGI application middleware.
This class is a PEP 3333 conforming WSGI middleware that starts and
annotates spans for any requests it is invoked with.
Args:
wsgi: The WSGI application callable to forward requests to.
request_hook: Optional callback which is called with the server span and WSGI
environ object for every incoming request.
response_hook: Optional callback which is called with the server span,
WSGI environ, status_code and response_headers for every
incoming request.
tracer_provider: Optional tracer provider to use. If omitted the current
globally configured one is used.
"""
def __init__(
self,
wsgi,
request_hook=None,
response_hook=None,
tracer_provider=None,
meter_provider=None,
):
self.wsgi = wsgi
self.tracer = trace.get_tracer(__name__, __version__, tracer_provider)
self.meter = get_meter(__name__, __version__, meter_provider)
self.duration_histogram = self.meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="measures the duration of the inbound HTTP request",
)
self.active_requests_counter = self.meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
description="measures the number of concurrent HTTP requests that are currently in-flight",
)
self.request_hook = request_hook
self.response_hook = response_hook
@staticmethod
def _create_start_response(
span, start_response, response_hook, duration_attrs
):
@functools.wraps(start_response)
def _start_response(status, response_headers, *args, **kwargs):
add_response_attributes(span, status, response_headers)
status_code = _parse_status_code(status)
if status_code is not None:
duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = status_code
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
custom_attributes = collect_custom_response_headers_attributes(
response_headers
)
if len(custom_attributes) > 0:
span.set_attributes(custom_attributes)
if response_hook:
response_hook(status, response_headers)
return start_response(status, response_headers, *args, **kwargs)
return _start_response
# pylint: disable=too-many-branches
def __call__(self, environ, start_response):
"""The WSGI application
Args:
environ: A WSGI environment.
start_response: The WSGI start_response callable.
"""
req_attrs = collect_request_attributes(environ)
active_requests_count_attrs = _parse_active_request_count_attrs(
req_attrs
)
duration_attrs = _parse_duration_attrs(req_attrs)
span, token = _start_internal_or_server_span(
tracer=self.tracer,
span_name=get_default_span_name(environ),
start_time=None,
context_carrier=environ,
context_getter=wsgi_getter,
attributes=req_attrs,
)
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
custom_attributes = collect_custom_request_headers_attributes(
environ
)
if len(custom_attributes) > 0:
span.set_attributes(custom_attributes)
if self.request_hook:
self.request_hook(span, environ)
response_hook = self.response_hook
if response_hook:
response_hook = functools.partial(response_hook, span, environ)
start = default_timer()
self.active_requests_counter.add(1, active_requests_count_attrs)
try:
with trace.use_span(span):
start_response = self._create_start_response(
span, start_response, response_hook, duration_attrs
)
iterable = self.wsgi(environ, start_response)
return _end_span_after_iterating(iterable, span, token)
except Exception as ex:
if span.is_recording():
span.set_status(Status(StatusCode.ERROR, str(ex)))
span.end()
if token is not None:
context.detach(token)
raise
finally:
duration = max(round((default_timer() - start) * 1000), 0)
self.duration_histogram.record(duration, duration_attrs)
self.active_requests_counter.add(-1, active_requests_count_attrs)
# Put this in a subfunction to not delay the call to the wrapped
# WSGI application (instrumentation should change the application
# behavior as little as possible).
def _end_span_after_iterating(iterable, span, token):
try:
with trace.use_span(span):
yield from iterable
finally:
close = getattr(iterable, "close", None)
if close:
close()
span.end()
if token is not None:
context.detach(token)
# TODO: inherit from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.propagators.Setter
class ResponsePropagationSetter:
def set(self, carrier, key, value): # pylint: disable=no-self-use
carrier.append((key, value))
default_response_propagation_setter = ResponsePropagationSetter()

Просмотреть файл

@ -0,0 +1,18 @@
# Copyright The 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.
_instruments = tuple()
_supports_metrics = True

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -0,0 +1,5 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License in the project root for
# license information.
# --------------------------------------------------------------------------

Просмотреть файл

@ -0,0 +1,213 @@
# Copyright The 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.
from os import environ
from re import IGNORECASE as RE_IGNORECASE
from re import compile as re_compile
from re import search
from typing import Iterable, List
from urllib.parse import urlparse, urlunparse
from opentelemetry.semconv.trace import SpanAttributes
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS = (
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS"
)
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST = (
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST"
)
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE = (
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE"
)
# List of recommended metrics attributes
_duration_attrs = {
SpanAttributes.HTTP_METHOD,
SpanAttributes.HTTP_HOST,
SpanAttributes.HTTP_SCHEME,
SpanAttributes.HTTP_STATUS_CODE,
SpanAttributes.HTTP_FLAVOR,
SpanAttributes.HTTP_SERVER_NAME,
SpanAttributes.NET_HOST_NAME,
SpanAttributes.NET_HOST_PORT,
}
_active_requests_count_attrs = {
SpanAttributes.HTTP_METHOD,
SpanAttributes.HTTP_HOST,
SpanAttributes.HTTP_SCHEME,
SpanAttributes.HTTP_FLAVOR,
SpanAttributes.HTTP_SERVER_NAME,
}
class ExcludeList:
"""Class to exclude certain paths (given as a list of regexes) from tracing requests"""
def __init__(self, excluded_urls: Iterable[str]):
self._excluded_urls = excluded_urls
if self._excluded_urls:
self._regex = re_compile("|".join(excluded_urls))
def url_disabled(self, url: str) -> bool:
return bool(self._excluded_urls and search(self._regex, url))
class SanitizeValue:
"""Class to sanitize (remove sensitive data from) certain headers (given as a list of regexes)"""
def __init__(self, sanitized_fields: Iterable[str]):
self._sanitized_fields = sanitized_fields
if self._sanitized_fields:
self._regex = re_compile("|".join(sanitized_fields), RE_IGNORECASE)
def sanitize_header_value(self, header: str, value: str) -> str:
return (
"[REDACTED]"
if (self._sanitized_fields and search(self._regex, header))
else value
)
def sanitize_header_values(
self, headers: dict, header_regexes: list, normalize_function: callable
) -> dict:
values = {}
if header_regexes:
header_regexes_compiled = re_compile(
"|".join("^" + i + "$" for i in header_regexes),
RE_IGNORECASE,
)
for header_name in list(
filter(
header_regexes_compiled.match,
headers.keys(),
)
):
header_values = headers.get(header_name)
if header_values:
key = normalize_function(header_name.lower())
values[key] = [
self.sanitize_header_value(
header=header_name, value=header_values
)
]
return values
_root = r"OTEL_PYTHON_{}"
def get_traced_request_attrs(instrumentation):
traced_request_attrs = environ.get(
_root.format(f"{instrumentation}_TRACED_REQUEST_ATTRS"), []
)
if traced_request_attrs:
traced_request_attrs = [
traced_request_attr.strip()
for traced_request_attr in traced_request_attrs.split(",")
]
return traced_request_attrs
def get_excluded_urls(instrumentation: str) -> ExcludeList:
# Get instrumentation-specific excluded URLs. If not set, retrieve them
# from generic variable.
excluded_urls = environ.get(
_root.format(f"{instrumentation}_EXCLUDED_URLS"),
environ.get(_root.format("EXCLUDED_URLS"), ""),
)
return parse_excluded_urls(excluded_urls)
def parse_excluded_urls(excluded_urls: str) -> ExcludeList:
"""
Small helper to put an arbitrary url list inside an ExcludeList
"""
if excluded_urls:
excluded_url_list = [
excluded_url.strip() for excluded_url in excluded_urls.split(",")
]
else:
excluded_url_list = []
return ExcludeList(excluded_url_list)
def remove_url_credentials(url: str) -> str:
"""Given a string url, remove the username and password only if it is a valid url"""
try:
parsed = urlparse(url)
if all([parsed.scheme, parsed.netloc]): # checks for valid url
parsed_url = urlparse(url)
netloc = (
(":".join(((parsed_url.hostname or ""), str(parsed_url.port))))
if parsed_url.port
else (parsed_url.hostname or "")
)
return urlunparse(
(
parsed_url.scheme,
netloc,
parsed_url.path,
parsed_url.params,
parsed_url.query,
parsed_url.fragment,
)
)
except ValueError: # an unparsable url was passed
pass
return url
def normalise_request_header_name(header: str) -> str:
key = header.lower().replace("-", "_")
return f"http.request.header.{key}"
def normalise_response_header_name(header: str) -> str:
key = header.lower().replace("-", "_")
return f"http.response.header.{key}"
def get_custom_headers(env_var: str) -> List[str]:
custom_headers = environ.get(env_var, [])
if custom_headers:
custom_headers = [
custom_headers.strip()
for custom_headers in custom_headers.split(",")
]
return custom_headers
def _parse_active_request_count_attrs(req_attrs):
active_requests_count_attrs = {
key: req_attrs[key]
for key in _active_requests_count_attrs.intersection(req_attrs.keys())
}
return active_requests_count_attrs
def _parse_duration_attrs(req_attrs):
duration_attrs = {
key: req_attrs[key]
for key in _duration_attrs.intersection(req_attrs.keys())
}
return duration_attrs

Просмотреть файл

@ -0,0 +1,183 @@
# Copyright The 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.
"""
This library provides functionality to enrich HTTP client spans with IPs. It does
not create spans on its own.
"""
import contextlib
import http.client
import logging
import socket # pylint:disable=unused-import # Used for typing
import typing
from typing import Collection
import wrapt
from opentelemetry import context
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.instrumentor import (
BaseInstrumentor,
)
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.utils import (
unwrap,
)
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace.span import Span
_STATE_KEY = "httpbase_instrumentation_state"
logger = logging.getLogger(__name__)
class HttpClientInstrumentor(BaseInstrumentor):
def instrumentation_dependencies(self) -> Collection[str]:
return () # This instruments http.client from stdlib; no extra deps.
def _instrument(self, **kwargs):
"""Instruments the http.client module (not creating spans on its own)"""
_instrument()
def _uninstrument(self, **kwargs):
_uninstrument()
def _remove_nonrecording(spanlist: typing.List[Span]):
idx = len(spanlist) - 1
while idx >= 0:
if not spanlist[idx].is_recording():
logger.debug("Span is not recording: %s", spanlist[idx])
islast = idx + 1 == len(spanlist)
if not islast:
spanlist[idx] = spanlist[len(spanlist) - 1]
spanlist.pop()
if islast:
if idx == 0:
return False # We removed everything
idx -= 1
else:
idx -= 1
return True
def trysetip(conn: http.client.HTTPConnection, loglevel=logging.DEBUG) -> bool:
"""Tries to set the net.peer.ip semantic attribute on the current span from the given
HttpConnection.
Returns False if the connection is not yet established, False if the IP was captured
or there is no need to capture it.
"""
state = _getstate()
if not state:
return True
spanlist = state.get("need_ip") # type: typing.List[Span]
if not spanlist:
return True
# Remove all non-recording spans from the list.
if not _remove_nonrecording(spanlist):
return True
sock = "<property not accessed>"
try:
sock = conn.sock # type: typing.Optional[socket.socket]
logger.debug("Got socket: %s", sock)
if sock is None:
return False
addr = sock.getpeername()
if addr and addr[0]:
ip = addr[0]
except Exception: # pylint:disable=broad-except
logger.log(
loglevel,
"Failed to get peer address from %s",
sock,
exc_info=True,
stack_info=True,
)
else:
for span in spanlist:
span.set_attribute(SpanAttributes.NET_PEER_IP, ip)
return True
def _instrumented_connect(
wrapped, instance: http.client.HTTPConnection, args, kwargs
):
result = wrapped(*args, **kwargs)
trysetip(instance, loglevel=logging.WARNING)
return result
def instrument_connect(module, name="connect"):
"""Instrument additional connect() methods, e.g. for derived classes."""
wrapt.wrap_function_wrapper(
module,
name,
_instrumented_connect,
)
def _instrument():
def instrumented_send(
wrapped, instance: http.client.HTTPConnection, args, kwargs
):
done = trysetip(instance)
result = wrapped(*args, **kwargs)
if not done:
trysetip(instance, loglevel=logging.WARNING)
return result
wrapt.wrap_function_wrapper(
http.client.HTTPConnection,
"send",
instrumented_send,
)
instrument_connect(http.client.HTTPConnection)
# No need to instrument HTTPSConnection, as it calls super().connect()
def _getstate() -> typing.Optional[dict]:
return context.get_value(_STATE_KEY)
@contextlib.contextmanager
def set_ip_on_next_http_connection(span: Span):
state = _getstate()
if not state:
token = context.attach(
context.set_value(_STATE_KEY, {"need_ip": [span]})
)
try:
yield
finally:
context.detach(token)
else:
spans = state["need_ip"] # type: typing.List[Span]
spans.append(span)
try:
yield
finally:
try:
spans.remove(span)
except ValueError: # Span might have become non-recording
pass
def _uninstrument():
unwrap(http.client.HTTPConnection, "send")
unwrap(http.client.HTTPConnection, "connect")

Просмотреть файл

@ -0,0 +1,15 @@
# Copyright The 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.38b0"

Просмотреть файл

@ -6,6 +6,9 @@
import logging
from os import environ
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.distro import (
BaseDistro,
)
from azure.monitor.opentelemetry.diagnostics._diagnostic_logging import (
AzureDiagnosticLogging,
)
@ -17,7 +20,6 @@ from opentelemetry.environment_variables import (
OTEL_METRICS_EXPORTER,
OTEL_TRACES_EXPORTER,
)
from opentelemetry.instrumentation.distro import BaseDistro
from opentelemetry.sdk.environment_variables import (
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED,
)

Просмотреть файл

@ -85,16 +85,9 @@ setup(
python_requires=">=3.7",
install_requires=[
"azure-monitor-opentelemetry-exporter>=1.0.0b13",
"opentelemetry-instrumentation~=0.38b0",
"opentelemetry-instrumentation-django~=0.38b0",
"opentelemetry-instrumentation-fastapi~=0.38b0",
"opentelemetry-instrumentation-flask~=0.38b0",
"opentelemetry-instrumentation-psycopg2~=0.38b0",
"opentelemetry-instrumentation-requests~=0.38b0",
"opentelemetry-instrumentation-urllib~=0.38b0",
"opentelemetry-instrumentation-urllib3~=0.38b0",
"opentelemetry-api==1.17.0",
"opentelemetry-sdk==1.17.0",
"wrapt >= 1.0.0, < 2.0.0",
],
entry_points={
"opentelemetry_distro": [
@ -103,5 +96,14 @@ setup(
"opentelemetry_configurator": [
"azure_monitor_opentelemetry_configurator = azure.monitor.opentelemetry.autoinstrumentation._configurator:AzureMonitorConfigurator"
],
"azure_monitor_opentelemetry_instrumentor": [
"django = azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.django:DjangoInstrumentor",
"fastapi = azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.fastapi:FastAPIInstrumentor",
"flask = azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.flask:FlaskInstrumentor",
"psycopg2 = azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.psycopg2:Psycopg2Instrumentor",
"requests = azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.requests:RequestsInstrumentor",
"urllib = azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.urllib:URLLibInstrumentor",
"urllib3 = azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.urllib3:URLLib3Instrumentor",
],
},
)

Просмотреть файл

@ -16,7 +16,7 @@ import unittest
from unittest.mock import Mock, patch
from azure.monitor.opentelemetry._configure import (
_SUPPORTED_INSTRUMENTED_LIBRARIES,
_SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP,
_setup_instrumentations,
_setup_logging,
_setup_metrics,
@ -321,9 +321,7 @@ class TestConfigure(unittest.TestCase):
metric_exporter_mock.assert_called_once_with(**configurations)
reader_mock.assert_called_once_with(metric_exp_init_mock)
@patch(
"azure.monitor.opentelemetry._configure.get_dist_dependency_conflicts"
)
@patch("azure.monitor.opentelemetry._configure.get_dependency_conflicts")
@patch("azure.monitor.opentelemetry._configure.iter_entry_points")
def test_setup_instrumentations_lib_not_supported(
self,
@ -337,19 +335,21 @@ class TestConfigure(unittest.TestCase):
instr_class_mock = Mock()
instr_class_mock.return_value = instrumentor_mock
ep_mock.name = "test_instr"
ep2_mock.name = _SUPPORTED_INSTRUMENTED_LIBRARIES[1]
ep2_mock.name = list(
_SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP.keys()
)[0]
ep2_mock.load.return_value = instr_class_mock
dep_mock.return_value = None
_setup_instrumentations()
dep_mock.assert_called_with(ep2_mock.dist)
dep_mock.assert_called_with(
_SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP[ep2_mock.name]
)
ep_mock.load.assert_not_called()
ep2_mock.load.assert_called_once()
instrumentor_mock.instrument.assert_called_once()
@patch("azure.monitor.opentelemetry._configure._logger")
@patch(
"azure.monitor.opentelemetry._configure.get_dist_dependency_conflicts"
)
@patch("azure.monitor.opentelemetry._configure.get_dependency_conflicts")
@patch("azure.monitor.opentelemetry._configure.iter_entry_points")
def test_setup_instrumentations_conflict(
self,
@ -362,19 +362,21 @@ class TestConfigure(unittest.TestCase):
instrumentor_mock = Mock()
instr_class_mock = Mock()
instr_class_mock.return_value = instrumentor_mock
ep_mock.name = _SUPPORTED_INSTRUMENTED_LIBRARIES[0]
ep_mock.name = list(
_SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP.keys()
)[0]
ep_mock.load.return_value = instr_class_mock
dep_mock.return_value = True
_setup_instrumentations()
dep_mock.assert_called_with(ep_mock.dist)
dep_mock.assert_called_with(
_SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP[ep_mock.name]
)
ep_mock.load.assert_not_called()
instrumentor_mock.instrument.assert_not_called()
logger_mock.debug.assert_called_once()
@patch("azure.monitor.opentelemetry._configure._logger")
@patch(
"azure.monitor.opentelemetry._configure.get_dist_dependency_conflicts"
)
@patch("azure.monitor.opentelemetry._configure.get_dependency_conflicts")
@patch("azure.monitor.opentelemetry._configure.iter_entry_points")
def test_setup_instrumentations_exception(
self,
@ -387,11 +389,15 @@ class TestConfigure(unittest.TestCase):
instrumentor_mock = Mock()
instr_class_mock = Mock()
instr_class_mock.return_value = instrumentor_mock
ep_mock.name = _SUPPORTED_INSTRUMENTED_LIBRARIES[0]
ep_mock.name = list(
_SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP.keys()
)[0]
ep_mock.load.side_effect = Exception()
dep_mock.return_value = None
_setup_instrumentations()
dep_mock.assert_called_with(ep_mock.dist)
dep_mock.assert_called_with(
_SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP[ep_mock.name]
)
ep_mock.load.assert_called_once()
instrumentor_mock.instrument.assert_not_called()
logger_mock.warning.assert_called_once()

Просмотреть файл

@ -6,7 +6,9 @@
import unittest
from opentelemetry.instrumentation.django import DjangoInstrumentor
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.django import (
DjangoInstrumentor,
)
class TestDjangoInstrumentation(unittest.TestCase):

Просмотреть файл

@ -6,7 +6,9 @@
import unittest
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.fastapi import (
FastAPIInstrumentor,
)
class TestFastApiInstrumentation(unittest.TestCase):

Просмотреть файл

@ -6,7 +6,9 @@
import unittest
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.flask import (
FlaskInstrumentor,
)
class TestFlaskInstrumentation(unittest.TestCase):

Просмотреть файл

@ -6,7 +6,9 @@
import unittest
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.psycopg2 import (
Psycopg2Instrumentor,
)
class TestPsycopg2Instrumentation(unittest.TestCase):

Просмотреть файл

@ -6,7 +6,9 @@
import unittest
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.requests import (
RequestsInstrumentor,
)
class TestRequestsInstrumentation(unittest.TestCase):

Просмотреть файл

@ -6,7 +6,9 @@
import unittest
from opentelemetry.instrumentation.urllib import URLLibInstrumentor
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.urllib import (
URLLibInstrumentor,
)
class TestUrllibInstrumentation(unittest.TestCase):

Просмотреть файл

@ -6,7 +6,9 @@
import unittest
from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor
from azure.monitor.opentelemetry._vendor.v0_38b0.opentelemetry.instrumentation.urllib3 import (
URLLib3Instrumentor,
)
class TestUrllib3Instrumentation(unittest.TestCase):

Просмотреть файл

@ -8,7 +8,8 @@ exclude = '''
.vscode|
venv|
.*/build/lib/.*|
scripts
scripts|
azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor|
)/
)
'''