Merge pull request #280 from jeremydvoss/vendor-instrumentations
Vendor instrumentations
This commit is contained in:
Коммит
992d12dce9
1
.flake8
1
.flake8
|
@ -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|
|
||||
)/
|
||||
)
|
||||
'''
|
||||
|
|
Загрузка…
Ссылка в новой задаче