diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index ef8f77c6..02fbbe70 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -9,7 +9,7 @@ permissions: jobs: quality: - name: Black, Pylint, and Mypy + name: Ruff, Pylint, and Mypy runs-on: ubuntu-latest steps: @@ -24,8 +24,8 @@ jobs: - name: Install tox run: python -m pip install --upgrade pip tox - - name: Check formatting with Black - run: tox -e black + - name: Lint and check formatting with Ruff + run: tox -e ruff - name: Lint with Pylint run: tox -e pylint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1613bade..aae051b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,13 +29,13 @@ python -m pip install -e .[dev] tox Run all environments at once or pick individual ones. ```bash -# Run everything (pylint, mypy, black, tests on Python 3.10-3.14) +# Run everything (pylint, mypy, ruff, tests on Python 3.10-3.14) tox # Run a single check tox -e pylint # Lint with pylint tox -e mypy # Type-check with mypy -tox -e black # Format with black +tox -e ruff # Lint and check formatting with ruff # Run tests on a specific Python version tox -e pytest-py312 diff --git a/dev_requirements.txt b/dev_requirements.txt index 875f20b5..2121a8fb 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -2,6 +2,6 @@ pytest>=8.0 pytest-asyncio>=0.23.0 pytest-cov>=5.0 pytest-benchmark>=4.0 -black>=24.0 +ruff>=0.6 microsoft-agents-activity microsoft-agents-hosting-core diff --git a/pyproject.toml b/pyproject.toml index 5c0c9064..98179030 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,13 +67,13 @@ test = [ "pytest-benchmark>=4.0", ] dev = [ - "black>=24.0", "pylint>=3.3", "mypy>=1.10", "pyright>=1.1", "pytest>=8.0", "pytest-cov>=5.0", "pytest-benchmark>=4.0", + "ruff>=0.6", ] docs = [ "sphinx>=7.0", @@ -100,10 +100,6 @@ package-dir = {"" = "src"} where = ["src"] namespaces = true -[tool.black] -line-length = 120 -target-version = ["py310"] - [tool.pylint.main] py-version = "3.10" @@ -147,3 +143,38 @@ testpaths = ["tests"] # Skip pytest-benchmark tests by default; the perf workflow opts in with # --benchmark-only. addopts = "--benchmark-skip" + +[tool.ruff] +line-length = 120 +target-version = "py310" +extend-exclude = [ + ".venv", + "build", + "dist", + "src/microsoft_opentelemetry.egg-info", + "docs", +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "B", # flake8-bugbear + "UP", # pyupgrade + "SIM", # flake8-simplify + "C4", # flake8-comprehensions +] +ignore = [ + "E501", # line length handled by formatter + "UP007", + "UP045", +] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["B011"] +"__init__.py" = ["F401"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" diff --git a/samples/a365/manual_telemetry.py b/samples/a365/manual_telemetry.py index c314611a..1503f4ca 100644 --- a/samples/a365/manual_telemetry.py +++ b/samples/a365/manual_telemetry.py @@ -112,7 +112,6 @@ def main(): agent_details=agent, caller_details=caller, ) as invoke_scope: - # Record structured input messages invoke_scope.record_input_messages( InputMessages( @@ -139,7 +138,6 @@ def main(): agent_details=agent, user_details=user, ) as inference_scope: - inference_scope.record_input_messages( InputMessages( messages=[ @@ -190,7 +188,6 @@ def main(): agent_details=agent, user_details=user, ) as tool_scope: - # Simulate tool execution time.sleep(0.02) tool_result = '{"temperature": 62, "condition": "Partly cloudy"}' @@ -213,7 +210,6 @@ def main(): agent_details=agent, user_details=user, ) as inference_scope_2: - time.sleep(0.05) final_answer = "It's currently 62°F and partly cloudy in Seattle." diff --git a/samples/distro/fastapi_app.py b/samples/distro/fastapi_app.py index 666ab8b8..c394c88e 100644 --- a/samples/distro/fastapi_app.py +++ b/samples/distro/fastapi_app.py @@ -17,7 +17,7 @@ logger = getLogger(__name__) logger.setLevel(INFO) -from fastapi import FastAPI # pylint: disable=wrong-import-position +from fastapi import FastAPI # noqa: E402, pylint: disable=wrong-import-position app = FastAPI() diff --git a/samples/langchain/validate_traces.py b/samples/langchain/validate_traces.py index e68c670f..cd5c95b8 100644 --- a/samples/langchain/validate_traces.py +++ b/samples/langchain/validate_traces.py @@ -134,9 +134,9 @@ def check(label, condition, detail=""): print(f" {status} {label}" + (f" — {detail}" if detail else "")) # ── LLM span checks ──────────────────────────────────────────────── - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(f"LLM SPANS ({len(llm_spans)} found)") - print(f"{'='*60}") + print(f"{'=' * 60}") for span in llm_spans: attrs = span.attributes @@ -150,12 +150,10 @@ def check(label, condition, detail=""): # ── Responses-API fake span: response.model / response.id from # message.response_metadata (the bug this fix addresses) ───────── - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print("RESPONSES-API FAKE SPAN CHECKS") - print(f"{'='*60}") - responses_spans = [ - s for s in llm_spans if s.attributes.get("gen_ai.response.id") == "resp_abc123" - ] + print(f"{'=' * 60}") + responses_spans = [s for s in llm_spans if s.attributes.get("gen_ai.response.id") == "resp_abc123"] check("Responses-API fake LLM span found", len(responses_spans) == 1, f"found {len(responses_spans)}") if responses_spans: attrs = responses_spans[0].attributes @@ -171,9 +169,9 @@ def check(label, condition, detail=""): ) # ── Tool span checks ─────────────────────────────────────────────── - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(f"TOOL SPANS ({len(tool_spans)} found)") - print(f"{'='*60}") + print(f"{'=' * 60}") for span in tool_spans: attrs = span.attributes @@ -194,9 +192,9 @@ def check(label, condition, detail=""): check("gen_ai.tool.call.result present (content on)", "gen_ai.tool.call.result" in attrs) # ── All-span attribute dump ───────────────────────────────────────── - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print("ALL SPANS ATTRIBUTE DUMP") - print(f"{'='*60}") + print(f"{'=' * 60}") for span in spans: print(f"\n [{span.kind.name}] {span.name}") for k, v in sorted(span.attributes.items()): @@ -204,12 +202,12 @@ def check(label, condition, detail=""): print(f" {k} = {val}") # ── Summary ───────────────────────────────────────────────────────── - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") if ok: print("✅ ALL CHECKS PASSED") else: print("❌ SOME CHECKS FAILED") - print(f"{'='*60}") + print(f"{'=' * 60}") inst.uninstrument() provider.shutdown() diff --git a/samples/microsoft_agent_framework/sample_maf_agent.py b/samples/microsoft_agent_framework/sample_maf_agent.py index cfbcd166..171a88d5 100644 --- a/samples/microsoft_agent_framework/sample_maf_agent.py +++ b/samples/microsoft_agent_framework/sample_maf_agent.py @@ -60,4 +60,4 @@ async def main(): if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/src/microsoft/opentelemetry/_azure_monitor/_autoinstrumentation/configurator.py b/src/microsoft/opentelemetry/_azure_monitor/_autoinstrumentation/configurator.py index 089a100d..34572c62 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_autoinstrumentation/configurator.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_autoinstrumentation/configurator.py @@ -41,7 +41,7 @@ class AzureMonitorConfigurator(_OTelSDKConfigurator): def _configure(self, **kwargs): if not _is_attach_enabled(): - warn(_PREVIEW_ENTRY_POINT_WARNING) + warn(_PREVIEW_ENTRY_POINT_WARNING, stacklevel=2) try: if environ.get(OTEL_TRACES_EXPORTER, "").lower().strip() != "none": kwargs.setdefault(TRACE_EXPORTER_NAMES_ARG, ["azure_monitor_opentelemetry_exporter"]) @@ -63,7 +63,7 @@ def _configure(self, **kwargs): ) except Exception as e: AzureDiagnosticLogging.error( # pylint: disable=C - "Azure Monitor Configurator failed during configuration: %s" % str(e), + "Azure Monitor Configurator failed during configuration: %s" % str(e), # noqa: UP031 _ATTACH_FAILURE_CONFIGURATOR, ) raise e diff --git a/src/microsoft/opentelemetry/_azure_monitor/_autoinstrumentation/distro.py b/src/microsoft/opentelemetry/_azure_monitor/_autoinstrumentation/distro.py index dc21233f..4bb87fa2 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_autoinstrumentation/distro.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_autoinstrumentation/distro.py @@ -40,7 +40,7 @@ class AzureMonitorDistro(BaseDistro): def _configure(self, **kwargs) -> None: if not _is_attach_enabled(): - warn(_PREVIEW_ENTRY_POINT_WARNING) + warn(_PREVIEW_ENTRY_POINT_WARNING, stacklevel=2) try: _configure_auto_instrumentation() AzureStatusLogger.log_status(True) @@ -51,7 +51,7 @@ def _configure(self, **kwargs) -> None: except Exception as e: AzureStatusLogger.log_status(False, reason=str(e)) AzureDiagnosticLogging.error( # pylint: disable=C - "Azure Monitor OpenTelemetry Distro failed during configuration: %s" % str(e), + "Azure Monitor OpenTelemetry Distro failed during configuration: %s" % str(e), # noqa: UP031 _ATTACH_FAILURE_DISTRO, ) raise e diff --git a/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/_config.py b/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/_config.py index 451dcc6f..c4774914 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/_config.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/_config.py @@ -4,7 +4,7 @@ # license information. # ------------------------------------------------------------------------- -from typing import Optional, Dict, Any +from typing import Optional, Any class BrowserSDKConfig: @@ -34,13 +34,13 @@ def __init__(self, enabled: bool = True, connection_string: Optional[str] = None self.enabled = enabled self.connection_string = connection_string - def to_dict(self) -> Dict[str, Dict[str, Any]]: + def to_dict(self) -> dict[str, dict[str, Any]]: """Convert the config to a dictionary for the web snippet. :return: Dictionary representation of the configuration. :rtype: dict """ - cfg: Dict[str, Any] = {} + cfg: dict[str, Any] = {} if self.connection_string: cfg["connectionString"] = self.connection_string return {"cfg": cfg} diff --git a/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/django_middleware.py b/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/django_middleware.py index acf3db55..f8a11630 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/django_middleware.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/django_middleware.py @@ -4,7 +4,8 @@ # license information. # ------------------------------------------------------------------------- -from typing import Optional, Callable, Any, Union +from typing import Optional, Any, Union +from collections.abc import Callable from logging import getLogger try: diff --git a/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/snippet_injector.py b/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/snippet_injector.py index 6afad1a8..fb26fb65 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/snippet_injector.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/snippet_injector.py @@ -8,7 +8,7 @@ import importlib import re from logging import getLogger -from typing import Any, Dict, Optional, Tuple +from typing import Any, Optional from ._config import BrowserSDKConfig @@ -194,7 +194,7 @@ def inject_with_compression( content: bytes, content_encoding: Optional[str] = None, encoding: str = "utf-8", - ) -> Tuple[bytes, Optional[str]]: # pylint: disable=too-many-return-statements + ) -> tuple[bytes, Optional[str]]: # pylint: disable=too-many-return-statements """Inject snippet handling compression/decompression efficiently using cached decompressed content. :param content: Response content bytes, potentially compressed. @@ -264,7 +264,7 @@ def _get_decompressed_content(self, content: bytes, content_encoding: Optional[s :rtype: bytes """ # Fast path: if no encoding specified and content doesn't look compressed, return as-is - if not content_encoding: + if not content_encoding: # noqa: SIM102 # Quick check for compression headers to avoid unnecessary processing if not self._appears_compressed(content): return content @@ -308,9 +308,7 @@ def _appears_compressed(self, content: bytes) -> bool: # Brotli: no reliable magic number, but check for common patterns # (Brotli detection is harder without trying to decompress) # zlib/deflate: starts with 0x78 followed by various values - if content[0] == 0x78 and content[1] in (0x01, 0x5E, 0x9C, 0xDA): - return True - return False + return content[0] == 0x78 and content[1] in (0x01, 0x5E, 0x9C, 0xDA) def _clear_decompression_cache(self) -> None: """Clear the decompressed content cache. @@ -464,7 +462,7 @@ def _format_config_value(self, value): return self._dict_to_js_object(value) return f'"{str(value)}"' - def _dict_to_js_object(self, obj: Dict[str, Any]) -> str: + def _dict_to_js_object(self, obj: dict[str, Any]) -> str: """Convert Python dict to JavaScript object literal. :param obj: Python dictionary to convert to JavaScript object syntax. diff --git a/src/microsoft/opentelemetry/_azure_monitor/_configure.py b/src/microsoft/opentelemetry/_azure_monitor/_configure.py index 8847b171..a91cd389 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_configure.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_configure.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- from logging import getLogger, Formatter -from typing import Any, Dict, List, Optional, cast +from typing import Any, Optional, cast from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader, MetricReader from opentelemetry.sdk.metrics.view import View @@ -151,7 +151,7 @@ def configure_azure_monitor(**kwargs) -> None: # pylint: disable=C4758 _setup_browser_sdk_loader(configurations) -def _setup_tracing(configurations: Dict[str, ConfigurationValue]): +def _setup_tracing(configurations: dict[str, ConfigurationValue]): resource: Resource = configurations[RESOURCE_ARG] # type: ignore enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG] if SAMPLING_ARG in configurations: @@ -188,7 +188,7 @@ def _setup_tracing(configurations: Dict[str, ConfigurationValue]): return tracer_provider -def _setup_logging(configurations: Dict[str, ConfigurationValue]): # pylint: disable=inconsistent-return-statements +def _setup_logging(configurations: dict[str, ConfigurationValue]): # pylint: disable=inconsistent-return-statements # Setup logging # Use try catch while signal is experimental try: @@ -248,9 +248,9 @@ def _setup_logging(configurations: Dict[str, ConfigurationValue]): # pylint: di ) -def _setup_metrics(configurations: Dict[str, ConfigurationValue]): +def _setup_metrics(configurations: dict[str, ConfigurationValue]): resource: Resource = configurations[RESOURCE_ARG] # type: ignore - views: List[View] = configurations[VIEWS_ARG] # type: ignore + views: list[View] = configurations[VIEWS_ARG] # type: ignore readers: list[MetricReader] = configurations[METRIC_READERS_ARG] # type: ignore enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG] metric_exporter = AzureMonitorMetricExporter(**configurations) @@ -269,10 +269,10 @@ def _setup_live_metrics(configurations): enable_live_metrics(**configurations) -def _setup_azure_instrumentations(configurations: Dict[str, ConfigurationValue]): +def _setup_azure_instrumentations(configurations: dict[str, ConfigurationValue]): """Set up Azure-specific instrumentations (Azure SDK tracing, AI instrumentors).""" # Azure Core tracing - if not configurations[DISABLE_TRACING_ARG]: + if not configurations[DISABLE_TRACING_ARG]: # noqa: SIM102 if _is_instrumentation_enabled(configurations, _AZURE_SDK_INSTRUMENTATION_NAME): try: from azure.core.settings import settings @@ -313,7 +313,7 @@ def _send_attach_warning(): def _setup_additional_azure_sdk_instrumentations( - configurations: Dict[str, ConfigurationValue], + configurations: dict[str, ConfigurationValue], ): if _AZURE_SDK_INSTRUMENTATION_NAME not in _ALL_SUPPORTED_INSTRUMENTED_LIBRARIES: return @@ -353,7 +353,7 @@ def _setup_additional_azure_sdk_instrumentations( ) -def _setup_browser_sdk_loader(configurations: Dict[str, ConfigurationValue]): +def _setup_browser_sdk_loader(configurations: dict[str, ConfigurationValue]): """Setup browser SDK loader for supported frameworks. :param configurations: Configuration dictionary containing browser SDK loader settings. @@ -366,7 +366,7 @@ def _setup_browser_sdk_loader(configurations: Dict[str, ConfigurationValue]): browser_sdk_loader_config = browser_sdk_loader_config_value else: # Create typed empty dict to satisfy mypy - browser_sdk_loader_config = cast(Dict[str, Any], {}) + browser_sdk_loader_config = cast(dict[str, Any], {}) # Check if browser SDK loader should be enabled (default False) enabled = browser_sdk_loader_config.get("enabled", False) diff --git a/src/microsoft/opentelemetry/_azure_monitor/_diagnostics/diagnostic_logging.py b/src/microsoft/opentelemetry/_azure_monitor/_diagnostics/diagnostic_logging.py index a1a50a64..c1518179 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_diagnostics/diagnostic_logging.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_diagnostics/diagnostic_logging.py @@ -4,6 +4,7 @@ # license information. # -------------------------------------------------------------------------- +import contextlib import logging import threading from os import makedirs @@ -60,50 +61,47 @@ def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: - cls._instance = super(AzureDiagnosticLogging, cls).__new__(cls) + cls._instance = super().__new__(cls) return cls._instance @classmethod def _initialize(cls): with cls._lock: - if not cls._initialized: - if _is_diagnostics_enabled() and _DIAGNOSTIC_LOG_PATH: - log_format = ( - "{" - + '"time":"%(asctime)s.%(msecs)03d", ' - + '"level":"%(levelname)s", ' - + '"logger":"%(name)s", ' - + '"message":"%(message)s", ' - + '"properties":{' - + '"operation":"Startup", ' - + f'"siteName":"{_SITE_NAME}", ' - + f'"ikey":"{_get_customer_ikey_from_env_var()}", ' - + f'"extensionVersion":"{_EXTENSION_VERSION}", ' - + f'"sdkVersion":"{VERSION}", ' - + f'"subscriptionId":"{_SUBSCRIPTION_ID}", ' - + '"msgId":"%(msgId)s", ' - + '"language":"python"' - + "}" - + "}" - ) - try: - if not exists(_DIAGNOSTIC_LOG_PATH): - try: - makedirs(_DIAGNOSTIC_LOG_PATH) - # Multi-thread can create a race condition for creating the log file - except FileExistsError: - pass - f_handler = logging.FileHandler(join(_DIAGNOSTIC_LOG_PATH, _DIAGNOSTIC_LOGGER_FILE_NAME)) - formatter = logging.Formatter(fmt=log_format, datefmt="%Y-%m-%dT%H:%M:%S") - f_handler.setFormatter(formatter) - _diagnostic_file_logger.addHandler(f_handler) - cls._initialized = True - except Exception as e: # pylint: disable=broad-except - _logger.error( - "Failed to initialize Azure Monitor diagnostic logging: %s", - e, - ) # pylint: disable=do-not-log-exceptions-if-not-debug - cls._initialized = False + if not cls._initialized and _is_diagnostics_enabled() and _DIAGNOSTIC_LOG_PATH: + log_format = ( + "{" + + '"time":"%(asctime)s.%(msecs)03d", ' + + '"level":"%(levelname)s", ' + + '"logger":"%(name)s", ' + + '"message":"%(message)s", ' + + '"properties":{' + + '"operation":"Startup", ' + + f'"siteName":"{_SITE_NAME}", ' + + f'"ikey":"{_get_customer_ikey_from_env_var()}", ' + + f'"extensionVersion":"{_EXTENSION_VERSION}", ' + + f'"sdkVersion":"{VERSION}", ' + + f'"subscriptionId":"{_SUBSCRIPTION_ID}", ' + + '"msgId":"%(msgId)s", ' + + '"language":"python"' + + "}" + + "}" + ) + try: + if not exists(_DIAGNOSTIC_LOG_PATH): + # Multi-thread can create a race condition for creating the log file + with contextlib.suppress(FileExistsError): + makedirs(_DIAGNOSTIC_LOG_PATH) + f_handler = logging.FileHandler(join(_DIAGNOSTIC_LOG_PATH, _DIAGNOSTIC_LOGGER_FILE_NAME)) + formatter = logging.Formatter(fmt=log_format, datefmt="%Y-%m-%dT%H:%M:%S") + f_handler.setFormatter(formatter) + _diagnostic_file_logger.addHandler(f_handler) + cls._initialized = True + except Exception as e: # pylint: disable=broad-except + _logger.error( + "Failed to initialize Azure Monitor diagnostic logging: %s", + e, + ) # pylint: disable=do-not-log-exceptions-if-not-debug + cls._initialized = False @classmethod def debug(cls, message: str, message_id: str): diff --git a/src/microsoft/opentelemetry/_azure_monitor/_types.py b/src/microsoft/opentelemetry/_azure_monitor/_types.py index 62f01fc9..ea8839c7 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_types.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_types.py @@ -4,7 +4,8 @@ # license information. # -------------------------------------------------------------------------- -from typing import Sequence, Union +from typing import Union +from collections.abc import Sequence from opentelemetry.sdk.metrics.export import MetricReader from opentelemetry.sdk.metrics.view import View diff --git a/src/microsoft/opentelemetry/_azure_monitor/_utils/configurations.py b/src/microsoft/opentelemetry/_azure_monitor/_utils/configurations.py index a10cc145..1918736e 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_utils/configurations.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_utils/configurations.py @@ -6,7 +6,6 @@ from logging import getLogger, Formatter from os import environ -from typing import Dict from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, @@ -85,7 +84,7 @@ _logger = getLogger(__name__) -def _get_configurations(**kwargs) -> Dict[str, ConfigurationValue]: +def _get_configurations(**kwargs) -> dict[str, ConfigurationValue]: configurations = {} for key, val in kwargs.items(): @@ -115,25 +114,22 @@ def _get_configurations(**kwargs) -> Dict[str, ConfigurationValue]: def _default_disable_logging(configurations): default = False - if OTEL_LOGS_EXPORTER in environ: - if environ[OTEL_LOGS_EXPORTER].lower().strip() == "none": - default = True + if OTEL_LOGS_EXPORTER in environ and environ[OTEL_LOGS_EXPORTER].lower().strip() == "none": + default = True configurations[DISABLE_LOGGING_ARG] = default def _default_disable_metrics(configurations): default = False - if OTEL_METRICS_EXPORTER in environ: - if environ[OTEL_METRICS_EXPORTER].lower().strip() == "none": - default = True + if OTEL_METRICS_EXPORTER in environ and environ[OTEL_METRICS_EXPORTER].lower().strip() == "none": + default = True configurations[DISABLE_METRICS_ARG] = default def _default_disable_tracing(configurations): default = False - if OTEL_TRACES_EXPORTER in environ: - if environ[OTEL_TRACES_EXPORTER].lower().strip() == "none": - default = True + if OTEL_TRACES_EXPORTER in environ and environ[OTEL_TRACES_EXPORTER].lower().strip() == "none": + default = True configurations[DISABLE_TRACING_ARG] = default @@ -353,7 +349,7 @@ def _default_browser_sdk_loader(configurations): # Use cast to Dict[str, Any] to avoid MyPy ConfigurationValue Union type issues from typing import cast, Any - configurations.setdefault(BROWSER_SDK_LOADER_CONFIG_ARG, cast(Dict[str, Any], {})) + configurations.setdefault(BROWSER_SDK_LOADER_CONFIG_ARG, cast(dict[str, Any], {})) def _get_otel_disabled_instrumentations(): diff --git a/src/microsoft/opentelemetry/_azure_monitor/_utils/instrumentation.py b/src/microsoft/opentelemetry/_azure_monitor/_utils/instrumentation.py index f5a2f9ba..5106aeeb 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_utils/instrumentation.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_utils/instrumentation.py @@ -7,7 +7,7 @@ from __future__ import annotations from logging import getLogger -from typing import Collection +from collections.abc import Collection from packaging.requirements import InvalidRequirement, Requirement diff --git a/src/microsoft/opentelemetry/_distro.py b/src/microsoft/opentelemetry/_distro.py index 45f2de8c..9ff09e6d 100644 --- a/src/microsoft/opentelemetry/_distro.py +++ b/src/microsoft/opentelemetry/_distro.py @@ -6,7 +6,7 @@ import os from functools import cached_property from logging import getLogger, Formatter -from typing import Any, Dict, List, Optional +from typing import Any, Optional from opentelemetry.metrics import set_meter_provider from opentelemetry.sdk.metrics import MeterProvider @@ -221,8 +221,8 @@ def use_microsoft_opentelemetry(**kwargs: object) -> None: # pylint: disable=to enable_sensitive_data: bool = bool(kwargs.pop(ENABLE_SENSITIVE_DATA_ARG, False)) # Separate Azure Monitor kwargs from generic OTel kwargs - otel_kwargs: Dict[str, Any] = {k: v for k, v in kwargs.items() if k not in _AZURE_MONITOR_KWARG_MAP} - azure_monitor_kwargs: Dict[str, Any] = { + otel_kwargs: dict[str, Any] = {k: v for k, v in kwargs.items() if k not in _AZURE_MONITOR_KWARG_MAP} + azure_monitor_kwargs: dict[str, Any] = { _AZURE_MONITOR_KWARG_MAP[k]: v for k, v in kwargs.items() if k in _AZURE_MONITOR_KWARG_MAP } # pylint: disable=line-too-long @@ -401,7 +401,7 @@ def _bridge_sdkstats_to_azure_monitor() -> None: def _append_a365_components( enable_a365: bool, - otel_kwargs: Dict[str, Any], + otel_kwargs: dict[str, Any], token_resolver: Any = None, cluster_category: Any = None, use_s2s_endpoint: Any = None, @@ -500,7 +500,7 @@ def _append_a365_components( # Enriching batch processor wrapping the exporter. # Only forward batch parameters when the user explicitly supplied # them so that BatchSpanProcessor uses its own defaults otherwise. - batch_kwargs: Dict[str, Any] = {} + batch_kwargs: dict[str, Any] = {} if max_queue_size is not None: batch_kwargs["max_queue_size"] = max_queue_size if scheduled_delay_ms is not None: @@ -529,7 +529,7 @@ def _append_a365_components( def _append_spectra_components( enable_spectra: bool, - otel_kwargs: Dict[str, Any], + otel_kwargs: dict[str, Any], endpoint: Any = None, protocol: Any = None, insecure: Any = None, @@ -616,7 +616,7 @@ def _append_spectra_components( def _setup_tracing( resource: Resource, - otel_kwargs: Dict[str, Any], + otel_kwargs: dict[str, Any], ) -> TracerProvider: """Create a TracerProvider with user-supplied span processors.""" tracer_provider = TracerProvider(resource=resource) @@ -627,11 +627,11 @@ def _setup_tracing( def _setup_metrics( resource: Resource, - otel_kwargs: Dict[str, Any], + otel_kwargs: dict[str, Any], ) -> MeterProvider: """Create a MeterProvider with user-supplied readers/views.""" - readers: List[MetricReader] = list(otel_kwargs.get(METRIC_READERS_ARG) or []) - views: List[View] = list(otel_kwargs.get(VIEWS_ARG) or []) + readers: list[MetricReader] = list(otel_kwargs.get(METRIC_READERS_ARG) or []) + views: list[View] = list(otel_kwargs.get(VIEWS_ARG) or []) meter_provider = MeterProvider( metric_readers=readers, resource=resource, @@ -642,7 +642,7 @@ def _setup_metrics( def _setup_logging( resource: Resource, - otel_kwargs: Dict[str, Any], + otel_kwargs: dict[str, Any], ) -> LoggerProvider | None: """Create a LoggerProvider with user-supplied processors.""" logger_provider = LoggerProvider(resource=resource) @@ -669,7 +669,7 @@ def _setup_logging( class _EntryPointDistFinder: @cached_property - def _mapping(self) -> Dict[str, Any]: + def _mapping(self) -> dict[str, Any]: return {self._key_for(ep): dist for dist in distributions() for ep in dist.entry_points} def dist_for(self, entry_point: EntryPoint) -> Any: @@ -683,7 +683,7 @@ def _key_for(entry_point: EntryPoint) -> str: return f"{entry_point.group}:{entry_point.name}:{entry_point.value}" -def _is_instrumentation_enabled(otel_kwargs: Dict[str, Any], lib_name: str) -> bool: +def _is_instrumentation_enabled(otel_kwargs: dict[str, Any], lib_name: str) -> bool: """Check if a library instrumentation is enabled via instrumentation_options.""" options = otel_kwargs.get(INSTRUMENTATION_OPTIONS_ARG) if not options or lib_name not in options: @@ -695,7 +695,7 @@ def _is_instrumentation_enabled(otel_kwargs: Dict[str, Any], lib_name: str) -> b return lib_options["enabled"] is True -def _get_instrumentation_kwargs(otel_kwargs: Dict[str, Any], lib_name: str) -> Dict[str, Any]: +def _get_instrumentation_kwargs(otel_kwargs: dict[str, Any], lib_name: str) -> dict[str, Any]: """Extract per-library kwargs from instrumentation_options (everything except 'enabled').""" options = otel_kwargs.get(INSTRUMENTATION_OPTIONS_ARG) if not options or lib_name not in options: @@ -704,7 +704,7 @@ def _get_instrumentation_kwargs(otel_kwargs: Dict[str, Any], lib_name: str) -> D return {k: v for k, v in lib_options.items() if k != "enabled"} -def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None: +def _setup_instrumentations(otel_kwargs: dict[str, Any], **kwargs: Any) -> None: """Discover and activate OTel instrumentations for supported libraries.""" enable_a365: bool = kwargs.pop("enable_a365", False) enable_sensitive_data: bool = kwargs.pop(ENABLE_SENSITIVE_DATA_ARG, False) diff --git a/src/microsoft/opentelemetry/_genai/_langchain/_tracer.py b/src/microsoft/opentelemetry/_genai/_langchain/_tracer.py index 213afb33..63e9f0dc 100644 --- a/src/microsoft/opentelemetry/_genai/_langchain/_tracer.py +++ b/src/microsoft/opentelemetry/_genai/_langchain/_tracer.py @@ -230,10 +230,7 @@ def _start_trace(self, run: Run) -> None: framework_name = self._resolve_framework_name(run) span_name = f"{INVOKE_AGENT_OPERATION_NAME} {framework_name}" - if run.run_type.lower() in ("llm", "chat_model"): - span_kind = SpanKind.CLIENT - else: - span_kind = SpanKind.INTERNAL + span_kind = SpanKind.CLIENT if run.run_type.lower() in ("llm", "chat_model") else SpanKind.INTERNAL span = self._tracer.start_span( name=span_name, @@ -395,9 +392,7 @@ def _is_agent_run(self, run: Run) -> bool: if not self._is_agent_like_chain(run): return False # Don't nest agents — if a parent is already an agent, this is internal - if run.parent_run_id and run.parent_run_id in self._agent_run_ids: - return False - return True + return not (run.parent_run_id and run.parent_run_id in self._agent_run_ids) def _resolve_agent_name(self, run: Run) -> str | None: """Resolve agent name from config override, run metadata, or run name.""" @@ -413,10 +408,9 @@ def _resolve_agent_name(self, run: Run) -> str | None: if name := meta.get("lc_agent_name"): return str(name) # 3. From serialized graph name - if run.serialized and isinstance(run.serialized, dict): - if name := run.serialized.get("name"): - if name != "LangGraph": # avoid generic name - return str(name) + if run.serialized and isinstance(run.serialized, dict) and (name := run.serialized.get("name")): # noqa: SIM102 + if name != "LangGraph": # avoid generic name + return str(name) # 4. Run name itself if it's not just "LangGraph" if run.name and run.name != "LangGraph": return str(run.name) diff --git a/src/microsoft/opentelemetry/_genai/_langchain/_tracer_instrumentor.py b/src/microsoft/opentelemetry/_genai/_langchain/_tracer_instrumentor.py index f8654d63..ede1d596 100644 --- a/src/microsoft/opentelemetry/_genai/_langchain/_tracer_instrumentor.py +++ b/src/microsoft/opentelemetry/_genai/_langchain/_tracer_instrumentor.py @@ -197,9 +197,8 @@ def _current_parent_run_id() -> UUID | None: if not isinstance(config, dict): return None for v in config.values(): - if isinstance(v, langchain_core.callbacks.BaseCallbackManager): - if v.parent_run_id: - return UUID(str(v.parent_run_id)) + if isinstance(v, langchain_core.callbacks.BaseCallbackManager) and v.parent_run_id: + return UUID(str(v.parent_run_id)) return None diff --git a/src/microsoft/opentelemetry/_genai/_langchain/_utils.py b/src/microsoft/opentelemetry/_genai/_langchain/_utils.py index 2425cb67..ef36e4da 100644 --- a/src/microsoft/opentelemetry/_genai/_langchain/_utils.py +++ b/src/microsoft/opentelemetry/_genai/_langchain/_utils.py @@ -71,6 +71,7 @@ from opentelemetry.util.types import AttributeValue from wrapt import ObjectProxy +import contextlib try: from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( @@ -307,7 +308,7 @@ def input_messages( elif hasattr(message_data, "get"): if content := message_data.get("content"): contents.append(str(content)) - elif kwargs := message_data.get("kwargs"): + elif kwargs := message_data.get("kwargs"): # noqa: SIM102 if hasattr(kwargs, "get") and (content := kwargs.get("content")): contents.append(str(content)) elif isinstance(message_data, Sequence) and len(message_data) == 2: @@ -387,7 +388,7 @@ def output_messages( elif hasattr(message_data, "get"): if content := message_data.get("content"): contents.append(str(content)) - elif kwargs := message_data.get("kwargs"): + elif kwargs := message_data.get("kwargs"): # noqa: SIM102 if hasattr(kwargs, "get") and (content := kwargs.get("content")): contents.append(str(content)) if contents: @@ -469,10 +470,8 @@ def _first_param(*keys: str) -> Any: # gen_ai.request.top_k if (top_k_val := _first_param("top_k")) is not None: - try: + with contextlib.suppress(ValueError, TypeError): yield GEN_AI_REQUEST_TOP_K_KEY, float(top_k_val) - except (ValueError, TypeError): - pass # gen_ai.openai.request.response_format + gen_ai.output.type if (response_format := _first_param("response_format")) is not None: @@ -686,8 +685,7 @@ def token_counts(outputs: Mapping[str, Any] | None) -> Iterator[tuple[str, int]] if (output_tokens := usage.get("output_tokens")) is not None: yield GEN_AI_USAGE_OUTPUT_TOKENS_KEY, output_tokens if usage_mapping := _as_usage_mapping(_parse_token_usage(outputs)): - for attribute_name, token_count in _extra_usage_attributes(usage_mapping): - yield attribute_name, token_count + yield from _extra_usage_attributes(usage_mapping) def _iter_generation_mappings(outputs: Mapping[str, Any] | None) -> Iterator[Mapping[str, Any]]: @@ -880,9 +878,8 @@ def tools(run: Run) -> Iterator[tuple[str, str]]: yield GEN_AI_TOOL_NAME_KEY, name if description := serialized.get("description"): yield GEN_AI_TOOL_DESCRIPTION_KEY, description - if run.extra and hasattr(run.extra, "get"): - if tool_call_id := run.extra.get("tool_call_id"): - yield GEN_AI_TOOL_CALL_ID_KEY, tool_call_id + if run.extra and hasattr(run.extra, "get") and (tool_call_id := run.extra.get("tool_call_id")): + yield GEN_AI_TOOL_CALL_ID_KEY, tool_call_id if _should_capture_content_on_spans(): if run.inputs and hasattr(run.inputs, "get"): _sentinel = object() @@ -1039,14 +1036,14 @@ def _first_param(*keys: str) -> Any: "endpoint_url", "service_url", ): - if (addr := _first_param(key)) is not None: + if (addr := _first_param(key)) is not None: # noqa: SIM102 if normalized_addr := _normalize_server_address(addr): inv.server_address = normalized_addr break if not inv.server_address and isinstance(meta := run.extra.get("metadata"), Mapping): for key in ("ls_server_address", "server.address"): - if (addr := meta.get(key)) is not None: + if (addr := meta.get(key)) is not None: # noqa: SIM102 if normalized_addr := _normalize_server_address(addr): inv.server_address = normalized_addr break @@ -1076,9 +1073,8 @@ def _first_param(*keys: str) -> Any: # --- Response ID --- if run.outputs and isinstance(run.outputs, Mapping): llm_output = run.outputs.get("llm_output") - if llm_output and hasattr(llm_output, "get"): - if resp_id := llm_output.get("id"): - inv.response_id = str(resp_id) + if llm_output and hasattr(llm_output, "get") and (resp_id := llm_output.get("id")): + inv.response_id = str(resp_id) # ``llm_output`` is only populated for the OpenAI client path patched by # ``opentelemetry-instrumentation-openai-v2``. ``AzureChatOpenAI`` and @@ -1087,12 +1083,11 @@ def _first_param(*keys: str) -> Any: # ``response_metadata`` (or ``generation_info``). Fall back to those. if not inv.response_model_name or not inv.response_id: for meta in _iter_generation_response_metadata(run.outputs): - if not inv.response_model_name: + if not inv.response_model_name: # noqa: SIM102 if resp_model := get_first_value(meta, ("model_name", "model")): inv.response_model_name = str(resp_model) - if not inv.response_id: - if resp_id := meta.get("id"): - inv.response_id = str(resp_id) + if not inv.response_id and (resp_id := meta.get("id")): + inv.response_id = str(resp_id) if inv.response_model_name and inv.response_id: break @@ -1120,17 +1115,16 @@ def _langchain_role(message: Any) -> str: if msg_type and msg_type != "constructor": return "assistant" if msg_type == "ai" else str(msg_type) # Fallback: parse role from serialized id field (e.g. ["langchain", "schema", "HumanMessage"]) - if id_field := message.get("id"): - if isinstance(id_field, list) and len(id_field) > 0: - type_name = id_field[-1] - if "Human" in type_name: - return "user" - if "AI" in type_name or "Assistant" in type_name: - return "assistant" - if "System" in type_name: - return "system" - if "Tool" in type_name: - return "tool" + if (id_field := message.get("id")) and isinstance(id_field, list) and len(id_field) > 0: + type_name = id_field[-1] + if "Human" in type_name: + return "user" + if "AI" in type_name or "Assistant" in type_name: + return "assistant" + if "System" in type_name: + return "system" + if "Tool" in type_name: + return "tool" return "unknown" @@ -1142,9 +1136,8 @@ def _langchain_content(message: Any) -> str | None: if hasattr(message, "get"): if c := message.get("content"): return str(c) - if kwargs := message.get("kwargs"): - if hasattr(kwargs, "get") and (c := kwargs.get("content")): - return str(c) + if (kwargs := message.get("kwargs")) and hasattr(kwargs, "get") and (c := kwargs.get("content")): + return str(c) return None @@ -1480,7 +1473,6 @@ def _output_message_to_input(out_msg: OutputMessage) -> InputMessage: return InputMessage(role=out_msg.role, parts=list(out_msg.parts)) - @stop_on_exception def invoke_agent_input_message( inputs: Mapping[str, Any] | None, @@ -1550,10 +1542,9 @@ def extract_agent_metadata(run: Run) -> Iterator[tuple[str, str]]: yield GEN_AI_AGENT_DESCRIPTION_KEY, desc return # From serialized graph - if run.serialized and isinstance(run.serialized, dict): - if name := run.serialized.get("name"): - if name != "LangGraph": - yield GEN_AI_AGENT_NAME_KEY, name + if run.serialized and isinstance(run.serialized, dict) and (name := run.serialized.get("name")): # noqa: SIM102 + if name != "LangGraph": + yield GEN_AI_AGENT_NAME_KEY, name @stop_on_exception diff --git a/src/microsoft/opentelemetry/_genai/_openai_agents/_message_mapper.py b/src/microsoft/opentelemetry/_genai/_openai_agents/_message_mapper.py index 4b1589ce..671ca481 100644 --- a/src/microsoft/opentelemetry/_genai/_openai_agents/_message_mapper.py +++ b/src/microsoft/opentelemetry/_genai/_openai_agents/_message_mapper.py @@ -197,15 +197,12 @@ def _map_chat_completions_message(msg: dict[str, Any]) -> ChatMessage | None: parts.append(TextPart(content=content)) elif isinstance(content, list): for item in content: - if isinstance(item, dict): - if item.get("type") in ("input_text", "text"): - text = item.get("text", "") - if text: - parts.append(TextPart(content=text)) - elif item.get("type") == "output_text": - text = item.get("text", "") - if text: - parts.append(TextPart(content=text)) + if isinstance(item, dict) and ( + item.get("type") in ("input_text", "text") or item.get("type") == "output_text" + ): + text = item.get("text", "") + if text: + parts.append(TextPart(content=text)) # Tool calls tool_calls = msg.get("tool_calls") diff --git a/src/microsoft/opentelemetry/_genai/_openai_agents/_trace_processor.py b/src/microsoft/opentelemetry/_genai/_openai_agents/_trace_processor.py index 85bee986..cfda9e41 100644 --- a/src/microsoft/opentelemetry/_genai/_openai_agents/_trace_processor.py +++ b/src/microsoft/opentelemetry/_genai/_openai_agents/_trace_processor.py @@ -68,6 +68,7 @@ get_span_status, get_tool_call_id, ) +import contextlib logger = logging.getLogger(__name__) @@ -267,10 +268,8 @@ def on_span_end(self, span: Span[Any]) -> None: # pylint: disable=too-many-stat end_time: int | None = None if span.ended_at: - try: + with contextlib.suppress(ValueError): end_time = as_utc_nano(datetime.fromisoformat(span.ended_at)) - except ValueError: - pass otel_span.set_status(status=get_span_status(span)) otel_span.end(end_time) diff --git a/src/microsoft/opentelemetry/_genai/_openai_agents/_utils.py b/src/microsoft/opentelemetry/_genai/_openai_agents/_utils.py index e2bbeab1..ec56403f 100644 --- a/src/microsoft/opentelemetry/_genai/_openai_agents/_utils.py +++ b/src/microsoft/opentelemetry/_genai/_openai_agents/_utils.py @@ -6,6 +6,7 @@ # HELPER FUNCTIONS ### # -------------------------------------------------- # +import contextlib from collections.abc import Iterable, Iterator, Mapping from typing import TYPE_CHECKING, Any @@ -231,10 +232,8 @@ def _get_attributes_from_chat_completions_input( ) -> Iterator[tuple[str, AttributeValue]]: if not obj: return - try: + with contextlib.suppress(Exception): # pylint: disable=broad-exception-caught yield GEN_AI_INPUT_MESSAGES_KEY, safe_json_dumps(obj) - except Exception: # pylint: disable=broad-exception-caught - pass yield from get_attributes_from_chat_completions_message_dicts( obj, f"{GEN_AI_INPUT_MESSAGES_KEY}.", @@ -246,10 +245,8 @@ def _get_attributes_from_chat_completions_output( ) -> Iterator[tuple[str, AttributeValue]]: if not obj: return - try: + with contextlib.suppress(Exception): # pylint: disable=broad-exception-caught yield GEN_AI_OUTPUT_MESSAGES_KEY, safe_json_dumps(obj) - except Exception: # pylint: disable=broad-exception-caught - pass # Collect all finish_reason values finish_reasons = [str(message.get("finish_reason")) for message in obj if message.get("finish_reason") is not None] @@ -323,9 +320,8 @@ def _get_attributes_from_chat_completions_tool_call_dict( if function := obj.get("function"): if name := function.get("name"): yield f"{prefix}{GEN_AI_TOOL_NAME_KEY}", name - if arguments := function.get("arguments"): - if arguments != "{}": - yield f"{prefix}{GEN_AI_TOOL_ARGS_KEY}", arguments + if (arguments := function.get("arguments")) and arguments != "{}": + yield f"{prefix}{GEN_AI_TOOL_ARGS_KEY}", arguments def _get_attributes_from_chat_completions_usage( diff --git a/src/microsoft/opentelemetry/_instrumentation.py b/src/microsoft/opentelemetry/_instrumentation.py index 54e3cb5d..17704d83 100644 --- a/src/microsoft/opentelemetry/_instrumentation.py +++ b/src/microsoft/opentelemetry/_instrumentation.py @@ -15,7 +15,7 @@ from __future__ import annotations from logging import getLogger -from typing import Collection +from collections.abc import Collection from packaging.requirements import InvalidRequirement, Requirement diff --git a/src/microsoft/opentelemetry/_sdkstats/_constants.py b/src/microsoft/opentelemetry/_sdkstats/_constants.py index 04df7813..2251bead 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_constants.py +++ b/src/microsoft/opentelemetry/_sdkstats/_constants.py @@ -5,7 +5,6 @@ # -------------------------------------------------------------------------- - from azure.monitor.opentelemetry.exporter._constants import ( # type: ignore[import-not-found] _REQ_SUCCESS_NAME, ) diff --git a/src/microsoft/opentelemetry/_sdkstats/_manager.py b/src/microsoft/opentelemetry/_sdkstats/_manager.py index 5475b6df..bcdcb895 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_manager.py +++ b/src/microsoft/opentelemetry/_sdkstats/_manager.py @@ -19,6 +19,7 @@ pipeline so usage/feature metrics are still collected. """ +import contextlib import logging import threading from typing import Optional @@ -196,10 +197,8 @@ def _do_initialize_with_reader( def _cleanup(self) -> None: if self._meter_provider: - try: + with contextlib.suppress(Exception): # pylint: disable=broad-exception-caught self._meter_provider.shutdown() - except Exception: # pylint: disable=broad-exception-caught - pass self._meter_provider = None self._metrics = None self._initialized = False diff --git a/src/microsoft/opentelemetry/_sdkstats/_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_metrics.py index c3552a54..e547d3b5 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_metrics.py @@ -13,7 +13,8 @@ from enum import Enum import platform -from typing import Any, Dict, Iterable, List +from typing import Any +from collections.abc import Iterable from opentelemetry.metrics import CallbackOptions, Observation from opentelemetry.sdk.metrics import MeterProvider @@ -63,7 +64,7 @@ def __init__( self._meter = meter_provider.get_meter("microsoft.opentelemetry.sdkstats") self._distro_version = distro_version or VERSION - self._common_attributes: Dict[str, Any] = { + self._common_attributes: dict[str, Any] = { "rp": _RP_Names.UNKNOWN.value, "attach": _AttachTypes.MANUAL.value, "cikey": None, @@ -106,7 +107,7 @@ def __init__( # ---- callbacks ---- def _observe_features(self, options: CallbackOptions) -> Iterable[Observation]: - observations: List[Observation] = [] + observations: list[Observation] = [] feature_bits = get_sdkstats_feature_flags() if feature_bits != 0: attrs = dict(self._common_attributes) @@ -116,7 +117,7 @@ def _observe_features(self, options: CallbackOptions) -> Iterable[Observation]: return observations def _observe_instrumentations(self, options: CallbackOptions) -> Iterable[Observation]: - observations: List[Observation] = [] + observations: list[Observation] = [] instr_bits = get_sdkstats_instrumentation_flags() if instr_bits != 0: attrs = dict(self._common_attributes) @@ -126,7 +127,7 @@ def _observe_instrumentations(self, options: CallbackOptions) -> Iterable[Observ return observations def _observe_request_success_count(self, options: CallbackOptions) -> Iterable[Observation]: - observations: List[Observation] = [] + observations: list[Observation] = [] for key, value in drain(REQUEST_SUCCESS_NAME).items(): attrs = dict(self._common_attributes) attrs["endpoint"] = key[0] diff --git a/src/microsoft/opentelemetry/_sdkstats/_utils.py b/src/microsoft/opentelemetry/_sdkstats/_utils.py index c0b15223..6fa1c366 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_utils.py +++ b/src/microsoft/opentelemetry/_sdkstats/_utils.py @@ -9,7 +9,6 @@ from __future__ import annotations import threading -from typing import Dict, Tuple from microsoft.opentelemetry._sdkstats._constants import REQUEST_SUCCESS_NAME @@ -53,13 +52,13 @@ def update_global_state_instrumentation_bits(instrumentation_bits: int) -> None: _REQUESTS_MAP_LOCK = threading.Lock() -_REQUESTS_MAP: Dict[str, Dict[Tuple[str, ...], float]] = { +_REQUESTS_MAP: dict[str, dict[tuple[str, ...], float]] = { REQUEST_SUCCESS_NAME: {}, } # Increment the counter for the given metric/key by the given value (default 1.0). -def _bump(metric: str, key: Tuple[str, ...], value: float = 1.0) -> None: +def _bump(metric: str, key: tuple[str, ...], value: float = 1.0) -> None: with _REQUESTS_MAP_LOCK: bucket = _REQUESTS_MAP[metric] bucket[key] = bucket.get(key, 0) + value @@ -70,7 +69,7 @@ def record_success(endpoint: str, host: str) -> None: # Returns the counts accumulated since the last call, and resets the counters to zero. -def drain(metric: str) -> Dict[Tuple[str, ...], float]: +def drain(metric: str) -> dict[tuple[str, ...], float]: with _REQUESTS_MAP_LOCK: bucket = _REQUESTS_MAP[metric] snapshot = dict(bucket) diff --git a/src/microsoft/opentelemetry/_types.py b/src/microsoft/opentelemetry/_types.py index 2a08c1d8..6f282b0e 100644 --- a/src/microsoft/opentelemetry/_types.py +++ b/src/microsoft/opentelemetry/_types.py @@ -4,7 +4,8 @@ # license information. # -------------------------------------------------------------------------- -from typing import Any, Sequence, Union +from typing import Any, Union +from collections.abc import Sequence ConfigurationValue = Union[ str, diff --git a/src/microsoft/opentelemetry/_utils.py b/src/microsoft/opentelemetry/_utils.py index 095d1b5e..234a6faf 100644 --- a/src/microsoft/opentelemetry/_utils.py +++ b/src/microsoft/opentelemetry/_utils.py @@ -6,7 +6,7 @@ from importlib.util import find_spec from logging import getLogger -from typing import Any, Dict +from typing import Any from microsoft.opentelemetry._constants import ( DISABLE_LOGGING_ARG, @@ -29,7 +29,7 @@ # --------------------------------------------------------------------------- -def _append_otlp_components(otel_kwargs: Dict[str, Any]) -> None: +def _append_otlp_components(otel_kwargs: dict[str, Any]) -> None: """Append OTLP processors/readers to otel_kwargs when OTLP is enabled. Respects per-signal disable flags so that disabled pipelines do not @@ -59,7 +59,7 @@ def _append_otlp_components(otel_kwargs: Dict[str, Any]) -> None: # --------------------------------------------------------------------------- -def _append_console_components(otel_kwargs: Dict[str, Any], enable_console: bool) -> None: +def _append_console_components(otel_kwargs: dict[str, Any], enable_console: bool) -> None: """Append console exporters to otel_kwargs when console export is enabled. Console export is enabled when ``enable_console=True`` is passed as a @@ -95,8 +95,8 @@ def _append_console_components(otel_kwargs: Dict[str, Any], enable_console: bool def _append_azure_monitor_components( - otel_kwargs: Dict[str, Any], - azure_monitor_kwargs: Dict[str, Any], + otel_kwargs: dict[str, Any], + azure_monitor_kwargs: dict[str, Any], ) -> tuple: """Call Azure Monitor _setup_* functions which build fully-configured providers. @@ -154,13 +154,9 @@ def _append_azure_monitor_components( return None, None, None -def _disable_openai_v2_instrumentation(otel_kwargs: Dict[str, Any]) -> None: +def _disable_openai_v2_instrumentation(otel_kwargs: dict[str, Any]) -> None: options = otel_kwargs.get(INSTRUMENTATION_OPTIONS_ARG) - if ( - isinstance(options, dict) - and isinstance(options.get("openai"), dict) - and "enabled" in options["openai"] - ): + if isinstance(options, dict) and isinstance(options.get("openai"), dict) and "enabled" in options["openai"]: return # User has explicitly set openai instrumentation options; do not override overlapping_present = any( diff --git a/src/microsoft/opentelemetry/a365/core/apply_guardrail_scope.py b/src/microsoft/opentelemetry/a365/core/apply_guardrail_scope.py index 4e06cad0..81cb983a 100644 --- a/src/microsoft/opentelemetry/a365/core/apply_guardrail_scope.py +++ b/src/microsoft/opentelemetry/a365/core/apply_guardrail_scope.py @@ -90,7 +90,7 @@ def start( request: Request | None = None, user_details: UserDetails | None = None, span_details: SpanDetails | None = None, - ) -> "ApplyGuardrailScope": + ) -> ApplyGuardrailScope: # noqa: UP037 """Create and start a new scope for guardrail evaluation tracing. Args: diff --git a/src/microsoft/opentelemetry/a365/core/exporters/agent365_exporter.py b/src/microsoft/opentelemetry/a365/core/exporters/agent365_exporter.py index 6a633fef..0dce9e02 100644 --- a/src/microsoft/opentelemetry/a365/core/exporters/agent365_exporter.py +++ b/src/microsoft/opentelemetry/a365/core/exporters/agent365_exporter.py @@ -39,6 +39,7 @@ truncate_span, ) from microsoft.opentelemetry.a365.constants import A365_HTTP_TIMEOUT_SECONDS +import contextlib # mypy: disable-error-code="import-untyped, union-attr" @@ -324,10 +325,8 @@ def shutdown(self) -> None: if self._closed: return self._closed = True - try: + with contextlib.suppress(Exception): self._session.close() - except Exception: - pass def force_flush(self, timeout_millis: int = 30000) -> bool: return True diff --git a/src/microsoft/opentelemetry/a365/core/exporters/enriched_span.py b/src/microsoft/opentelemetry/a365/core/exporters/enriched_span.py index f6d960e4..9cc2ff5c 100644 --- a/src/microsoft/opentelemetry/a365/core/exporters/enriched_span.py +++ b/src/microsoft/opentelemetry/a365/core/exporters/enriched_span.py @@ -13,7 +13,7 @@ import json from datetime import datetime, timezone -from typing import Any, Optional, Set +from typing import Any, Optional from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.util import types @@ -34,7 +34,7 @@ def __init__( self, span: ReadableSpan, extra_attributes: dict[str, Any], - excluded_attribute_keys: Optional[Set[str]] = None, + excluded_attribute_keys: Optional[set[str]] = None, ): self._span = span self._extra_attributes = extra_attributes diff --git a/src/microsoft/opentelemetry/a365/core/exporters/span_processor.py b/src/microsoft/opentelemetry/a365/core/exporters/span_processor.py index c855c37e..066c42df 100644 --- a/src/microsoft/opentelemetry/a365/core/exporters/span_processor.py +++ b/src/microsoft/opentelemetry/a365/core/exporters/span_processor.py @@ -57,6 +57,7 @@ USER_ID_KEY, USER_NAME_KEY, ) +import contextlib # mypy: disable-error-code="no-untyped-def" @@ -130,15 +131,11 @@ def on_start(self, span, parent_context=None): # type: ignore[override] existing = {} if self._tenant_id and TENANT_ID_KEY not in existing: - try: + with contextlib.suppress(Exception): span.set_attribute(TENANT_ID_KEY, self._tenant_id) - except Exception: - pass if self._agent_id and GEN_AI_AGENT_ID_KEY not in existing: - try: + with contextlib.suppress(Exception): span.set_attribute(GEN_AI_AGENT_ID_KEY, self._agent_id) - except Exception: - pass # Refresh existing after stamping identity try: @@ -156,9 +153,11 @@ def on_start(self, span, parent_context=None): # type: ignore[override] operation_name = existing.get(GEN_AI_OPERATION_NAME_KEY) is_invoke_agent = False - if operation_name == INVOKE_AGENT_OPERATION_NAME: - is_invoke_agent = True - elif isinstance(getattr(span, "name", None), str) and span.name.startswith(INVOKE_AGENT_OPERATION_NAME): + if ( + operation_name == INVOKE_AGENT_OPERATION_NAME + or isinstance(getattr(span, "name", None), str) + and span.name.startswith(INVOKE_AGENT_OPERATION_NAME) + ): is_invoke_agent = True target_keys = list(COMMON_ATTRIBUTES) diff --git a/src/microsoft/opentelemetry/a365/core/exporters/utils.py b/src/microsoft/opentelemetry/a365/core/exporters/utils.py index eeb295f0..379a25e8 100644 --- a/src/microsoft/opentelemetry/a365/core/exporters/utils.py +++ b/src/microsoft/opentelemetry/a365/core/exporters/utils.py @@ -18,7 +18,7 @@ import time from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass, field -from typing import Any, List, Optional, TypeVar +from typing import Any, Optional, TypeVar from urllib.parse import urlparse from opentelemetry.sdk.trace import ReadableSpan @@ -600,7 +600,7 @@ def _create_default_token_resolver( class A365Handlers: """Processors created for Agent365 export, mirroring ``OtlpHandlers``.""" - span_processors: List[SpanProcessor] = field(default_factory=list) + span_processors: list[SpanProcessor] = field(default_factory=list) def is_a365_enabled(enable_a365: bool = False) -> bool: diff --git a/src/microsoft/opentelemetry/a365/core/inference_scope.py b/src/microsoft/opentelemetry/a365/core/inference_scope.py index 92e70272..ee90f024 100644 --- a/src/microsoft/opentelemetry/a365/core/inference_scope.py +++ b/src/microsoft/opentelemetry/a365/core/inference_scope.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from typing import List from opentelemetry.trace import SpanKind @@ -184,7 +183,7 @@ def record_output_tokens(self, output_tokens: int) -> None: """ self.set_tag_maybe(GEN_AI_USAGE_OUTPUT_TOKENS_KEY, output_tokens) - def record_finish_reasons(self, finish_reasons: List[str]) -> None: + def record_finish_reasons(self, finish_reasons: list[str]) -> None: """Records the finish reasons for telemetry tracking. Args: diff --git a/src/microsoft/opentelemetry/a365/core/message_utils.py b/src/microsoft/opentelemetry/a365/core/message_utils.py index b2d762fa..21f86565 100644 --- a/src/microsoft/opentelemetry/a365/core/message_utils.py +++ b/src/microsoft/opentelemetry/a365/core/message_utils.py @@ -117,9 +117,7 @@ def serialize_messages( message parts contain non-JSON-serializable values. """ try: - serialized_list = [ - asdict(msg, dict_factory=_message_dict_factory) for msg in wrapper.messages - ] + serialized_list = [asdict(msg, dict_factory=_message_dict_factory) for msg in wrapper.messages] return json.dumps( serialized_list, default=str, diff --git a/src/microsoft/opentelemetry/a365/core/middleware/baggage_builder.py b/src/microsoft/opentelemetry/a365/core/middleware/baggage_builder.py index 2b96c586..4a6c49e1 100644 --- a/src/microsoft/opentelemetry/a365/core/middleware/baggage_builder.py +++ b/src/microsoft/opentelemetry/a365/core/middleware/baggage_builder.py @@ -228,10 +228,7 @@ def set_pairs(self, pairs: Any) -> "BaggageBuilder": """ if not pairs: return self - if isinstance(pairs, dict): - iterator = pairs.items() - else: - iterator = pairs + iterator = pairs.items() if isinstance(pairs, dict) else pairs for k, v in iterator: if v is None: continue diff --git a/src/microsoft/opentelemetry/a365/runtime/operation_result.py b/src/microsoft/opentelemetry/a365/runtime/operation_result.py index b9d3d7ee..ef3a14e7 100644 --- a/src/microsoft/opentelemetry/a365/runtime/operation_result.py +++ b/src/microsoft/opentelemetry/a365/runtime/operation_result.py @@ -5,7 +5,7 @@ Represents the result of an operation. """ -from typing import List, Optional +from typing import Optional from microsoft.opentelemetry.a365.runtime.operation_error import OperationError @@ -20,7 +20,7 @@ class OperationResult: _success_instance: Optional["OperationResult"] = None - def __init__(self, succeeded: bool, errors: Optional[List[OperationError]] = None): + def __init__(self, succeeded: bool, errors: Optional[list[OperationError]] = None): """ Initialize a new instance of the OperationResult class. @@ -42,7 +42,7 @@ def succeeded(self) -> bool: return self._succeeded @property - def errors(self) -> List[OperationError]: + def errors(self) -> list[OperationError]: """ Get the list of errors that occurred during the operation. @@ -52,7 +52,7 @@ def errors(self) -> List[OperationError]: protecting the singleton instance returned by success(). Returns: - List[OperationError]: A copy of the list of operation errors. + list[OperationError]: A copy of the list of operation errors. """ return list(self._errors) diff --git a/tests/a365/integration/agentframework/test_agentframework_trace_processor.py b/tests/a365/integration/agentframework/test_agentframework_trace_processor.py index 15d79c4c..e0cbe8b0 100644 --- a/tests/a365/integration/agentframework/test_agentframework_trace_processor.py +++ b/tests/a365/integration/agentframework/test_agentframework_trace_processor.py @@ -43,9 +43,7 @@ def add_numbers(a: float, b: float) -> float: class TestAgentFrameworkTraceProcessorIntegration: """Integration tests for AgentFramework trace processor with real Azure OpenAI.""" - def test_agentframework_trace_processor_integration( - self, distro_exporter, azure_openai_config, agent365_config - ): + def test_agentframework_trace_processor_integration(self, distro_exporter, azure_openai_config, agent365_config): """Test AgentFramework trace processor with real Azure OpenAI call.""" # Create Azure OpenAI ChatClient @@ -152,24 +150,24 @@ def _validate_span_attributes(self, distro_exporter, agent365_config): if ( GEN_AI_PROVIDER_NAME_KEY in attributes and attributes[GEN_AI_PROVIDER_NAME_KEY] == "openai" + and GEN_AI_REQUEST_MODEL_KEY in attributes ): - if GEN_AI_REQUEST_MODEL_KEY in attributes: - llm_spans_found += 1 - # Validate LLM span attributes - assert GEN_AI_REQUEST_MODEL_KEY in attributes - assert attributes[GEN_AI_REQUEST_MODEL_KEY] is not None - print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") - - # Check for input/output messages - if GEN_AI_INPUT_MESSAGES_KEY in attributes: - input_messages = attributes[GEN_AI_INPUT_MESSAGES_KEY] - assert input_messages is not None - print(f"✓ Input messages found: {input_messages[:100]}...") - - if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: - output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] - assert output_messages is not None - print(f"✓ Output messages found: {output_messages[:100]}...") + llm_spans_found += 1 + # Validate LLM span attributes + assert GEN_AI_REQUEST_MODEL_KEY in attributes + assert attributes[GEN_AI_REQUEST_MODEL_KEY] is not None + print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") + + # Check for input/output messages + if GEN_AI_INPUT_MESSAGES_KEY in attributes: + input_messages = attributes[GEN_AI_INPUT_MESSAGES_KEY] + assert input_messages is not None + print(f"✓ Input messages found: {input_messages[:100]}...") + + if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: + output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] + assert output_messages is not None + print(f"✓ Output messages found: {output_messages[:100]}...") # Check for agent spans if "agent" in span.name.lower(): @@ -196,16 +194,15 @@ def _validate_tool_span_attributes(self, distro_exporter, agent365_config): assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"] # Check for LLM spans - if "chat" in span.name.lower(): - if GEN_AI_REQUEST_MODEL_KEY in attributes: - llm_spans_found += 1 - print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") - - # Check for tool calls in messages - if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: - output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] - if "tool_calls" in output_messages: - print("✓ Found tool calls in LLM output messages") + if "chat" in span.name.lower() and GEN_AI_REQUEST_MODEL_KEY in attributes: # noqa: SIM102 + llm_spans_found += 1 + print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") + + # Check for tool calls in messages + if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: + output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] + if "tool_calls" in output_messages: + print("✓ Found tool calls in LLM output messages") # Check for agent spans if "agent" in span.name.lower(): diff --git a/tests/a365/integration/agentframework/test_message_format.py b/tests/a365/integration/agentframework/test_message_format.py index 0272a051..8a27f49f 100644 --- a/tests/a365/integration/agentframework/test_message_format.py +++ b/tests/a365/integration/agentframework/test_message_format.py @@ -69,16 +69,10 @@ def _find_chat_spans(self, distro_exporter) -> list[ReadableSpan]: """ get_tracer_provider().force_flush() time.sleep(0.5) - return [ - s - for s in distro_exporter.spans - if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes - ] + return [s for s in distro_exporter.spans if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes] @pytest.mark.asyncio - async def test_simple_chat_message_mapping( - self, distro_exporter, chat_client: OpenAIChatClient - ) -> None: + async def test_simple_chat_message_mapping(self, distro_exporter, chat_client: OpenAIChatClient) -> None: """Simple chat: verify exported spans contain structured A365 messages after enrichment (no manual mapper call).""" agent = RawAgent( @@ -92,9 +86,7 @@ async def test_simple_chat_message_mapping( assert len(result.text) > 0 chat_spans = self._find_chat_spans(distro_exporter) - assert len(chat_spans) > 0, ( - f"No chat spans found. All spans: {[s.name for s in distro_exporter.spans]}" - ) + assert len(chat_spans) > 0, f"No chat spans found. All spans: {[s.name for s in distro_exporter.spans]}" attrs = dict(chat_spans[-1].attributes or {}) @@ -132,9 +124,7 @@ async def test_simple_chat_message_mapping( print(f"\n=== Enriched output ===\n{json.dumps(output_data, indent=2)}") @pytest.mark.asyncio - async def test_tool_call_message_mapping( - self, distro_exporter, chat_client: OpenAIChatClient - ) -> None: + async def test_tool_call_message_mapping(self, distro_exporter, chat_client: OpenAIChatClient) -> None: """Tool-calling chat: verify tool_call and tool_call_response parts survive enrichment in exported spans.""" agent = RawAgent( @@ -170,7 +160,5 @@ async def test_tool_call_message_mapping( part_types.add(part.get("type", "")) assert "tool_call" in part_types, f"Expected tool_call in exported parts: {part_types}" - assert "tool_call_response" in part_types, ( - f"Expected tool_call_response in exported parts: {part_types}" - ) + assert "tool_call_response" in part_types, f"Expected tool_call_response in exported parts: {part_types}" print(f"\n Exported part types: {part_types}") diff --git a/tests/a365/integration/agentframework/test_observability_pipeline.py b/tests/a365/integration/agentframework/test_observability_pipeline.py index 07cbeff6..cfd9dfb2 100644 --- a/tests/a365/integration/agentframework/test_observability_pipeline.py +++ b/tests/a365/integration/agentframework/test_observability_pipeline.py @@ -170,16 +170,13 @@ async def test_pipeline_invoke_agent_with_tool_call( # pylint: disable=too-many # --- 1. All spans share the same trace_id --- invoke_spans = _find_spans_by_name_prefix(spans, "invoke_agent") - assert len(invoke_spans) >= 1, ( - f"Expected at least 1 invoke_agent span, got: {[s.name for s in spans]}" - ) + assert len(invoke_spans) >= 1, f"Expected at least 1 invoke_agent span, got: {[s.name for s in spans]}" invoke_span = invoke_spans[0] trace_id = invoke_span.context.trace_id for s in spans: assert s.context.trace_id == trace_id, ( - f"Span '{s.name}' has different trace_id: " - f"{s.context.trace_id:032x} vs {trace_id:032x}" + f"Span '{s.name}' has different trace_id: {s.context.trace_id:032x} vs {trace_id:032x}" ) # --- 2. invoke_agent span is the root (no parent) --- @@ -200,15 +197,11 @@ async def test_pipeline_invoke_agent_with_tool_call( # pylint: disable=too-many if _get_span_attr(s, GEN_AI_OPERATION_NAME_KEY) == "chat" or (s.name.startswith("chat") and _get_span_attr(s, GEN_AI_REQUEST_MODEL_KEY)) ] - assert len(chat_spans) >= 1, ( - f"Expected at least 1 chat span, got: {[s.name for s in spans]}" - ) + assert len(chat_spans) >= 1, f"Expected at least 1 chat span, got: {[s.name for s in spans]}" invoke_span_id = invoke_span.context.span_id for chat_span in chat_spans: - assert chat_span.parent is not None, ( - f"Chat span '{chat_span.name}' should have a parent" - ) + assert chat_span.parent is not None, f"Chat span '{chat_span.name}' should have a parent" # Chat span should be a child of invoke_agent (directly or transitively) self._assert_ancestor( chat_span, @@ -223,13 +216,9 @@ async def test_pipeline_invoke_agent_with_tool_call( # pylint: disable=too-many # Also check by operation name tool_spans = _find_spans_by_operation(spans, EXECUTE_TOOL_OPERATION_NAME) - assert len(tool_spans) >= 1, ( - f"Expected at least 1 execute_tool span. All spans: {[s.name for s in spans]}" - ) + assert len(tool_spans) >= 1, f"Expected at least 1 execute_tool span. All spans: {[s.name for s in spans]}" for tool_span in tool_spans: - assert tool_span.parent is not None, ( - f"Tool span '{tool_span.name}' should have a parent" - ) + assert tool_span.parent is not None, f"Tool span '{tool_span.name}' should have a parent" self._assert_ancestor( tool_span, invoke_span_id, diff --git a/tests/a365/integration/langchain/test_message_format.py b/tests/a365/integration/langchain/test_message_format.py index 06f5f542..6dd1d693 100644 --- a/tests/a365/integration/langchain/test_message_format.py +++ b/tests/a365/integration/langchain/test_message_format.py @@ -59,11 +59,7 @@ def _find_chat_spans(distro_exporter: SpanCapturingExporter) -> list[ReadableSpa """Find exported spans that have gen_ai.input.messages.""" get_tracer_provider().force_flush() time.sleep(0.5) - return [ - s - for s in distro_exporter.spans - if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes - ] + return [s for s in distro_exporter.spans if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes] @pytest.mark.asyncio async def test_simple_chat_message_mapping( @@ -82,9 +78,7 @@ async def test_simple_chat_message_mapping( assert len(result.content) > 0 chat_spans = self._find_chat_spans(distro_exporter) - assert len(chat_spans) > 0, ( - f"No chat spans found. All spans: {[s.name for s in distro_exporter.spans]}" - ) + assert len(chat_spans) > 0, f"No chat spans found. All spans: {[s.name for s in distro_exporter.spans]}" print(f"\n=== All exported spans ({len(distro_exporter.spans)}) ===") for s in distro_exporter.spans: @@ -123,9 +117,7 @@ async def test_simple_chat_message_mapping( for part in item.get("parts", []): if isinstance(part, dict) and "content" in part: flat_text += part["content"].lower() - assert "capital" in flat_text, ( - f"Expected 'capital' in input messages content, got: {input_data}" - ) + assert "capital" in flat_text, f"Expected 'capital' in input messages content, got: {input_data}" print("\n → List format (pre-mapper)") # --- Output messages --- diff --git a/tests/a365/integration/langchain/test_observability_pipeline.py b/tests/a365/integration/langchain/test_observability_pipeline.py index 7bed0531..7b930916 100644 --- a/tests/a365/integration/langchain/test_observability_pipeline.py +++ b/tests/a365/integration/langchain/test_observability_pipeline.py @@ -197,17 +197,14 @@ async def test_pipeline_invoke_agent_with_tool_call( # --- 1. Find invoke_agent span --- invoke_spans = _find_spans_by_name_prefix(spans, "invoke_agent") - assert len(invoke_spans) >= 1, ( - f"Expected at least 1 invoke_agent span, got: {[s.name for s in spans]}" - ) + assert len(invoke_spans) >= 1, f"Expected at least 1 invoke_agent span, got: {[s.name for s in spans]}" invoke_span = invoke_spans[0] trace_id = invoke_span.context.trace_id # --- 2. All spans share the same trace_id --- for s in spans: assert s.context.trace_id == trace_id, ( - f"Span '{s.name}' has different trace_id: " - f"{s.context.trace_id:032x} vs {trace_id:032x}" + f"Span '{s.name}' has different trace_id: {s.context.trace_id:032x} vs {trace_id:032x}" ) # --- 3. invoke_agent span is the root --- @@ -233,9 +230,7 @@ async def test_pipeline_invoke_agent_with_tool_call( ) and not s.name.startswith("execute_tool") ] - assert len(inference_spans) >= 1, ( - f"Expected at least 1 inference span, got: {[s.name for s in spans]}" - ) + assert len(inference_spans) >= 1, f"Expected at least 1 inference span, got: {[s.name for s in spans]}" invoke_span_id = invoke_span.context.span_id for inf_span in inference_spans: @@ -334,9 +329,7 @@ async def test_pipeline_invoke_agent_simple_inference( # Inference spans are descendants inference_spans = [ - s - for s in spans - if s != invoke_span and _get_span_attr(s, GEN_AI_INPUT_MESSAGES_KEY) is not None + s for s in spans if s != invoke_span and _get_span_attr(s, GEN_AI_INPUT_MESSAGES_KEY) is not None ] assert len(inference_spans) >= 1 diff --git a/tests/a365/integration/openai/test_message_format.py b/tests/a365/integration/openai/test_message_format.py index b7df828e..516185b2 100644 --- a/tests/a365/integration/openai/test_message_format.py +++ b/tests/a365/integration/openai/test_message_format.py @@ -65,16 +65,20 @@ def _span_to_json(span: ReadableSpan) -> dict[str, object]: events_list: list[dict[str, object]] = [] for e in getattr(span, "events", None) or []: - events_list.append({ - "name": e.name, - "attributes": dict(e.attributes) if e.attributes else {}, - }) + events_list.append( + { + "name": e.name, + "attributes": dict(e.attributes) if e.attributes else {}, + } + ) links_list: list[dict[str, object]] = [] for lnk in getattr(span, "links", None) or []: - links_list.append({ - "attributes": dict(lnk.attributes) if lnk.attributes else {}, - }) + links_list.append( + { + "attributes": dict(lnk.attributes) if lnk.attributes else {}, + } + ) result: dict[str, object] = { "name": span.name, @@ -116,11 +120,7 @@ def _find_message_spans(self, distro_exporter) -> list[ReadableSpan]: """Find exported spans that have gen_ai.input.messages.""" get_tracer_provider().force_flush() time.sleep(0.5) - return [ - s - for s in distro_exporter.spans - if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes - ] + return [s for s in distro_exporter.spans if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes] @pytest.mark.asyncio async def test_simple_chat_message_mapping( @@ -153,9 +153,7 @@ async def test_simple_chat_message_mapping( print(json.dumps(span_json, indent=2, default=str)) message_spans = self._find_message_spans(distro_exporter) - assert len(message_spans) > 0, ( - f"No message spans found. All spans: {[s.name for s in distro_exporter.spans]}" - ) + assert len(message_spans) > 0, f"No message spans found. All spans: {[s.name for s in distro_exporter.spans]}" # Verify at least one span has structured A365 array format found_structured = False diff --git a/tests/a365/integration/openai/test_openai_trace_processor.py b/tests/a365/integration/openai/test_openai_trace_processor.py index b2cdf039..c40c5d01 100644 --- a/tests/a365/integration/openai/test_openai_trace_processor.py +++ b/tests/a365/integration/openai/test_openai_trace_processor.py @@ -64,9 +64,7 @@ def test_openai_trace_processor_integration(self, distro_exporter, azure_openai_ agent = Agent( name="TestAgent", instructions="You are a helpful assistant.", - model=OpenAIChatCompletionsModel( - model=azure_openai_config["deployment"], openai_client=openai_client - ), + model=OpenAIChatCompletionsModel(model=azure_openai_config["deployment"], openai_client=openai_client), ) # Execute a simple prompt using async runner @@ -111,9 +109,7 @@ def test_openai_trace_processor_with_tool_calls(self, distro_exporter, azure_ope agent = Agent( name="MathAgent", instructions="You are a helpful math assistant. Use the add_numbers tool to perform calculations.", - model=OpenAIChatCompletionsModel( - model=azure_openai_config["deployment"], openai_client=openai_client - ), + model=OpenAIChatCompletionsModel(model=azure_openai_config["deployment"], openai_client=openai_client), tools=[add_numbers], ) @@ -162,9 +158,7 @@ def test_invoke_agent_span_required_attributes(self, distro_exporter, azure_open agent = Agent( name="TestAgent", instructions="You are a helpful assistant. Answer briefly.", - model=OpenAIChatCompletionsModel( - model=azure_openai_config["deployment"], openai_client=openai_client - ), + model=OpenAIChatCompletionsModel(model=azure_openai_config["deployment"], openai_client=openai_client), ) import asyncio @@ -195,10 +189,7 @@ async def run_agent(): # 1. The InvokeAgentScope span (parent) - has gen_ai.agent.id/name # 2. The instrumentor span (child) - has gen_ai.input/output.messages # Find the scope span (has gen_ai.agent.id) for attribute validation - invoke_agent_spans = [ - s for s in distro_exporter.spans - if s.name.startswith(INVOKE_AGENT_OPERATION_NAME) - ] + invoke_agent_spans = [s for s in distro_exporter.spans if s.name.startswith(INVOKE_AGENT_OPERATION_NAME)] assert len(invoke_agent_spans) >= 1, "No invoke_agent spans found" # The scope span is the one with gen_ai.agent.id set @@ -255,24 +246,24 @@ def _validate_span_attributes(self, distro_exporter, agent365_config): if ( GEN_AI_PROVIDER_NAME_KEY in attributes and attributes[GEN_AI_PROVIDER_NAME_KEY] == "openai" + and GEN_AI_REQUEST_MODEL_KEY in attributes ): - if GEN_AI_REQUEST_MODEL_KEY in attributes: - llm_spans_found += 1 - # Validate LLM span attributes - assert GEN_AI_REQUEST_MODEL_KEY in attributes - assert attributes[GEN_AI_REQUEST_MODEL_KEY] is not None - print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") - - # Check for input/output messages - if GEN_AI_INPUT_MESSAGES_KEY in attributes: - input_messages = attributes[GEN_AI_INPUT_MESSAGES_KEY] - assert input_messages is not None - print(f"✓ Input messages found: {input_messages[:100]}...") - - if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: - output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] - assert output_messages is not None - print(f"✓ Output messages found: {output_messages[:100]}...") + llm_spans_found += 1 + # Validate LLM span attributes + assert GEN_AI_REQUEST_MODEL_KEY in attributes + assert attributes[GEN_AI_REQUEST_MODEL_KEY] is not None + print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") + + # Check for input/output messages + if GEN_AI_INPUT_MESSAGES_KEY in attributes: + input_messages = attributes[GEN_AI_INPUT_MESSAGES_KEY] + assert input_messages is not None + print(f"✓ Input messages found: {input_messages[:100]}...") + + if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: + output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] + assert output_messages is not None + print(f"✓ Output messages found: {output_messages[:100]}...") # Check for agent spans if "agent" in span.name.lower(): @@ -305,16 +296,16 @@ def _validate_tool_span_attributes(self, distro_exporter, agent365_config): if ( GEN_AI_PROVIDER_NAME_KEY in attributes and attributes[GEN_AI_PROVIDER_NAME_KEY] == "openai" + and GEN_AI_REQUEST_MODEL_KEY in attributes ): - if GEN_AI_REQUEST_MODEL_KEY in attributes: - llm_spans_found += 1 - print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") - - # Check for tool calls in messages - if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: - output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] - if "tool_calls" in output_messages: - print("✓ Found tool calls in LLM output messages") + llm_spans_found += 1 + print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}") + + # Check for tool calls in messages + if GEN_AI_OUTPUT_MESSAGES_KEY in attributes: + output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY] + if "tool_calls" in output_messages: + print("✓ Found tool calls in LLM output messages") # Check for agent spans if "agent" in span.name.lower(): diff --git a/tests/a365/runtime/test_export_config_consistency.py b/tests/a365/runtime/test_export_config_consistency.py index 08f29c75..8648ed26 100644 --- a/tests/a365/runtime/test_export_config_consistency.py +++ b/tests/a365/runtime/test_export_config_consistency.py @@ -61,7 +61,7 @@ def test_prod_observability_scope_value(self): def test_export_url_standard_path_structure(self): """Standard export URL must use the pinned path pattern.""" url = build_export_url(self.EXPECTED_ENDPOINT, "a1", "t1") - expected = f"{self.EXPECTED_ENDPOINT}" f"{self.EXPECTED_STANDARD_PATH.format(tid='t1', aid='a1')}?api-version=1" + expected = f"{self.EXPECTED_ENDPOINT}{self.EXPECTED_STANDARD_PATH.format(tid='t1', aid='a1')}?api-version=1" self.assertEqual( url, expected, @@ -72,7 +72,7 @@ def test_export_url_standard_path_structure(self): def test_export_url_s2s_path_structure(self): """S2S export URL must use the pinned path pattern.""" url = build_export_url(self.EXPECTED_ENDPOINT, "a1", "t1", use_s2s_endpoint=True) - expected = f"{self.EXPECTED_ENDPOINT}" f"{self.EXPECTED_S2S_PATH.format(tid='t1', aid='a1')}?api-version=1" + expected = f"{self.EXPECTED_ENDPOINT}{self.EXPECTED_S2S_PATH.format(tid='t1', aid='a1')}?api-version=1" self.assertEqual( url, expected, diff --git a/tests/a365/runtime/test_utility.py b/tests/a365/runtime/test_utility.py index f9255902..4c0f5c20 100644 --- a/tests/a365/runtime/test_utility.py +++ b/tests/a365/runtime/test_utility.py @@ -259,11 +259,13 @@ def test_env_var_takes_priority_over_pyproject(self, tmp_path): pyproject = tmp_path / "pyproject.toml" pyproject.write_text('[project]\nname = "pyproject-app-name"') - with patch.dict(os.environ, {"AGENT365_APPLICATION_NAME": "env-app-name"}): - with patch.object(Path, "cwd", return_value=tmp_path): - Utility.reset_application_name_cache() - result = Utility.get_application_name() - assert result == "env-app-name" + with ( + patch.dict(os.environ, {"AGENT365_APPLICATION_NAME": "env-app-name"}), + patch.object(Path, "cwd", return_value=tmp_path), + ): + Utility.reset_application_name_cache() + result = Utility.get_application_name() + assert result == "env-app-name" def test_reads_from_pyproject_when_env_not_set(self, tmp_path): """Test reads from pyproject.toml when env var is not set.""" @@ -271,11 +273,10 @@ def test_reads_from_pyproject_when_env_not_set(self, tmp_path): pyproject.write_text('[project]\nname = "pyproject-app-name"') env = {k: v for k, v in os.environ.items() if k != "AGENT365_APPLICATION_NAME"} - with patch.dict(os.environ, env, clear=True): - with patch.object(Path, "cwd", return_value=tmp_path): - Utility.reset_application_name_cache() - result = Utility.get_application_name() - assert result == "pyproject-app-name" + with patch.dict(os.environ, env, clear=True), patch.object(Path, "cwd", return_value=tmp_path): + Utility.reset_application_name_cache() + result = Utility.get_application_name() + assert result == "pyproject-app-name" def test_caches_pyproject_result(self, tmp_path): """Test that pyproject.toml is only read once.""" @@ -283,20 +284,19 @@ def test_caches_pyproject_result(self, tmp_path): pyproject.write_text('[project]\nname = "cached-name"') env = {k: v for k, v in os.environ.items() if k != "AGENT365_APPLICATION_NAME"} - with patch.dict(os.environ, env, clear=True): - with patch.object(Path, "cwd", return_value=tmp_path): - Utility.reset_application_name_cache() + with patch.dict(os.environ, env, clear=True), patch.object(Path, "cwd", return_value=tmp_path): + Utility.reset_application_name_cache() - # First call - result1 = Utility.get_application_name() + # First call + result1 = Utility.get_application_name() - # Modify file (but cache should prevent re-read) - pyproject.write_text('[project]\nname = "new-name"') - # Second call should return cached value - result2 = Utility.get_application_name() + # Modify file (but cache should prevent re-read) + pyproject.write_text('[project]\nname = "new-name"') + # Second call should return cached value + result2 = Utility.get_application_name() - assert result1 == "cached-name" - assert result2 == "cached-name" + assert result1 == "cached-name" + assert result2 == "cached-name" def test_returns_none_when_nothing_available(self, tmp_path): """Test returns None when no env var and no pyproject.toml.""" @@ -304,11 +304,10 @@ def test_returns_none_when_nothing_available(self, tmp_path): empty_dir.mkdir() env = {k: v for k, v in os.environ.items() if k != "AGENT365_APPLICATION_NAME"} - with patch.dict(os.environ, env, clear=True): - with patch.object(Path, "cwd", return_value=empty_dir): - Utility.reset_application_name_cache() - result = Utility.get_application_name() - assert result is None + with patch.dict(os.environ, env, clear=True), patch.object(Path, "cwd", return_value=empty_dir): + Utility.reset_application_name_cache() + result = Utility.get_application_name() + assert result is None def test_handles_pyproject_without_name(self, tmp_path): """Test handles pyproject.toml without name field.""" @@ -316,11 +315,10 @@ def test_handles_pyproject_without_name(self, tmp_path): pyproject.write_text('[project]\nversion = "1.0.0"') env = {k: v for k, v in os.environ.items() if k != "AGENT365_APPLICATION_NAME"} - with patch.dict(os.environ, env, clear=True): - with patch.object(Path, "cwd", return_value=tmp_path): - Utility.reset_application_name_cache() - result = Utility.get_application_name() - assert result is None + with patch.dict(os.environ, env, clear=True), patch.object(Path, "cwd", return_value=tmp_path): + Utility.reset_application_name_cache() + result = Utility.get_application_name() + assert result is None def test_handles_pyproject_with_different_sections(self, tmp_path): """Test correctly parses name from [project] section only.""" @@ -328,11 +326,10 @@ def test_handles_pyproject_with_different_sections(self, tmp_path): pyproject.write_text('[tool.ruff]\nname = "ruff-name"\n\n[project]\nname = "project-name"\n') env = {k: v for k, v in os.environ.items() if k != "AGENT365_APPLICATION_NAME"} - with patch.dict(os.environ, env, clear=True): - with patch.object(Path, "cwd", return_value=tmp_path): - Utility.reset_application_name_cache() - result = Utility.get_application_name() - assert result == "project-name" + with patch.dict(os.environ, env, clear=True), patch.object(Path, "cwd", return_value=tmp_path): + Utility.reset_application_name_cache() + result = Utility.get_application_name() + assert result == "project-name" def test_ignores_fields_starting_with_name(self, tmp_path): """Test only matches exact 'name' field, not 'name_something'.""" @@ -340,11 +337,10 @@ def test_ignores_fields_starting_with_name(self, tmp_path): pyproject.write_text('[project]\nname_something = "wrong"\nnamespace = "also-wrong"\nname = "correct"\n') env = {k: v for k, v in os.environ.items() if k != "AGENT365_APPLICATION_NAME"} - with patch.dict(os.environ, env, clear=True): - with patch.object(Path, "cwd", return_value=tmp_path): - Utility.reset_application_name_cache() - result = Utility.get_application_name() - assert result == "correct" + with patch.dict(os.environ, env, clear=True), patch.object(Path, "cwd", return_value=tmp_path): + Utility.reset_application_name_cache() + result = Utility.get_application_name() + assert result == "correct" def test_handles_inline_comments(self, tmp_path): """Test ignores inline comments after the value.""" @@ -352,8 +348,7 @@ def test_handles_inline_comments(self, tmp_path): pyproject.write_text('[project]\nname = "my-app" # this is a comment\n') env = {k: v for k, v in os.environ.items() if k != "AGENT365_APPLICATION_NAME"} - with patch.dict(os.environ, env, clear=True): - with patch.object(Path, "cwd", return_value=tmp_path): - Utility.reset_application_name_cache() - result = Utility.get_application_name() - assert result == "my-app" + with patch.dict(os.environ, env, clear=True), patch.object(Path, "cwd", return_value=tmp_path): + Utility.reset_application_name_cache() + result = Utility.get_application_name() + assert result == "my-app" diff --git a/tests/a365/test_exporter.py b/tests/a365/test_exporter.py index c9e1753a..17ed0fec 100644 --- a/tests/a365/test_exporter.py +++ b/tests/a365/test_exporter.py @@ -358,7 +358,15 @@ def test_success_records_success(self, _enabled): exporter._session.post.return_value = _make_response(200) ok = exporter._post_with_retries(self.URL, "{}", {}) self.assertTrue(ok) - self.assertEqual(drain(REQUEST_SUCCESS_NAME), {(self.ENDPOINT, self.HOST,): 1}) + self.assertEqual( + drain(REQUEST_SUCCESS_NAME), + { + ( + self.ENDPOINT, + self.HOST, + ): 1 + }, + ) exporter.shutdown() @patch("microsoft.opentelemetry.a365.core.exporters.agent365_exporter.is_sdkstats_enabled", return_value=True) diff --git a/tests/azure_monitor/autoinstrumentation/test_configurator.py b/tests/azure_monitor/autoinstrumentation/test_configurator.py index fe5c5bd7..21a4b000 100644 --- a/tests/azure_monitor/autoinstrumentation/test_configurator.py +++ b/tests/azure_monitor/autoinstrumentation/test_configurator.py @@ -100,7 +100,7 @@ def test_configure_preview(self, mock_diagnostics, attach_mock, sampler_mock, su def test_configure_exc(self, mock_diagnostics, attach_mock, super_mock): configurator = AzureMonitorConfigurator() super_mock()._configure.side_effect = Exception("Test Exception") - with self.assertRaises(Exception): + with self.assertRaises(Exception): # noqa: B017 configurator._configure() mock_diagnostics.error.assert_called_once_with( "Azure Monitor Configurator failed during configuration: Test Exception", _ATTACH_FAILURE_CONFIGURATOR diff --git a/tests/azure_monitor/autoinstrumentation/test_distro.py b/tests/azure_monitor/autoinstrumentation/test_distro.py index 732247eb..2f946f48 100644 --- a/tests/azure_monitor/autoinstrumentation/test_distro.py +++ b/tests/azure_monitor/autoinstrumentation/test_distro.py @@ -111,7 +111,7 @@ def test_configure_preview(self, mock_diagnostics, azure_core_mock, attach_mock) def test_configure_exc(self, mock_diagnostics, azure_core_mock, attach_mock, configure_mock): distro = AzureMonitorDistro() configure_mock.side_effect = Exception("Test Exception") - with self.assertRaises(Exception): + with self.assertRaises(Exception): # noqa: B017 distro.configure() mock_diagnostics.error.assert_called_once_with( "Azure Monitor OpenTelemetry Distro failed during configuration: Test Exception", _ATTACH_FAILURE_DISTRO diff --git a/tests/azure_monitor/browserSdkLoader/test_django_middleware.py b/tests/azure_monitor/browserSdkLoader/test_django_middleware.py index d7656967..a8f19eef 100644 --- a/tests/azure_monitor/browserSdkLoader/test_django_middleware.py +++ b/tests/azure_monitor/browserSdkLoader/test_django_middleware.py @@ -173,7 +173,6 @@ def mock_get(header, default=""): patch.object(middleware._injector, "should_inject", return_value=True), patch.object(middleware._injector, "inject_with_compression") as mock_inject_with_compression, ): - modified_content = b"
Test" mock_inject_with_compression.return_value = (modified_content, None) @@ -244,7 +243,6 @@ def test_process_response_injection_exception(self, mock_logger): patch.object(middleware._injector, "should_inject", return_value=True), patch.object(middleware._injector, "inject_with_compression", side_effect=Exception("Injection failed")), ): - result = middleware.process_response(mock_request, mock_response) # Should log error and return original response @@ -282,7 +280,6 @@ def test_process_response_with_content_encoding(self): patch.object(middleware._injector, "should_inject", return_value=True) as mock_should_inject, patch.object(middleware._injector, "inject_with_compression") as mock_inject_with_compression, ): - modified_content = b"modified_gzip_content" new_encoding = "gzip" mock_inject_with_compression.return_value = (modified_content, new_encoding) diff --git a/tests/azure_monitor/conftest.py b/tests/azure_monitor/conftest.py index 236d7f14..d4f72e27 100644 --- a/tests/azure_monitor/conftest.py +++ b/tests/azure_monitor/conftest.py @@ -8,6 +8,7 @@ from tempfile import mkstemp import pytest +import contextlib @pytest.fixture @@ -15,7 +16,5 @@ def temp_file_path(): f, path = mkstemp() os.close(f) yield path - try: + with contextlib.suppress(OSError): os.unlink(path) - except OSError: - pass diff --git a/tests/azure_monitor/diagnostics/test_diagnostic_logging.py b/tests/azure_monitor/diagnostics/test_diagnostic_logging.py index 32d215a4..724ba80f 100644 --- a/tests/azure_monitor/diagnostics/test_diagnostic_logging.py +++ b/tests/azure_monitor/diagnostics/test_diagnostic_logging.py @@ -29,7 +29,7 @@ def clear_file(file_path): def check_file_for_messages(file_path, level, messages): - with open(file_path, "r", encoding="utf-8") as f: + with open(file_path, encoding="utf-8") as f: f.seek(0) for message, message_id in messages: json = loads(f.readline()) @@ -49,7 +49,7 @@ def check_file_for_messages(file_path, level, messages): def check_file_is_empty(file_path): - with open(file_path, "r", encoding="utf-8") as f: + with open(file_path, encoding="utf-8") as f: f.seek(0) assert not f.read() diff --git a/tests/azure_monitor/diagnostics/test_status_logger.py b/tests/azure_monitor/diagnostics/test_status_logger.py index 1b35e960..e28420af 100644 --- a/tests/azure_monitor/diagnostics/test_status_logger.py +++ b/tests/azure_monitor/diagnostics/test_status_logger.py @@ -62,7 +62,7 @@ def set_up(file_path, is_diagnostics_enabled=True): def check_file_for_messages(agent_initialized_successfully, file_path, reason=None, sdk_present=None): - with open(file_path, "r", encoding="utf-8") as f: + with open(file_path, encoding="utf-8") as f: f.seek(0) json = loads(f.readline()) assert json["AgentInitializedSuccessfully"] == agent_initialized_successfully @@ -84,7 +84,7 @@ def check_file_for_messages(agent_initialized_successfully, file_path, reason=No def check_file_is_empty(file_path): - with open(file_path, "r", encoding="utf-8") as f: + with open(file_path, encoding="utf-8") as f: f.seek(0) assert not f.read() diff --git a/tests/langchain/test_main_agent_propagation.py b/tests/langchain/test_main_agent_propagation.py index fa63400c..9847d4d9 100644 --- a/tests/langchain/test_main_agent_propagation.py +++ b/tests/langchain/test_main_agent_propagation.py @@ -104,12 +104,12 @@ def _make_run(**kwargs): run.id = kwargs.get("id", uuid4()) run.name = kwargs.get("name", "test_run") run.run_type = kwargs.get("run_type", "chain") - run.inputs = kwargs.get("inputs", None) - run.outputs = kwargs.get("outputs", None) - run.extra = kwargs.get("extra", None) - run.serialized = kwargs.get("serialized", None) - run.error = kwargs.get("error", None) - run.parent_run_id = kwargs.get("parent_run_id", None) + run.inputs = kwargs.get("inputs") + run.outputs = kwargs.get("outputs") + run.extra = kwargs.get("extra") + run.serialized = kwargs.get("serialized") + run.error = kwargs.get("error") + run.parent_run_id = kwargs.get("parent_run_id") run.start_time = kwargs.get("start_time", _NOW) run.end_time = kwargs.get("end_time", _NOW_END) return run diff --git a/tests/langchain/test_tracer.py b/tests/langchain/test_tracer.py index 05430aca..74f127f9 100644 --- a/tests/langchain/test_tracer.py +++ b/tests/langchain/test_tracer.py @@ -9,6 +9,8 @@ import pytest +import contextlib + pytest.importorskip("langchain_core") from microsoft.opentelemetry._genai._langchain._tracer import ( # noqa: E402 # pylint: disable=wrong-import-position @@ -39,12 +41,12 @@ def _make_run(**kwargs): run.id = kwargs.get("id", uuid4()) run.name = kwargs.get("name", "test_run") run.run_type = kwargs.get("run_type", "chain") - run.inputs = kwargs.get("inputs", None) - run.outputs = kwargs.get("outputs", None) - run.extra = kwargs.get("extra", None) - run.serialized = kwargs.get("serialized", None) - run.error = kwargs.get("error", None) - run.parent_run_id = kwargs.get("parent_run_id", None) + run.inputs = kwargs.get("inputs") + run.outputs = kwargs.get("outputs") + run.extra = kwargs.get("extra") + run.serialized = kwargs.get("serialized") + run.error = kwargs.get("error") + run.parent_run_id = kwargs.get("parent_run_id") run.start_time = kwargs.get("start_time", _NOW) run.end_time = kwargs.get("end_time", _NOW_END) return run @@ -59,7 +61,7 @@ def _make_tracer(**kwargs): otel_tracer, kwargs.get("separate_trace", False), agent_config=kwargs.get("agent_config", {}), - event_logger=kwargs.get("event_logger", None), + event_logger=kwargs.get("event_logger"), ) return tracer, otel_tracer, mock_span @@ -342,11 +344,8 @@ def test_on_llm_error_records_exception(self, mock_ctx): run = _make_run(run_type="llm", name="gpt-4") tracer._start_trace(run) error = ValueError("test error") - with patch.object(tracer, "_persist_run"): - try: - tracer.on_llm_error(error, run_id=run.id) - except Exception: - pass + with patch.object(tracer, "_persist_run"), contextlib.suppress(Exception): + tracer.on_llm_error(error, run_id=run.id) mock_span.record_exception.assert_called_with(error) @patch("microsoft.opentelemetry._genai._langchain._tracer.context_api") @@ -356,11 +355,8 @@ def test_on_chain_error_records_exception(self, mock_ctx): run = _make_run(run_type="chain", name="test") tracer._start_trace(run) error = RuntimeError("chain failed") - with patch.object(tracer, "_persist_run"): - try: - tracer.on_chain_error(error, run_id=run.id) - except Exception: - pass + with patch.object(tracer, "_persist_run"), contextlib.suppress(Exception): + tracer.on_chain_error(error, run_id=run.id) mock_span.record_exception.assert_called_with(error) @patch("microsoft.opentelemetry._genai._langchain._tracer.context_api") @@ -370,11 +366,8 @@ def test_on_tool_error_records_exception(self, mock_ctx): run = _make_run(run_type="tool", name="calc") tracer._start_trace(run) error = RuntimeError("tool failed") - with patch.object(tracer, "_persist_run"): - try: - tracer.on_tool_error(error, run_id=run.id) - except Exception: - pass + with patch.object(tracer, "_persist_run"), contextlib.suppress(Exception): + tracer.on_tool_error(error, run_id=run.id) mock_span.record_exception.assert_called_with(error) @@ -760,6 +753,7 @@ def test_attach_uses_inner_span_for_agent(self, mock_ctx): self.assertIs(attach_call_args[0][0], inner_span) mock_ctx.attach.assert_called_once() + # ---- invoke_agent aggregation fixes (issue #172) ----------------------------- @@ -1070,9 +1064,7 @@ def test_tool_role_message_becomes_tool_call_response(self): "id": ["langchain", "schema", "messages", "AIMessage"], "kwargs": { "content": "", - "tool_calls": [ - {"name": "get_weather", "args": {"location": "Paris"}, "id": "tc1"} - ], + "tool_calls": [{"name": "get_weather", "args": {"location": "Paris"}, "id": "tc1"}], "type": "ai", }, }, @@ -1086,4 +1078,3 @@ def test_tool_role_message_becomes_tool_call_response(self): self.assertEqual(tool_part.type, "tool_call_response") self.assertEqual(tool_part.id, "tc1") self.assertEqual(tool_part.response, "rainy") - diff --git a/tests/langchain/test_utils.py b/tests/langchain/test_utils.py index abb93ccb..6f9c2125 100644 --- a/tests/langchain/test_utils.py +++ b/tests/langchain/test_utils.py @@ -12,7 +12,8 @@ pytest.importorskip("langchain_core") -from microsoft.opentelemetry._genai._langchain._utils import ( # noqa: E402 # pylint: disable=wrong-import-position +# pylint: disable=wrong-import-position +from microsoft.opentelemetry._genai._langchain._utils import ( # noqa: E402 DictWithLock, CHAT_OPERATION_NAME, EXECUTE_TOOL_OPERATION_NAME, @@ -74,12 +75,12 @@ def _make_run(**kwargs): run.id = kwargs.get("id", "run-1") run.name = kwargs.get("name", "test_run") run.run_type = kwargs.get("run_type", "chain") - run.inputs = kwargs.get("inputs", None) - run.outputs = kwargs.get("outputs", None) - run.extra = kwargs.get("extra", None) - run.serialized = kwargs.get("serialized", None) - run.error = kwargs.get("error", None) - run.parent_run_id = kwargs.get("parent_run_id", None) + run.inputs = kwargs.get("inputs") + run.outputs = kwargs.get("outputs") + run.extra = kwargs.get("extra") + run.serialized = kwargs.get("serialized") + run.error = kwargs.get("error") + run.parent_run_id = kwargs.get("parent_run_id") run.start_time = kwargs.get("start_time", datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)) run.end_time = kwargs.get("end_time", datetime.datetime(2024, 1, 1, second=1, tzinfo=datetime.timezone.utc)) return run @@ -146,7 +147,7 @@ def test_catches_exception(self): @stop_on_exception def gen(): raise ValueError("boom") - yield ("k", "v") # noqa: unreachable # pylint: disable=unreachable + yield ("k", "v") # noqa: unreachable, pylint: disable=unreachable self.assertEqual(list(gen()), []) @@ -1196,6 +1197,7 @@ def test_response_model_from_message_kwargs_response_metadata(self): self.assertEqual(inv.response_model_name, "gpt-4o-2024-11-20") self.assertEqual(inv.response_id, "chatcmpl-kwargs") + # ---- Spec-compliant input.messages (issue #172) ------------------------------ diff --git a/tests/openai_agents/test_trace_instrumentor.py b/tests/openai_agents/test_trace_instrumentor.py index c3647041..cae565e3 100644 --- a/tests/openai_agents/test_trace_instrumentor.py +++ b/tests/openai_agents/test_trace_instrumentor.py @@ -47,18 +47,20 @@ def test_instrument_creates_processor(self, mock_trace_api): instrumentor = A365OpenAIAgentsInstrumentor() # Test the actual _instrument logic - with patch.dict("sys.modules", {"agents.tracing": MagicMock()}): - with patch( + with ( + patch.dict("sys.modules", {"agents.tracing": MagicMock()}), + patch( "microsoft.opentelemetry._genai._openai_agents._trace_instrumentor.OpenAIAgentsTraceProcessor" - ) as MockProc: - mock_proc_instance = MagicMock() - MockProc.return_value = mock_proc_instance + ) as MockProc, + ): + mock_proc_instance = MagicMock() + MockProc.return_value = mock_proc_instance - instrumentor._instrument() + instrumentor._instrument() - mock_trace_api.get_tracer.assert_called_once() - MockProc.assert_called_once_with(mock_tracer) - self.assertIs(instrumentor._processor, mock_proc_instance) + mock_trace_api.get_tracer.assert_called_once() + MockProc.assert_called_once_with(mock_tracer) + self.assertIs(instrumentor._processor, mock_proc_instance) def test_instrument_idempotent(self): """Calling _instrument twice should not create a second processor.""" diff --git a/tests/test_distro.py b/tests/test_distro.py index a45f0de7..d7531995 100644 --- a/tests/test_distro.py +++ b/tests/test_distro.py @@ -855,18 +855,20 @@ def test_grpc_fallback_to_http_when_grpc_unavailable(self): mock_http_exporter = MagicMock() otel_kwargs = {} - with patch.dict( - "sys.modules", - { - "opentelemetry.exporter.otlp.proto.grpc": None, - "opentelemetry.exporter.otlp.proto.grpc.trace_exporter": None, - }, - ): - with patch( + with ( + patch.dict( + "sys.modules", + { + "opentelemetry.exporter.otlp.proto.grpc": None, + "opentelemetry.exporter.otlp.proto.grpc.trace_exporter": None, + }, + ), + patch( "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", return_value=mock_http_exporter, - ): - _append_spectra_components(True, otel_kwargs, protocol="grpc") + ), + ): + _append_spectra_components(True, otel_kwargs, protocol="grpc") processors = otel_kwargs.get("span_processors", []) self.assertEqual(len(processors), 1) diff --git a/tests/test_langchain_integration.py b/tests/test_langchain_integration.py index 4a1e638c..c605fc67 100644 --- a/tests/test_langchain_integration.py +++ b/tests/test_langchain_integration.py @@ -135,8 +135,6 @@ def test_callback_manager_patched(self, mock_get_tracer, mock_get_logger): inst = LangChainInstrumentor() inst._instrument() - from langchain_core.callbacks import CallbackManager # noqa: F811 pylint: disable=unused-import - # After instrumentation, creating a new CallbackManager should include # the OTel tracer in its handlers (or the __init__ should be wrapped). # We verify by checking the instrumentor recorded the original init. diff --git a/tests/test_openai_agents_integration.py b/tests/test_openai_agents_integration.py index e233e571..74314b93 100644 --- a/tests/test_openai_agents_integration.py +++ b/tests/test_openai_agents_integration.py @@ -134,13 +134,15 @@ def test_instrument_creates_processor(self): mock_trace_api.get_tracer.return_value = MagicMock() inst = self.instrumentor_cls() - with patch.dict("sys.modules", {"agents.tracing": agents.tracing}): - with patch( + with ( + patch.dict("sys.modules", {"agents.tracing": agents.tracing}), + patch( "microsoft.opentelemetry._genai._openai_agents._trace_instrumentor.OpenAIAgentsTraceProcessor" - ) as MockProc: - MockProc.return_value = MagicMock() - inst._instrument() - MockProc.assert_called_once() + ) as MockProc, + ): + MockProc.return_value = MagicMock() + inst._instrument() + MockProc.assert_called_once() if __name__ == "__main__": diff --git a/tests/test_openai_integration.py b/tests/test_openai_integration.py index 3ebb41a8..08f59d16 100644 --- a/tests/test_openai_integration.py +++ b/tests/test_openai_integration.py @@ -15,6 +15,8 @@ import pytest +import contextlib + openai = pytest.importorskip("openai") # pylint: disable=wrong-import-position @@ -124,13 +126,11 @@ def test_chat_completion_produces_span(self): create=True, ): client = openai.OpenAI(api_key="test-key") - try: + with contextlib.suppress(Exception): client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Hi"}], ) - except Exception: # pylint: disable=broad-exception-caught - pass # API call may fail; we still get spans from the wrapper self.provider.force_flush() spans = self.exporter.get_finished_spans() diff --git a/tests/test_sdkstats.py b/tests/test_sdkstats.py index 9f0dc61e..d6eb0f0e 100644 --- a/tests/test_sdkstats.py +++ b/tests/test_sdkstats.py @@ -565,7 +565,7 @@ def test_span_success_records(self): wrapper = _NetworkStatsSpanExporter(self._inner_span(SpanExportResult.SUCCESS)) self.assertEqual(wrapper.export([]), SpanExportResult.SUCCESS) - self.assertEqual(drain(REQUEST_SUCCESS_NAME), {("otlp","otlp.example.com"): 1}) + self.assertEqual(drain(REQUEST_SUCCESS_NAME), {("otlp", "otlp.example.com"): 1}) def test_span_failure_does_not_record(self): from opentelemetry.sdk.trace.export import SpanExportResult @@ -595,7 +595,7 @@ def test_metric_success_records(self): inner.export.return_value = MetricExportResult.SUCCESS wrapper = _NetworkStatsMetricExporter(inner) wrapper.export(MagicMock()) - self.assertEqual(drain(REQUEST_SUCCESS_NAME), {("otlp","otlp.example.com"): 1}) + self.assertEqual(drain(REQUEST_SUCCESS_NAME), {("otlp", "otlp.example.com"): 1}) def test_metric_failure_does_not_record(self): from opentelemetry.sdk.metrics.export import MetricExportResult @@ -618,7 +618,7 @@ def test_log_success_records(self): inner.export.return_value = LogRecordExportResult.SUCCESS wrapper = _NetworkStatsLogExporter(inner) wrapper.export([]) - self.assertEqual(drain(REQUEST_SUCCESS_NAME), {("otlp","otlp.example.com"): 1}) + self.assertEqual(drain(REQUEST_SUCCESS_NAME), {("otlp", "otlp.example.com"): 1}) def test_log_failure_does_not_record(self): from opentelemetry.sdk._logs.export import LogRecordExportResult diff --git a/tox.ini b/tox.ini index 2d1b0fb3..150fe463 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ requires = tox>=4.4.10 # agent-framework-azure-ai supports 310-314 -envlist = pylint,mypy,black,pytest-py{310,311,312,313,314} +envlist = pylint,mypy,ruff,pytest-py{310,311,312,313,314} [testenv] skip_install = true @@ -35,13 +35,6 @@ commands = python -m pylint --output-format=parseable --disable=protected-access,too-many-public-methods,too-many-lines {tox_root}/tests python -m pylint --output-format=parseable --disable=import-error {tox_root}/samples -[testenv:black] -description = Format code with black -deps = - black>=24.0 -commands = - python -m black {tox_root}/src {tox_root}/tests {tox_root}/samples - [testenv:docs] description = Build Sphinx documentation deps = @@ -81,3 +74,11 @@ passenv = commands = python -m pip install -e {tox_root}[langchain,agent-framework] python -m pytest -v {tox_root}/tests/a365/integration {posargs} + +[testenv:ruff] +description = Format and Lint the code with ruff +deps = + ruff>=0.6 +commands = + ruff check src tests samples + ruff format --check src tests samples \ No newline at end of file