Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

feat: add tracing support #171

Merged
merged 64 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
b1fea22
Add opentelemetry libraries
musale Jul 31, 2023
e081902
Remove unrecognized options from disabled prop
musale Jul 31, 2023
a0db9c4
Add custom exceptions for kiota http
musale Jul 31, 2023
4099278
Add o11y options
musale Jul 31, 2023
e567d80
Fix exceptions formatting
musale Jul 31, 2023
8431167
Fix o11y options formatting
musale Jul 31, 2023
1ce332d
Add decoding uri string method and fix formatting
musale Jul 31, 2023
2739751
Add telemetry for send_async method
musale Jul 31, 2023
1e7d670
Add send_collection_async tracing
musale Jul 31, 2023
f0252b2
Fix formatting errors
musale Jul 31, 2023
766bc16
Add tracing on send_collection_of_primitive_async
musale Jul 31, 2023
ca5cd5c
Add tracing on send_primitive_async
musale Jul 31, 2023
c0314d7
Add tracing on send_no_response_content_async
musale Jul 31, 2023
9385f8a
Add tracing on convert_to_native_async
musale Jul 31, 2023
5d89a57
Fix closing of parent span in send_collection_async
musale Jul 31, 2023
4f74a13
Fix failing tests with a mock span
musale Jul 31, 2023
41da962
Fix code smells and rename exceptions
musale Jul 31, 2023
c28bbef
Add tracing to parameters_name_decoding_handler
musale Aug 2, 2023
39e0c44
Add tracing to redirect_handler
musale Aug 2, 2023
5001484
Fix redirect_handler keys and attributes used
musale Aug 2, 2023
d6dbc27
Fix formatting issues
musale Aug 2, 2023
8c397a9
Add tracing to retry_handler
musale Aug 2, 2023
9e85b72
Add tracing to url_replace_handler
musale Aug 2, 2023
547993f
Add tracing to user_agent_handler
musale Aug 2, 2023
1d123a3
Fix method names used in span creation
musale Aug 2, 2023
472f9eb
Refactor retry handler to reduce complexity
musale Aug 2, 2023
3c11db3
Refactor middleware to have a common interface
musale Aug 2, 2023
cdc4461
Track the parent_span in the base middleware
musale Aug 2, 2023
1b076fc
Fix sorting of imports
musale Aug 2, 2023
d0f42de
Add a test to check if the methods that setup o11y are called
musale Aug 2, 2023
f8490d7
Fix indentation of code to fix unreachable code bug
musale Aug 2, 2023
6ca8168
Bump platformdirs from 3.9.1 to 3.10.0
dependabot[bot] Jul 31, 2023
c9fa008
Bump microsoft-kiota-abstractions from 0.6.0 to 0.7.0
dependabot[bot] Aug 4, 2023
afceee8
Initialize backingstorefactorysingleton with the provided factory
samwelkanda Jul 27, 2023
93ec1df
Remove pipenv dependency
samwelkanda Jul 27, 2023
7631718
Bump package version and update CHANGELOG
samwelkanda Jul 27, 2023
9332539
Bump mypy from 1.4.1 to 1.5.0
dependabot[bot] Aug 10, 2023
c1f1440
Bump microsoft-kiota-abstractions from 0.7.0 to 0.7.1
dependabot[bot] Aug 10, 2023
3b12b75
Bump exceptiongroup from 1.1.2 to 1.1.3
dependabot[bot] Aug 14, 2023
78bc4a4
Bump coverage[toml] from 7.2.7 to 7.3.0
dependabot[bot] Aug 14, 2023
907a0c5
Add telemetry for send_async method
musale Jul 31, 2023
e493c15
Fix code smells and rename exceptions
musale Jul 31, 2023
363000b
Enable tracing if options is enabled
musale Aug 16, 2023
aeb77c7
Enable tracing if options is enabled
musale Aug 16, 2023
0009644
Fix formatting errors
musale Aug 16, 2023
e25ad84
Merge branch 'main' into o11y
musale Aug 16, 2023
582a8ee
Change the casing for BackingStoreError
musale Sep 11, 2023
ca12997
Merge branch 'o11y' of github.com:musale/kiota-http-python into o11y
musale Sep 11, 2023
b6be592
Update httpx and httpcore
musale Sep 11, 2023
5e6d90c
Remove TODO and fix code smell
musale Sep 11, 2023
6cd0860
Merge branch 'main' into o11y
samwelkanda Sep 12, 2023
b1fa8f2
Fix merge conflict
samwelkanda Sep 12, 2023
c38cf76
Fix typo
samwelkanda Sep 12, 2023
f945d2d
Add tracing for CAE retries
samwelkanda Sep 12, 2023
ee257d3
Code formatting
samwelkanda Sep 12, 2023
ea67a91
Fix code format
samwelkanda Sep 12, 2023
c72f530
Fix indentation
samwelkanda Sep 12, 2023
d1e1f5b
Add tracer to cae test
samwelkanda Sep 12, 2023
2f6e49e
Bugfix additional authentication context
samwelkanda Sep 12, 2023
ed71c3f
Fix failing CAE test
samwelkanda Sep 12, 2023
105c628
Fix bug in changes to redirect handler
samwelkanda Sep 13, 2023
ea8e0bc
Bump abtractions version to fix failing tests
samwelkanda Sep 13, 2023
9b62292
Update abstractions version to 0.8.3
samwelkanda Sep 13, 2023
2fbb43f
Merge branch 'main' into o11y
samwelkanda Sep 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,14 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=long-suffix,
old-ne-operator,
old-octal-literal,
non-ascii-bytes-literal,
raw-checker-failed,
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
eq-without-hash,
too-few-public-methods,
missing-module-docstring,
missing-class-docstring,
Expand Down
21 changes: 21 additions & 0 deletions kiota_http/_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Exceptions raised in Kiota HTTP."""


class KiotaHTTPXError(Exception):
"""Base class for Kiota HTTP exceptions."""


class BackingStoreError(KiotaHTTPXError):
"""Raised for the backing store."""


class DeserializationError(KiotaHTTPXError):
"""Raised for deserialization."""


class RequestError(KiotaHTTPXError):
"""Raised for request building errors."""


class RedirectError(KiotaHTTPXError):
"""Raised when a redirect has errors."""
515 changes: 369 additions & 146 deletions kiota_http/httpx_request_adapter.py

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions kiota_http/middleware/middleware.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import ssl
from typing import Optional

import httpx
from opentelemetry import trace
from urllib3 import PoolManager

from .._version import VERSION
from ..observability_options import ObservabilityOptions

tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION)


class MiddlewarePipeline():
"""MiddlewarePipeline, entry point of middleware
Expand Down Expand Up @@ -45,6 +52,7 @@ class BaseMiddleware():

def __init__(self):
self.next = None
self.parent_span = None

async def send(self, request, transport):
if self.next is None:
Expand All @@ -55,3 +63,16 @@ async def send(self, request, transport):
response.request = request
return response
return await self.next.send(request, transport)

def _create_observability_span(self, request, span_name: str) -> trace.Span:
"""Gets the parent_span from the request options and creates a new span.
If no parent_span is found, we try to get the current span."""
_span = None
if options := getattr(request, "options", None):
if parent_span := options.get("parent_span", None):
self.parent_span = parent_span
_context = trace.set_span_in_context(parent_span)
_span = tracer.start_span(span_name, _context)
if _span is None:
_span = trace.get_current_span()
return _span
23 changes: 18 additions & 5 deletions kiota_http/middleware/parameters_name_decoding_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from .middleware import BaseMiddleware
from .options import ParametersNameDecodingHandlerOption

PARAMETERS_NAME_DECODING_KEY = "com.microsoft.kiota.handler.parameters_name_decoding.enable"


class ParametersNameDecodingHandler(BaseMiddleware):

Expand All @@ -25,7 +27,7 @@ def __init__(

async def send(
self, request: httpx.Request, transport: httpx.AsyncBaseTransport
) -> httpx.Response: #type: ignore
) -> httpx.Response: # type: ignore
"""To execute the current middleware

Args:
Expand All @@ -36,8 +38,12 @@ async def send(
Response: The response object.
"""
current_options = self._get_current_options(request)
span = self._create_observability_span(request, "ParametersNameDecodingHandler_send")
if current_options.enabled:
span.set_attribute(PARAMETERS_NAME_DECODING_KEY, current_options.enabled)
span.end()

updated_url: str = str(request.url) #type: ignore
updated_url: str = str(request.url) # type: ignore
if all(
[
current_options, current_options.enabled, '%' in updated_url,
Expand All @@ -59,7 +65,14 @@ def _get_current_options(self, request: httpx.Request) -> ParametersNameDecoding
Returns:
ParametersNameDecodingHandlerOption: The options to used.
"""
current_options =request.options.get( # type:ignore
ParametersNameDecodingHandlerOption.get_key(), self.options
)
if options := getattr(request, 'options', None):
current_options = options.get( # type:ignore
ParametersNameDecodingHandlerOption.get_key(), self.options
)
return current_options

def decode_uri_encoded_string(self, original: str) -> str:
"""Decodes a uri encoded string ."""
if original and '%' in original:
return unquote(original)
return original
22 changes: 18 additions & 4 deletions kiota_http/middleware/redirect_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

import httpx
from kiota_abstractions.request_option import RequestOption
from opentelemetry.semconv.trace import SpanAttributes

from .._exceptions import RedirectError
from .middleware import BaseMiddleware
from .options import RedirectHandlerOption

REDIRECT_ENABLE_KEY = "com.microsoft.kiota.handler.redirect.enable"
REDIRECT_COUNT_KEY = "com.microsoft.kiota.handler.redirect.count"


class RedirectHandler(BaseMiddleware):
"""Middlware that allows us to define the redirect policy for all requests
Expand Down Expand Up @@ -59,23 +64,32 @@ async def send(
"""Sends the http request object to the next middleware or redirects
the request if necessary.
"""
_enable_span = self._create_observability_span(request, "RedirectHandler_send")
current_options = self._get_current_options(request)
_enable_span.set_attribute(REDIRECT_ENABLE_KEY, True)
_enable_span.end()

retryable = True
_redirect_span = self._create_observability_span(
request, f"RedirectHandler_send - redirect {len(self.history)}"
)
while retryable:
response = await super().send(request, transport)
_redirect_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code)
redirect_location = self.get_redirect_location(response)
if redirect_location and current_options.should_redirect:
current_options.max_redirect -= 1
retryable = self.increment(response, current_options.max_redirect)
_redirect_span.set_attribute(REDIRECT_COUNT_KEY, len(self.history))
new_request = self._build_redirect_request(request, response)
request = new_request
continue

response.history = self.history
return response

raise Exception(f"Too many redirects. {response.history}")
exc = RedirectError(f"Too many redirects. {response.history}")
_redirect_span.record_exception(exc)
_redirect_span.end()
raise exc
return response

def _get_current_options(self, request: httpx.Request) -> RedirectHandlerOption:
"""Returns the options to use for the request.Overries default options if
Expand Down
46 changes: 27 additions & 19 deletions kiota_http/middleware/retry_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import httpx
from kiota_abstractions.request_option import RequestOption
from opentelemetry.semconv.trace import SpanAttributes

from .middleware import BaseMiddleware
from .options import RetryHandlerOption
Expand Down Expand Up @@ -66,36 +67,43 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport
"""
Sends the http request object to the next middleware or retries the request if necessary.
"""
current_options = self._get_current_options(request)
response = None
retry_count = 0
retry_valid = current_options.should_retry

_span = self._create_observability_span(request, "RetryHandler_send")
current_options = self._get_current_options(request)
_span.set_attribute("com.microsoft.kiota.handler.retry.enable", True)
_span.end()
retry_valid = current_options.should_retry
_retry_span = self._create_observability_span(
request, f"RetryHandler_send - attempt {retry_count}"
)
while retry_valid:
start_time = time.time()
if retry_count > 0:
request.headers.update({'retry-attempt': f'{retry_count}'})
response = await super().send(request, transport)
# Check if the request needs to be retried based on the response method
# and status code
if self.should_retry(request, current_options, response):
# check that max retries has not been hit
retry_valid = self.check_retry_valid(retry_count, current_options)

# Get the delay time between retries
delay = self.get_delay_time(retry_count, response)
_retry_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code)
# check that max retries has not been hit
retry_valid = self.check_retry_valid(retry_count, current_options)

if retry_valid and delay < current_options.max_delay:
time.sleep(delay)
end_time = time.time()
current_options.max_delay -= (end_time - start_time)
# increment the count for retries
retry_count += 1
# Get the delay time between retries
delay = self.get_delay_time(retry_count, response)

continue
# Check if the request needs to be retried based on the response method
# and status code
should_retry = self.should_retry(request, current_options, response)
if all([should_retry, retry_valid, delay < current_options.max_delay]):
time.sleep(delay)
end_time = time.time()
current_options.max_delay -= (end_time - start_time)
# increment the count for retries
retry_count += 1
request.headers.update({'retry-attempt': f'{retry_count}'})
_retry_span.set_attribute(SpanAttributes.HTTP_RETRY_COUNT, retry_count)
continue
break
if response is None:
response = await super().send(request, transport)
_retry_span.end()
return response

def _get_current_options(self, request: httpx.Request) -> RetryHandlerOption:
Expand Down
19 changes: 13 additions & 6 deletions kiota_http/middleware/url_replace_handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import httpx
from kiota_abstractions.request_option import RequestOption
from opentelemetry.semconv.trace import SpanAttributes

from .middleware import BaseMiddleware
from .options import UrlReplaceHandlerOption
Expand All @@ -20,7 +21,7 @@ def __init__(self, options: RequestOption = UrlReplaceHandlerOption()):

async def send(
self, request: httpx.Request, transport: httpx.AsyncBaseTransport
) -> httpx.Response: #type: ignore
) -> httpx.Response: # type: ignore
"""To execute the current middleware

Args:
Expand All @@ -30,12 +31,18 @@ async def send(
Returns:
Response: The response object.
"""
current_options = self._get_current_options(request)
response = None
_enable_span = self._create_observability_span(request, "UrlReplaceHandler_send")
if self.options and self.options.is_enabled:
_enable_span.set_attribute("com.microsoft.kiota.handler.url_replacer.enable", True)
current_options = self._get_current_options(request)

url_string: str = str(request.url) #type: ignore
url_string = self.replace_url_segment(url_string, current_options)
request.url = httpx.URL(url_string)
url_string: str = str(request.url) # type: ignore
url_string = self.replace_url_segment(url_string, current_options)
request.url = httpx.URL(url_string)
_enable_span.set_attribute(SpanAttributes.HTTP_URL, str(request.url))
response = await super().send(request, transport)
_enable_span.end()
return response

def _get_current_options(self, request: httpx.Request) -> UrlReplaceHandlerOption:
Expand All @@ -48,7 +55,7 @@ def _get_current_options(self, request: httpx.Request) -> UrlReplaceHandlerOptio
Returns:
UrlReplaceHandlerOption: The options to be used.
"""
current_options =request.options.get( # type:ignore
current_options = request.options.get( # type:ignore
UrlReplaceHandlerOption.get_key(), self.options
)
return current_options
Expand Down
22 changes: 14 additions & 8 deletions kiota_http/middleware/user_agent_handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from httpx import AsyncBaseTransport, Request, Response
from kiota_abstractions.request_option import RequestOption
from opentelemetry.semconv.trace import SpanAttributes

from .middleware import BaseMiddleware
from .options import UserAgentHandlerOption
Expand All @@ -19,14 +20,19 @@ async def send(self, request: Request, transport: AsyncBaseTransport) -> Respons
Checks if the request has a User-Agent header and updates it if the
platform config allows.
"""
_span = self._create_observability_span(request, "UserAgentHandler_send")
if self.options and self.options.is_enabled:
_span.set_attribute("com.microsoft.kiota.handler.useragent.enable", True)
value = f"{self.options.product_name}/{self.options.product_version}"

user_agent = request.headers.get("User-Agent", "")
if not user_agent:
request.headers.update({"User-Agent": value})
else:
if value not in user_agent:
request.headers.update({"User-Agent": f"{user_agent} {value}"})

self._update_user_agent(request, value)
_span.end()
return await super().send(request, transport)

def _update_user_agent(self, request: Request, value: str):
"""Updates the values of the User-Agent header."""
user_agent = request.headers.get("User-Agent", "")
if not user_agent:
request.headers.update({"User-Agent": value})
else:
if value not in user_agent:
request.headers.update({"User-Agent": f"{user_agent} {value}"})
48 changes: 48 additions & 0 deletions kiota_http/observability_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Observability options for python http package."""
from kiota_abstractions.request_option import RequestOption


class ObservabilityOptions(RequestOption):
"""Defines the metrics, tracing and logging configurations."""
OBSERVABILITY_OPTION_KEY = "ObservabilityOptionKey"

def __init__(self, enabled: bool = True, include_euii_attributes: bool = True) -> None:
"""Initialize the observability options.

Args:
enabled(bool): whether to enable the ObservabilityOptions in the middleware chain.
include_euii_attributes(bool): whether to include attributes that
could contain EUII information likr URLS.
"""
self._enabled = enabled
self._include_euii_attributes = include_euii_attributes

@property
def enabled(self) -> bool:
"""Gets the enabled option value."""
return self._enabled

@enabled.setter
def enabled(self, value: bool):
"""sets whether to enable ObservabilityOptions in the middleware chain."""
self._enabled = value

@property
def include_euii_attributes(self) -> bool:
"""Returns whether to include EUII attributes."""
return self._include_euii_attributes

@include_euii_attributes.setter
def include_euii_attributes(self, value: bool) -> None:
"""Sets whether to include EUII attributes."""
self._include_euii_attributes = value

@staticmethod
def get_key() -> str:
"""The middleware key name."""
return ObservabilityOptions.OBSERVABILITY_OPTION_KEY

@staticmethod
def get_tracer_instrumentation_name() -> str:
"""Returns the instrumentation name used for tracing"""
return "com.microsoft.com:microsoft-kiota-http-httpx"
Loading