diff --git a/pccommon/pccommon/constants.py b/pccommon/pccommon/constants.py index dbac8e08..0c36b729 100644 --- a/pccommon/pccommon/constants.py +++ b/pccommon/pccommon/constants.py @@ -1,7 +1,5 @@ import os -from opencensus.trace.attributes_helper import COMMON_ATTRIBUTES - CACHE_KEY_ITEM = "/item" DEFAULT_COLLECTION_CONFIG_TABLE_NAME = "collectionconfig" @@ -30,10 +28,11 @@ HTTP_429_TOO_MANY_REQUESTS = 429 -HTTP_PATH = COMMON_ATTRIBUTES["HTTP_PATH"] -HTTP_URL = COMMON_ATTRIBUTES["HTTP_URL"] -HTTP_STATUS_CODE = COMMON_ATTRIBUTES["HTTP_STATUS_CODE"] -HTTP_METHOD = COMMON_ATTRIBUTES["HTTP_METHOD"] +# TODO: switch to new stable http semantic conventions +# https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/ +HTTP_PATH = "http.path" +HTTP_URL = "http.url" +HTTP_METHOD = "http.method" # This is the Azurite storage account key. # This is not a key for a real Storage Account and is publicly accessible diff --git a/pccommon/pccommon/logging.py b/pccommon/pccommon/logging.py index 01aa2125..cd4e1acf 100644 --- a/pccommon/pccommon/logging.py +++ b/pccommon/pccommon/logging.py @@ -7,8 +7,11 @@ from typing import Optional, Tuple, Union, cast from urllib.parse import urlparse +from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter from fastapi import Request -from opencensus.ext.azure.log_exporter import AzureLogHandler +from opentelemetry._logs import set_logger_provider +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from pccommon.config import get_apis_config from pccommon.constants import ( @@ -104,15 +107,19 @@ def init_logging(service_name: str, app_root_path: str) -> None: if config.debug: logger.setLevel(logging.DEBUG) - # Azure log handler instrumentation_key = config.app_insights_instrumentation_key if instrumentation_key: - azure_handler = AzureLogHandler( + logger_provider = LoggerProvider() + set_logger_provider(logger_provider) + + exporter = AzureMonitorLogExporter( connection_string=f"InstrumentationKey={instrumentation_key}" ) - azure_handler.addFilter(CustomDimensionsFilter()) + logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) - logger.addHandler(azure_handler) + otel_handler = LoggingHandler(level=logging.NOTSET) + otel_handler.addFilter(CustomDimensionsFilter()) + logger.addHandler(otel_handler) else: logger.info(f"Azure log handler not attached: {package} (missing key)") diff --git a/pccommon/pccommon/tracing.py b/pccommon/pccommon/tracing.py index bd4cc7e6..26d4fdf4 100644 --- a/pccommon/pccommon/tracing.py +++ b/pccommon/pccommon/tracing.py @@ -1,14 +1,14 @@ import json import logging import re -from typing import List, Optional, Tuple, Union, cast +from typing import Any, List, Optional, Tuple, Union, cast +from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter from fastapi import Request -from opencensus.ext.azure.trace_exporter import AzureExporter -from opencensus.trace import execution_context -from opencensus.trace.samplers import ProbabilitySampler -from opencensus.trace.span import SpanKind -from opencensus.trace.tracer import Tracer +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.trace import Span, SpanKind, Tracer from starlette.datastructures import QueryParams from pccommon.config import get_apis_config @@ -26,17 +26,23 @@ logger = logging.getLogger(__name__) -exporter = ( - AzureExporter( - connection_string=( - f"InstrumentationKey={_config.app_insights_instrumentation_key}" - ) +tracer: Optional[Tracer] + +if _config.app_insights_instrumentation_key: + tracer_provider = TracerProvider() + trace.set_tracer_provider(tracer_provider) + + azure_exporter = AzureMonitorTraceExporter( + connection_string="InstrumentationKey=" + + _config.app_insights_instrumentation_key ) - if _config.app_insights_instrumentation_key - else None -) -is_trace_enabled = exporter is not None + span_processor = BatchSpanProcessor(azure_exporter) + tracer_provider.add_span_processor(span_processor) + + tracer = trace.get_tracer(__name__) +else: + tracer = None async def trace_request( @@ -47,57 +53,29 @@ async def trace_request( request_path = request_to_path(request).strip("/") if _should_trace_request(request): - tracer = Tracer( - exporter=exporter, - sampler=ProbabilitySampler(1.0), - ) - with tracer.span("main") as span: + assert tracer + with tracer.start_as_current_span("main", kind=SpanKind.SERVER) as span: (collection_id, item_id) = await _collection_item_from_request( service_name, request ) - span.span_kind = SpanKind.SERVER - # Throwing the main span into request state lets us create child spans - # in downstream request processing, if there are specific things that - # are slow. request.state.parent_span = span - # Add request dimensions to the trace prior to calling the next middleware - tracer.add_attribute_to_current_span( - attribute_key="ref_id", - attribute_value=request.headers.get(X_AZURE_REF), - ) - tracer.add_attribute_to_current_span( - attribute_key="request_entity", - attribute_value=request.headers.get(X_REQUEST_ENTITY), - ) - tracer.add_attribute_to_current_span( - attribute_key="request_ip", - attribute_value=get_request_ip(request), - ) - tracer.add_attribute_to_current_span( - attribute_key=HTTP_METHOD, attribute_value=str(request.method) - ) - tracer.add_attribute_to_current_span( - attribute_key=HTTP_URL, attribute_value=str(request.url) - ) - tracer.add_attribute_to_current_span( - attribute_key=HTTP_PATH, attribute_value=request_path - ) - tracer.add_attribute_to_current_span( - attribute_key="service", attribute_value=service_name - ) - tracer.add_attribute_to_current_span( - attribute_key="in-server", attribute_value="true" + span.set_attribute("ref_id", request.headers.get(X_AZURE_REF) or "") + span.set_attribute( + "request_entity", request.headers.get(X_REQUEST_ENTITY) or "" ) + span.set_attribute("request_ip", get_request_ip(request) or "") + span.set_attribute(HTTP_METHOD, str(request.method)) + span.set_attribute(HTTP_URL, str(request.url)) + span.set_attribute(HTTP_PATH, request_path) + span.set_attribute("service", service_name) + span.set_attribute("in-server", "true") + if collection_id is not None: - tracer.add_attribute_to_current_span( - attribute_key="collection", attribute_value=collection_id - ) + span.set_attribute("collection", collection_id) if item_id is not None: - tracer.add_attribute_to_current_span( - attribute_key="item", attribute_value=item_id - ) + span.set_attribute("item", item_id) collection_id_re = re.compile( @@ -139,7 +117,7 @@ def _should_trace_request(request: Request) -> bool: - Not a health check endpoint """ return ( - is_trace_enabled + (tracer is not None) and request.method.lower() != "head" and not request.url.path.strip("/").endswith("_mgmt/ping") ) @@ -217,15 +195,17 @@ def add_stac_attributes_from_search(search_json: str, request: Request) -> None: collection_id, item_id = parse_collection_from_search( json.loads(search_json), request.method, request.query_params ) - parent_span = getattr(request.state, "parent_span", None) - current_span = execution_context.get_current_span() or parent_span + current_span: Union[Optional[Any], Span] + current_span = trace.get_current_span() + if not current_span.is_recording(): + current_span = getattr(request.state, "parent_span", None) - if current_span: + if current_span and current_span.is_recording(): if collection_id is not None: - current_span.add_attribute("collection", collection_id) + current_span.set_attribute("collection", collection_id) if item_id is not None: - current_span.add_attribute("item", item_id) + current_span.set_attribute("item", item_id) else: logger.warning("No active or parent span available for adding attributes.") diff --git a/pccommon/pyproject.toml b/pccommon/pyproject.toml index a9e0a0d8..f4797ffa 100644 --- a/pccommon/pyproject.toml +++ b/pccommon/pyproject.toml @@ -11,14 +11,15 @@ requires-python = ">=3.7" dependencies = [ "azure-data-tables>=12.5.0", "azure-identity>=1.16.1", + "azure-monitor-opentelemetry-exporter>=1.0.0b38", "azure-storage-blob>=12.20.0", "cachetools~=5.3", "fastapi==0.112.3", "html-sanitizer>=2.4.4", "idna>=3.7.0", "lxml_html_clean>=0.1.0", - "opencensus-ext-azure>=1.1.13", - "opencensus-ext-logging>=0.1.1", + "opentelemetry-api>=1.34.1", + "opentelemetry-sdk>=1.34.1", "orjson>=3.10.4", "pydantic-settings>=2.3", "pydantic>=2.7", diff --git a/pccommon/requirements.txt b/pccommon/requirements.txt index 99dd5ef1..345408c6 100644 --- a/pccommon/requirements.txt +++ b/pccommon/requirements.txt @@ -6,37 +6,40 @@ # annotated-types==0.7.0 # via pydantic -anyio==4.9.0 +anyio==4.4.0 # via starlette async-timeout==5.0.1 # via redis -azure-core==1.34.0 +azure-core==1.30.2 # via # azure-data-tables # azure-identity + # azure-monitor-opentelemetry-exporter # azure-storage-blob - # opencensus-ext-azure -azure-data-tables==12.7.0 + # msrest +azure-data-tables==12.5.0 # via pccommon (pccommon/pyproject.toml) -azure-identity==1.23.0 +azure-identity==1.17.1 # via - # opencensus-ext-azure + # azure-monitor-opentelemetry-exporter # pccommon (pccommon/pyproject.toml) -azure-storage-blob==12.25.1 +azure-monitor-opentelemetry-exporter==1.0.0b38 # via pccommon (pccommon/pyproject.toml) -beautifulsoup4==4.13.4 +azure-storage-blob==12.20.0 + # via pccommon (pccommon/pyproject.toml) +beautifulsoup4==4.12.3 # via html-sanitizer -cachetools==5.5.2 +cachetools==5.3.3 + # via pccommon (pccommon/pyproject.toml) +certifi==2024.7.4 # via - # google-auth - # pccommon (pccommon/pyproject.toml) -certifi==2025.6.15 - # via requests -cffi==1.17.1 + # msrest + # requests +cffi==1.16.0 # via cryptography -charset-normalizer==3.4.2 +charset-normalizer==3.3.2 # via requests -cryptography==45.0.4 +cryptography==42.0.8 # via # azure-identity # azure-storage-blob @@ -46,133 +49,125 @@ exceptiongroup==1.3.0 # via anyio fastapi==0.112.3 # via pccommon (pccommon/pyproject.toml) -google-api-core==2.25.1 - # via opencensus -google-auth==2.40.3 - # via google-api-core -googleapis-common-protos==1.70.0 - # via google-api-core -html-sanitizer==2.5.0 +fixedint==0.1.6 + # via azure-monitor-opentelemetry-exporter +html-sanitizer==2.4.4 # via pccommon (pccommon/pyproject.toml) -idna==3.10 +idna==3.7 # via # anyio # pccommon (pccommon/pyproject.toml) # requests # yarl -isodate==0.7.2 +importlib-metadata==8.7.0 + # via opentelemetry-api +isodate==0.6.1 # via # azure-data-tables # azure-storage-blob -lxml==5.4.0 + # msrest +lxml==5.2.2 # via # html-sanitizer # lxml-html-clean -lxml-html-clean==0.4.2 +lxml-html-clean==0.1.0 # via # html-sanitizer # pccommon (pccommon/pyproject.toml) -msal==1.32.3 +msal==1.28.1 # via # azure-identity # msal-extensions -msal-extensions==1.3.1 +msal-extensions==1.1.0 # via azure-identity -multidict==6.5.0 +msrest==0.7.1 + # via azure-monitor-opentelemetry-exporter +multidict==6.0.5 # via yarl -opencensus==0.11.4 +oauthlib==3.3.1 + # via requests-oauthlib +opentelemetry-api==1.34.1 # via - # opencensus-ext-azure - # opencensus-ext-logging -opencensus-context==0.1.3 - # via opencensus -opencensus-ext-azure==1.1.15 - # via pccommon (pccommon/pyproject.toml) -opencensus-ext-logging==0.1.1 - # via pccommon (pccommon/pyproject.toml) -orjson==3.10.18 + # azure-monitor-opentelemetry-exporter + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # pccommon (pccommon/pyproject.toml) +opentelemetry-sdk==1.34.1 + # via + # azure-monitor-opentelemetry-exporter + # pccommon (pccommon/pyproject.toml) +opentelemetry-semantic-conventions==0.55b1 + # via opentelemetry-sdk +orjson==3.10.5 # via pccommon (pccommon/pyproject.toml) -propcache==0.3.2 - # via yarl -proto-plus==1.26.1 - # via google-api-core -protobuf==6.31.1 - # via - # google-api-core - # googleapis-common-protos - # proto-plus -psutil==7.0.0 - # via opencensus-ext-azure -pyasn1==0.6.1 - # via - # pyasn1-modules - # rsa -pyasn1-modules==0.4.2 - # via google-auth +packaging==24.1 + # via msal-extensions +portalocker==2.8.2 + # via msal-extensions +psutil==5.9.8 + # via azure-monitor-opentelemetry-exporter pycparser==2.22 # via cffi -pydantic==2.11.7 +pydantic==2.7.4 # via # fastapi # pccommon (pccommon/pyproject.toml) # pydantic-settings -pydantic-core==2.33.2 +pydantic-core==2.18.4 # via pydantic -pydantic-settings==2.9.1 +pydantic-settings==2.3.3 # via pccommon (pccommon/pyproject.toml) -pyhumps==3.8.0 +pyhumps==3.5.3 # via pccommon (pccommon/pyproject.toml) -pyjwt[crypto]==2.10.1 +pyjwt[crypto]==2.8.0 # via msal -python-dotenv==1.1.0 +python-dotenv==1.0.1 # via pydantic-settings -redis==6.2.0 +redis==4.6.0 # via pccommon (pccommon/pyproject.toml) -requests==2.32.4 +requests==2.32.3 # via # azure-core - # google-api-core # msal - # opencensus-ext-azure + # msrest # pccommon (pccommon/pyproject.toml) -rsa==4.9.1 - # via google-auth -six==1.17.0 + # requests-oauthlib +requests-oauthlib==2.0.0 + # via msrest +six==1.16.0 # via # azure-core - # opencensus + # isodate sniffio==1.3.1 # via anyio -soupsieve==2.7 +soupsieve==2.5 # via beautifulsoup4 -starlette==0.38.6 +starlette==0.37.2 # via # fastapi # pccommon (pccommon/pyproject.toml) -types-cachetools==6.0.0.20250525 +types-cachetools==4.2.9 # via pccommon (pccommon/pyproject.toml) -typing-extensions==4.14.0 +typing-extensions==4.12.2 # via # anyio # azure-core # azure-data-tables # azure-identity # azure-storage-blob - # beautifulsoup4 # exceptiongroup # fastapi - # multidict + # opentelemetry-api + # opentelemetry-sdk + # opentelemetry-semantic-conventions # pydantic # pydantic-core # starlette - # typing-inspection -typing-inspection==0.4.1 - # via - # pydantic - # pydantic-settings -urllib3==2.4.0 +urllib3==2.2.2 # via # pccommon (pccommon/pyproject.toml) # requests -yarl==1.20.1 +yarl==1.9.4 # via azure-data-tables +zipp==3.23.0 + # via importlib-metadata