diff --git a/aws-opentelemetry-distro/pyproject.toml b/aws-opentelemetry-distro/pyproject.toml index 3d8eadbc1..f8984854d 100644 --- a/aws-opentelemetry-distro/pyproject.toml +++ b/aws-opentelemetry-distro/pyproject.toml @@ -24,62 +24,62 @@ classifiers = [ ] dependencies = [ - "opentelemetry-api == 1.27.0", - "opentelemetry-sdk == 1.27.0", - "opentelemetry-exporter-otlp-proto-grpc == 1.27.0", - "opentelemetry-exporter-otlp-proto-http == 1.27.0", - "opentelemetry-propagator-b3 == 1.27.0", - "opentelemetry-propagator-jaeger == 1.27.0", - "opentelemetry-exporter-otlp-proto-common == 1.27.0", + "opentelemetry-api == 1.33.1", + "opentelemetry-sdk == 1.33.1", + "opentelemetry-exporter-otlp-proto-grpc == 1.33.1", + "opentelemetry-exporter-otlp-proto-http == 1.33.1", + "opentelemetry-propagator-b3 == 1.33.1", + "opentelemetry-propagator-jaeger == 1.33.1", + "opentelemetry-exporter-otlp-proto-common == 1.33.1", "opentelemetry-sdk-extension-aws == 2.0.2", "opentelemetry-propagator-aws-xray == 1.0.1", - "opentelemetry-distro == 0.48b0", - "opentelemetry-processor-baggage == 0.48b0", - "opentelemetry-propagator-ot-trace == 0.48b0", - "opentelemetry-instrumentation == 0.48b0", - "opentelemetry-instrumentation-aws-lambda == 0.48b0", - "opentelemetry-instrumentation-aio-pika == 0.48b0", - "opentelemetry-instrumentation-aiohttp-client == 0.48b0", - "opentelemetry-instrumentation-aiopg == 0.48b0", - "opentelemetry-instrumentation-asgi == 0.48b0", - "opentelemetry-instrumentation-asyncpg == 0.48b0", - "opentelemetry-instrumentation-boto == 0.48b0", - "opentelemetry-instrumentation-boto3sqs == 0.48b0", - "opentelemetry-instrumentation-botocore == 0.48b0", - "opentelemetry-instrumentation-celery == 0.48b0", - "opentelemetry-instrumentation-confluent-kafka == 0.48b0", - "opentelemetry-instrumentation-dbapi == 0.48b0", - "opentelemetry-instrumentation-django == 0.48b0", - "opentelemetry-instrumentation-elasticsearch == 0.48b0", - "opentelemetry-instrumentation-falcon == 0.48b0", - "opentelemetry-instrumentation-fastapi == 0.48b0", - "opentelemetry-instrumentation-flask == 0.48b0", - "opentelemetry-instrumentation-grpc == 0.48b0", - "opentelemetry-instrumentation-httpx == 0.48b0", - "opentelemetry-instrumentation-jinja2 == 0.48b0", - "opentelemetry-instrumentation-kafka-python == 0.48b0", - "opentelemetry-instrumentation-logging == 0.48b0", - "opentelemetry-instrumentation-mysql == 0.48b0", - "opentelemetry-instrumentation-mysqlclient == 0.48b0", - "opentelemetry-instrumentation-pika == 0.48b0", - "opentelemetry-instrumentation-psycopg2 == 0.48b0", - "opentelemetry-instrumentation-pymemcache == 0.48b0", - "opentelemetry-instrumentation-pymongo == 0.48b0", - "opentelemetry-instrumentation-pymysql == 0.48b0", - "opentelemetry-instrumentation-pyramid == 0.48b0", - "opentelemetry-instrumentation-redis == 0.48b0", - "opentelemetry-instrumentation-remoulade == 0.48b0", - "opentelemetry-instrumentation-requests == 0.48b0", - "opentelemetry-instrumentation-sqlalchemy == 0.48b0", - "opentelemetry-instrumentation-sqlite3 == 0.48b0", - "opentelemetry-instrumentation-starlette == 0.48b0", - "opentelemetry-instrumentation-system-metrics == 0.48b0", - "opentelemetry-instrumentation-tornado == 0.48b0", - "opentelemetry-instrumentation-tortoiseorm == 0.48b0", - "opentelemetry-instrumentation-urllib == 0.48b0", - "opentelemetry-instrumentation-urllib3 == 0.48b0", - "opentelemetry-instrumentation-wsgi == 0.48b0", - "opentelemetry-instrumentation-cassandra == 0.48b0", + "opentelemetry-distro == 0.54b1", + "opentelemetry-processor-baggage == 0.54b1", + "opentelemetry-propagator-ot-trace == 0.54b1", + "opentelemetry-instrumentation == 0.54b1", + "opentelemetry-instrumentation-aws-lambda == 0.54b1", + "opentelemetry-instrumentation-aio-pika == 0.54b1", + "opentelemetry-instrumentation-aiohttp-client == 0.54b1", + "opentelemetry-instrumentation-aiopg == 0.54b1", + "opentelemetry-instrumentation-asgi == 0.54b1", + "opentelemetry-instrumentation-asyncpg == 0.54b1", + "opentelemetry-instrumentation-boto == 0.54b1", + "opentelemetry-instrumentation-boto3sqs == 0.54b1", + "opentelemetry-instrumentation-botocore == 0.54b1", + "opentelemetry-instrumentation-celery == 0.54b1", + "opentelemetry-instrumentation-confluent-kafka == 0.54b1", + "opentelemetry-instrumentation-dbapi == 0.54b1", + "opentelemetry-instrumentation-django == 0.54b1", + "opentelemetry-instrumentation-elasticsearch == 0.54b1", + "opentelemetry-instrumentation-falcon == 0.54b1", + "opentelemetry-instrumentation-fastapi == 0.54b1", + "opentelemetry-instrumentation-flask == 0.54b1", + "opentelemetry-instrumentation-grpc == 0.54b1", + "opentelemetry-instrumentation-httpx == 0.54b1", + "opentelemetry-instrumentation-jinja2 == 0.54b1", + "opentelemetry-instrumentation-kafka-python == 0.54b1", + "opentelemetry-instrumentation-logging == 0.54b1", + "opentelemetry-instrumentation-mysql == 0.54b1", + "opentelemetry-instrumentation-mysqlclient == 0.54b1", + "opentelemetry-instrumentation-pika == 0.54b1", + "opentelemetry-instrumentation-psycopg2 == 0.54b1", + "opentelemetry-instrumentation-pymemcache == 0.54b1", + "opentelemetry-instrumentation-pymongo == 0.54b1", + "opentelemetry-instrumentation-pymysql == 0.54b1", + "opentelemetry-instrumentation-pyramid == 0.54b1", + "opentelemetry-instrumentation-redis == 0.54b1", + "opentelemetry-instrumentation-remoulade == 0.54b1", + "opentelemetry-instrumentation-requests == 0.54b1", + "opentelemetry-instrumentation-sqlalchemy == 0.54b1", + "opentelemetry-instrumentation-sqlite3 == 0.54b1", + "opentelemetry-instrumentation-starlette == 0.54b1", + "opentelemetry-instrumentation-system-metrics == 0.54b1", + "opentelemetry-instrumentation-tornado == 0.54b1", + "opentelemetry-instrumentation-tortoiseorm == 0.54b1", + "opentelemetry-instrumentation-urllib == 0.54b1", + "opentelemetry-instrumentation-urllib3 == 0.54b1", + "opentelemetry-instrumentation-wsgi == 0.54b1", + "opentelemetry-instrumentation-cassandra == 0.54b1", ] [project.optional-dependencies] diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index ec5b693ed..173f8492b 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -35,7 +35,6 @@ ) from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute from amazon.opentelemetry.distro._aws_span_processing_util import ( - GEN_AI_REQUEST_MODEL, LOCAL_ROOT, MAX_KEYWORD_LENGTH, SQL_KEYWORD_PATTERN, @@ -60,6 +59,7 @@ from amazon.opentelemetry.distro.sqs_url_parser import SqsUrlParser from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import BoundedAttributes, ReadableSpan +from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_REQUEST_MODEL from opentelemetry.semconv.trace import SpanAttributes # Pertinent OTEL attribute keys diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py index 21e19afa9..d2a039861 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py @@ -26,16 +26,6 @@ # Max keyword length supported by parsing into remote_operation from DB_STATEMENT MAX_KEYWORD_LENGTH = 27 -# TODO: Use Semantic Conventions once upgrade to 0.47b0 -GEN_AI_REQUEST_MODEL: str = "gen_ai.request.model" -GEN_AI_SYSTEM: str = "gen_ai.system" -GEN_AI_REQUEST_MAX_TOKENS: str = "gen_ai.request.max_tokens" -GEN_AI_REQUEST_TEMPERATURE: str = "gen_ai.request.temperature" -GEN_AI_REQUEST_TOP_P: str = "gen_ai.request.top_p" -GEN_AI_RESPONSE_FINISH_REASONS: str = "gen_ai.response.finish_reasons" -GEN_AI_USAGE_INPUT_TOKENS: str = "gen_ai.usage.input_tokens" -GEN_AI_USAGE_OUTPUT_TOKENS: str = "gen_ai.usage.output_tokens" - # Get dialect keywords retrieved from dialect_keywords.json file. # Only meant to be invoked by SQL_KEYWORD_PATTERN and unit tests diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index 149f9ad29..fa5acf42c 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -2,10 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 import os -import sys +from importlib.metadata import PackageNotFoundError, version from logging import Logger, getLogger -import pkg_resources +from packaging.requirements import Requirement _logger: Logger = getLogger(__name__) @@ -14,15 +14,21 @@ def is_installed(req: str) -> bool: """Is the given required package installed?""" - - if req in sys.modules and sys.modules[req] is not None: - return True + req = Requirement(req) try: - pkg_resources.get_distribution(req) - except Exception as exc: # pylint: disable=broad-except + dist_version = version(req.name) + except PackageNotFoundError as exc: _logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc) return False + + if not list(req.specifier.filter([dist_version])): + _logger.debug( + "instrumentation for package %s is available but version %s is installed. Skipping.", + req, + dist_version, + ) + return False return True diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py index a25e55330..549154771 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py @@ -2,13 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 import abc import inspect -import io -import json import logging -import math -from typing import Any, Dict, Optional - -from botocore.response import StreamingBody +from typing import Dict, Optional from amazon.opentelemetry.distro._aws_attribute_keys import ( AWS_BEDROCK_AGENT_ID, @@ -17,20 +12,11 @@ AWS_BEDROCK_GUARDRAIL_ID, AWS_BEDROCK_KNOWLEDGE_BASE_ID, ) -from amazon.opentelemetry.distro._aws_span_processing_util import ( - GEN_AI_REQUEST_MAX_TOKENS, - GEN_AI_REQUEST_MODEL, - GEN_AI_REQUEST_TEMPERATURE, - GEN_AI_REQUEST_TOP_P, - GEN_AI_RESPONSE_FINISH_REASONS, - GEN_AI_SYSTEM, - GEN_AI_USAGE_INPUT_TOKENS, - GEN_AI_USAGE_OUTPUT_TOKENS, -) from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkCallContext, _AwsSdkExtension, + _BotocoreInstrumentorContext, _BotoResultT, ) from opentelemetry.trace.span import Span @@ -192,7 +178,7 @@ def extract_attributes(self, attributes: _AttributeMapT): if request_param_value: attributes[attribute_key] = request_param_value - def on_success(self, span: Span, result: _BotoResultT): + def on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): if self._operation_class is None: return @@ -229,7 +215,7 @@ class _BedrockExtension(_AwsSdkExtension): """ # pylint: disable=no-self-use - def on_success(self, span: Span, result: _BotoResultT): + def on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): # _GUARDRAIL_ID can only be retrieved from the response, not from the request guardrail_id = result.get(_GUARDRAIL_ID) if guardrail_id: @@ -244,205 +230,3 @@ def on_success(self, span: Span, result: _BotoResultT): AWS_BEDROCK_GUARDRAIL_ARN, guardrail_arn, ) - - -class _BedrockRuntimeExtension(_AwsSdkExtension): - """ - This class is an extension for - Amazon Bedrock Runtime. - """ - - def extract_attributes(self, attributes: _AttributeMapT): - attributes[GEN_AI_SYSTEM] = _AWS_BEDROCK_SYSTEM - - model_id = self._call_context.params.get(_MODEL_ID) - if model_id: - attributes[GEN_AI_REQUEST_MODEL] = model_id - - # Get the request body if it exists - body = self._call_context.params.get("body") - if body: - try: - request_body = json.loads(body) - - if "amazon.titan" in model_id: - self._extract_titan_attributes(attributes, request_body) - if "amazon.nova" in model_id: - self._extract_nova_attributes(attributes, request_body) - elif "anthropic.claude" in model_id: - self._extract_claude_attributes(attributes, request_body) - elif "meta.llama" in model_id: - self._extract_llama_attributes(attributes, request_body) - elif "cohere.command" in model_id: - self._extract_cohere_attributes(attributes, request_body) - elif "ai21.jamba" in model_id: - self._extract_ai21_attributes(attributes, request_body) - elif "mistral" in model_id: - self._extract_mistral_attributes(attributes, request_body) - - except json.JSONDecodeError: - _logger.debug("Error: Unable to parse the body as JSON") - - def _extract_titan_attributes(self, attributes, request_body): - config = request_body.get("textGenerationConfig", {}) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, config.get("topP")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("maxTokenCount")) - - def _extract_nova_attributes(self, attributes, request_body): - config = request_body.get("inferenceConfig", {}) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, config.get("top_p")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("max_new_tokens")) - - def _extract_claude_attributes(self, attributes, request_body): - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) - - def _extract_cohere_attributes(self, attributes, request_body): - prompt = request_body.get("message") - if prompt: - attributes[GEN_AI_USAGE_INPUT_TOKENS] = math.ceil(len(prompt) / 6) - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("p")) - - def _extract_ai21_attributes(self, attributes, request_body): - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) - - def _extract_llama_attributes(self, attributes, request_body): - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_gen_len")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) - - def _extract_mistral_attributes(self, attributes, request_body): - prompt = request_body.get("prompt") - if prompt: - attributes[GEN_AI_USAGE_INPUT_TOKENS] = math.ceil(len(prompt) / 6) - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) - - @staticmethod - def _set_if_not_none(attributes, key, value): - if value is not None: - attributes[key] = value - - # pylint: disable=too-many-branches - def on_success(self, span: Span, result: Dict[str, Any]): - model_id = self._call_context.params.get(_MODEL_ID) - - if not model_id: - return - - if "body" in result and isinstance(result["body"], StreamingBody): - original_body = None - try: - original_body = result["body"] - body_content = original_body.read() - - # Use one stream for telemetry - stream = io.BytesIO(body_content) - telemetry_content = stream.read() - response_body = json.loads(telemetry_content.decode("utf-8")) - if "amazon.titan" in model_id: - self._handle_amazon_titan_response(span, response_body) - if "amazon.nova" in model_id: - self._handle_amazon_nova_response(span, response_body) - elif "anthropic.claude" in model_id: - self._handle_anthropic_claude_response(span, response_body) - elif "meta.llama" in model_id: - self._handle_meta_llama_response(span, response_body) - elif "cohere.command" in model_id: - self._handle_cohere_command_response(span, response_body) - elif "ai21.jamba" in model_id: - self._handle_ai21_jamba_response(span, response_body) - elif "mistral" in model_id: - self._handle_mistral_mistral_response(span, response_body) - # Replenish stream for downstream application use - new_stream = io.BytesIO(body_content) - result["body"] = StreamingBody(new_stream, len(body_content)) - - except json.JSONDecodeError: - _logger.debug("Error: Unable to parse the response body as JSON") - except Exception as e: # pylint: disable=broad-exception-caught, invalid-name - _logger.debug("Error processing response: %s", e) - finally: - if original_body is not None: - original_body.close() - - # pylint: disable=no-self-use - def _handle_amazon_titan_response(self, span: Span, response_body: Dict[str, Any]): - if "inputTextTokenCount" in response_body: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, response_body["inputTextTokenCount"]) - if "results" in response_body and response_body["results"]: - result = response_body["results"][0] - if "tokenCount" in result: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, result["tokenCount"]) - if "completionReason" in result: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [result["completionReason"]]) - - # pylint: disable=no-self-use - def _handle_amazon_nova_response(self, span: Span, response_body: Dict[str, Any]): - if "usage" in response_body: - usage = response_body["usage"] - if "inputTokens" in usage: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["inputTokens"]) - if "outputTokens" in usage: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["outputTokens"]) - if "stopReason" in response_body: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stopReason"]]) - - # pylint: disable=no-self-use - def _handle_anthropic_claude_response(self, span: Span, response_body: Dict[str, Any]): - if "usage" in response_body: - usage = response_body["usage"] - if "input_tokens" in usage: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["input_tokens"]) - if "output_tokens" in usage: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["output_tokens"]) - if "stop_reason" in response_body: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]) - - # pylint: disable=no-self-use - def _handle_cohere_command_response(self, span: Span, response_body: Dict[str, Any]): - # Output tokens: Approximate from the response text - if "text" in response_body: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, math.ceil(len(response_body["text"]) / 6)) - if "finish_reason" in response_body: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["finish_reason"]]) - - # pylint: disable=no-self-use - def _handle_ai21_jamba_response(self, span: Span, response_body: Dict[str, Any]): - if "usage" in response_body: - usage = response_body["usage"] - if "prompt_tokens" in usage: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["prompt_tokens"]) - if "completion_tokens" in usage: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["completion_tokens"]) - if "choices" in response_body: - choices = response_body["choices"][0] - if "finish_reason" in choices: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [choices["finish_reason"]]) - - # pylint: disable=no-self-use - def _handle_meta_llama_response(self, span: Span, response_body: Dict[str, Any]): - if "prompt_token_count" in response_body: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, response_body["prompt_token_count"]) - if "generation_token_count" in response_body: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, response_body["generation_token_count"]) - if "stop_reason" in response_body: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]) - - # pylint: disable=no-self-use - def _handle_mistral_mistral_response(self, span: Span, response_body: Dict[str, Any]): - if "outputs" in response_body: - outputs = response_body["outputs"][0] - if "text" in outputs: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, math.ceil(len(outputs["text"]) / 6)) - if "stop_reason" in outputs: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [outputs["stop_reason"]]) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py index 0f4a77d1e..ffc81b348 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py @@ -19,13 +19,17 @@ _BedrockAgentExtension, _BedrockAgentRuntimeExtension, _BedrockExtension, - _BedrockRuntimeExtension, ) from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS from opentelemetry.instrumentation.botocore.extensions.lmbd import _LambdaExtension from opentelemetry.instrumentation.botocore.extensions.sns import _SnsExtension from opentelemetry.instrumentation.botocore.extensions.sqs import _SqsExtension -from opentelemetry.instrumentation.botocore.extensions.types import _AttributeMapT, _AwsSdkExtension, _BotoResultT +from opentelemetry.instrumentation.botocore.extensions.types import ( + _AttributeMapT, + _AwsSdkExtension, + _BotocoreInstrumentorContext, + _BotoResultT, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.span import Span @@ -75,8 +79,8 @@ def patch_extract_attributes(self, attributes: _AttributeMapT): old_on_success = _LambdaExtension.on_success - def patch_on_success(self, span: Span, result: _BotoResultT): - old_on_success(self, span, result) + def patch_on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): + old_on_success(self, span, result, instrumentor_context) lambda_configuration = result.get("Configuration", {}) function_arn = lambda_configuration.get("FunctionArn") if function_arn: @@ -180,8 +184,8 @@ def patch_extract_attributes(self, attributes: _AttributeMapT): old_on_success = _SqsExtension.on_success - def patch_on_success(self, span: Span, result: _BotoResultT): - old_on_success(self, span, result) + def patch_on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): + old_on_success(self, span, result, instrumentor_context) queue_url = result.get("QueueUrl") if queue_url: span.set_attribute(AWS_SQS_QUEUE_URL, queue_url) @@ -191,17 +195,17 @@ def patch_on_success(self, span: Span, result: _BotoResultT): def _apply_botocore_bedrock_patch() -> None: - """Botocore instrumentation patch for Bedrock, Bedrock Agent, Bedrock Runtime and Bedrock Agent Runtime + """Botocore instrumentation patch for Bedrock, Bedrock Agent, and Bedrock Agent Runtime This patch adds an extension to the upstream's list of known extension for Bedrock. Extensions allow for custom logic for adding service-specific information to spans, such as attributes. - Specifically, we are adding logic to add the AWS_BEDROCK attributes referenced in _aws_attribute_keys, - GEN_AI_REQUEST_MODEL and GEN_AI_SYSTEM attributes referenced in _aws_span_processing_util. + Specifically, we are adding logic to add the AWS_BEDROCK attributes referenced in _aws_attribute_keys. + Note: Bedrock Runtime uses the upstream extension directly. """ _KNOWN_EXTENSIONS["bedrock"] = _lazy_load(".", "_BedrockExtension") _KNOWN_EXTENSIONS["bedrock-agent"] = _lazy_load(".", "_BedrockAgentExtension") _KNOWN_EXTENSIONS["bedrock-agent-runtime"] = _lazy_load(".", "_BedrockAgentRuntimeExtension") - _KNOWN_EXTENSIONS["bedrock-runtime"] = _lazy_load(".", "_BedrockRuntimeExtension") + # bedrock-runtime is handled by upstream # The OpenTelemetry Authors code @@ -243,7 +247,7 @@ def extract_attributes(self, attributes: _AttributeMapT): attributes[AWS_SECRETSMANAGER_SECRET_ARN] = secret_id # pylint: disable=no-self-use - def on_success(self, span: Span, result: _BotoResultT): + def on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): secret_arn = result.get("ARN") if secret_arn: span.set_attribute(AWS_SECRETSMANAGER_SECRET_ARN, secret_arn) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py index e0c62b89d..7d6479251 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py @@ -1,5 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from importlib.metadata import PackageNotFoundError from unittest import TestCase from unittest.mock import patch @@ -19,11 +20,12 @@ class TestAwsAuthSession(TestCase): - @patch("pkg_resources.get_distribution", side_effect=ImportError("test error")) - @patch.dict("sys.modules", {"botocore": None}, clear=False) + @patch("amazon.opentelemetry.distro._utils.version") + @patch.dict("sys.modules", {"botocore": None}) @patch("requests.Session.request", return_value=requests.Response()) - def test_aws_auth_session_no_botocore(self, _, __): + def test_aws_auth_session_no_botocore(self, mock_request, mock_version): """Tests that aws_auth_session will not inject SigV4 Headers if botocore is not installed.""" + mock_version.side_effect = PackageNotFoundError("botocore") session = AwsAuthSession("us-east-1", "xray") actual_headers = {"test": "test"} diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py index d122519cf..f99b0d154 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py @@ -37,12 +37,12 @@ AWS_STEPFUNCTIONS_STATEMACHINE_ARN, ) from amazon.opentelemetry.distro._aws_metric_attribute_generator import _AwsMetricAttributeGenerator -from amazon.opentelemetry.distro._aws_span_processing_util import GEN_AI_REQUEST_MODEL from amazon.opentelemetry.distro.metric_attribute_generator import DEPENDENCY_METRIC, SERVICE_METRIC from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.resources import _DEFAULT_RESOURCE, SERVICE_NAME from opentelemetry.sdk.trace import ReadableSpan, Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_REQUEST_MODEL from opentelemetry.semconv.trace import MessagingOperationValues, SpanAttributes from opentelemetry.trace import SpanContext, SpanKind from opentelemetry.util.types import Attributes diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py index 13397a0d5..dbaee3c33 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py @@ -50,6 +50,7 @@ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.metrics import get_meter_provider from opentelemetry.processor.baggage import BaggageSpanProcessor from opentelemetry.sdk.environment_variables import OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG from opentelemetry.sdk.metrics._internal.export import PeriodicExportingMetricReader @@ -87,6 +88,22 @@ def setUpClass(cls): aws_otel_configurator.configure() cls.tracer_provider: TracerProvider = get_tracer_provider() + @classmethod + def tearDownClass(cls): + # Explicitly shut down meter provider to avoid I/O errors on Python 3.9 with gevent + # This ensures ConsoleMetricExporter is properly closed before Python cleanup + try: + meter_provider = get_meter_provider() + if hasattr(meter_provider, "force_flush"): + meter_provider.force_flush() + if hasattr(meter_provider, "shutdown"): + meter_provider.shutdown() + except (ValueError, RuntimeError): + # Ignore errors during cleanup: + # - ValueError: I/O operation on closed file (the exact error we're trying to prevent) + # - RuntimeError: Provider already shut down or threading issues + pass + def tearDown(self): os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None) os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED", None) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py index b77e4fbf8..7368a04c8 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py @@ -1,13 +1,12 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from importlib.metadata import PackageNotFoundError, version from unittest import TestCase -from pkg_resources import DistributionNotFound, require - class TestAwsOpenTelemetryDistro(TestCase): def test_package_available(self): try: - require(["aws-opentelemetry-distro"]) - except DistributionNotFound: + version("aws-opentelemetry-distro") + except PackageNotFoundError: self.fail("aws-opentelemetry-distro not installed") diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index 87e6c4810..8eff6f2e6 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -1,17 +1,15 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import json -import math import os -from io import BytesIO +from importlib.metadata import PackageNotFoundError from typing import Any, Dict from unittest import TestCase from unittest.mock import MagicMock, patch import gevent.monkey -import pkg_resources -from botocore.response import StreamingBody +import opentelemetry.sdk.extension.aws.resource.ec2 as ec2_resource +import opentelemetry.sdk.extension.aws.resource.eks as eks_resource from amazon.opentelemetry.distro.patches._instrumentation_patch import ( AWS_GEVENT_PATCH_MODULES, apply_instrumentation_patches, @@ -38,7 +36,7 @@ _LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID" # Patch names -GET_DISTRIBUTION_PATCH: str = "amazon.opentelemetry.distro._utils.pkg_resources.get_distribution" +IMPORTLIB_METADATA_VERSION_PATCH: str = "amazon.opentelemetry.distro._utils.version" class TestInstrumentationPatch(TestCase): @@ -60,7 +58,7 @@ class TestInstrumentationPatch(TestCase): def test_instrumentation_patch(self): # Set up method patches used by all tests - self.method_patches[GET_DISTRIBUTION_PATCH] = patch(GET_DISTRIBUTION_PATCH).start() + self.method_patches[IMPORTLIB_METADATA_VERSION_PATCH] = patch(IMPORTLIB_METADATA_VERSION_PATCH).start() # Run tests that validate patch behaviour before and after patching self._run_patch_behaviour_tests() @@ -73,7 +71,7 @@ def test_instrumentation_patch(self): def _run_patch_behaviour_tests(self): # Test setup - self.method_patches[GET_DISTRIBUTION_PATCH].return_value = "CorrectDistributionObject" + self.method_patches[IMPORTLIB_METADATA_VERSION_PATCH].return_value = "1.0.0" # Test setup to not patch gevent os.environ[AWS_GEVENT_PATCH_MODULES] = "none" @@ -120,6 +118,8 @@ def _run_patch_mechanism_tests(self): """ self._test_botocore_installed_flag() self._reset_mocks() + self._test_resource_detector_patches() + self._reset_mocks() def _test_unpatched_botocore_instrumentation(self): # Kinesis @@ -147,7 +147,7 @@ def _test_unpatched_botocore_instrumentation(self): ) # BedrockRuntime - self.assertFalse("bedrock-runtime" in _KNOWN_EXTENSIONS, "Upstream has added a bedrock-runtime extension") + self.assertTrue("bedrock-runtime" in _KNOWN_EXTENSIONS, "Upstream has added a bedrock-runtime extension") # SecretsManager self.assertFalse("secretsmanager" in _KNOWN_EXTENSIONS, "Upstream has added a SecretsManager extension") @@ -213,95 +213,9 @@ def _test_patched_botocore_instrumentation(self): bedrock_agent_runtime_sucess_attributes: Dict[str, str] = _do_on_success_bedrock("bedrock-agent-runtime") self.assertEqual(len(bedrock_agent_runtime_sucess_attributes), 0) - # BedrockRuntime - Amazon Titan + # BedrockRuntime self.assertTrue("bedrock-runtime" in _KNOWN_EXTENSIONS) - self._test_patched_bedrock_runtime_invoke_model( - model_id="amazon.titan-embed-text-v1", - max_tokens=512, - temperature=0.9, - top_p=0.75, - finish_reason="FINISH", - input_tokens=123, - output_tokens=456, - ) - - self._test_patched_bedrock_runtime_invoke_model( - model_id="amazon.nova-pro-v1:0", - max_tokens=500, - temperature=0.9, - top_p=0.7, - finish_reason="FINISH", - input_tokens=123, - output_tokens=456, - ) - - # BedrockRuntime - Anthropic Claude - self._test_patched_bedrock_runtime_invoke_model( - model_id="anthropic.claude-v2:1", - max_tokens=512, - temperature=0.5, - top_p=0.999, - finish_reason="end_turn", - input_tokens=23, - output_tokens=36, - ) - - # BedrockRuntime - Meta LLama - self._test_patched_bedrock_runtime_invoke_model( - model_id="meta.llama2-13b-chat-v1", - max_tokens=512, - temperature=0.5, - top_p=0.9, - finish_reason="stop", - input_tokens=31, - output_tokens=36, - ) - - # BedrockRuntime - Cohere Command-r - cohere_input = "Hello, world" - cohere_output = "Goodbye, world" - - self._test_patched_bedrock_runtime_invoke_model( - model_id="cohere.command-r-v1:0", - max_tokens=512, - temperature=0.5, - top_p=0.75, - finish_reason="COMPLETE", - input_tokens=math.ceil(len(cohere_input) / 6), - output_tokens=math.ceil(len(cohere_output) / 6), - input_prompt=cohere_input, - output_prompt=cohere_output, - ) - - # BedrockRuntime - AI21 Jambda - self._test_patched_bedrock_runtime_invoke_model( - model_id="ai21.jamba-1-5-large-v1:0", - max_tokens=512, - temperature=0.5, - top_p=0.999, - finish_reason="end_turn", - input_tokens=23, - output_tokens=36, - ) - - # BedrockRuntime - Mistral - msg = "Hello World" - mistral_input = f"[INST] {msg} [/INST]" - mistral_output = "Goodbye, World" - - self._test_patched_bedrock_runtime_invoke_model( - model_id="mistral.mistral-7b-instruct-v0:2", - max_tokens=512, - temperature=0.5, - top_p=0.9, - finish_reason="stop", - input_tokens=math.ceil(len(mistral_input) / 6), - output_tokens=math.ceil(len(mistral_output) / 6), - input_prompt=mistral_input, - output_prompt=mistral_output, - ) - # SecretsManager self.assertTrue("secretsmanager" in _KNOWN_EXTENSIONS) secretsmanager_attributes: Dict[str, str] = _do_extract_secretsmanager_attributes() @@ -369,17 +283,13 @@ def _test_botocore_installed_flag(self): with patch( "amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches" ) as mock_apply_patches: - get_distribution_patch: patch = self.method_patches[GET_DISTRIBUTION_PATCH] - get_distribution_patch.side_effect = pkg_resources.DistributionNotFound - apply_instrumentation_patches() - mock_apply_patches.assert_not_called() - - get_distribution_patch.side_effect = pkg_resources.VersionConflict("botocore==1.0.0", "botocore==0.0.1") + get_distribution_patch: patch = self.method_patches[IMPORTLIB_METADATA_VERSION_PATCH] + get_distribution_patch.side_effect = PackageNotFoundError apply_instrumentation_patches() mock_apply_patches.assert_not_called() get_distribution_patch.side_effect = None - get_distribution_patch.return_value = "CorrectDistributionObject" + get_distribution_patch.return_value = "1.0.0" apply_instrumentation_patches() mock_apply_patches.assert_called() @@ -389,146 +299,6 @@ def _test_patched_bedrock_instrumentation(self): self.assertEqual(len(bedrock_sucess_attributes), 1) self.assertEqual(bedrock_sucess_attributes["aws.bedrock.guardrail.id"], _BEDROCK_GUARDRAIL_ID) - def _test_patched_bedrock_runtime_invoke_model(self, **args): - model_id = args.get("model_id", None) - max_tokens = args.get("max_tokens", None) - temperature = args.get("temperature", None) - top_p = args.get("top_p", None) - finish_reason = args.get("finish_reason", None) - input_tokens = args.get("input_tokens", None) - output_tokens = args.get("output_tokens", None) - input_prompt = args.get("input_prompt", None) - output_prompt = args.get("output_prompt", None) - - def get_model_response_request(): - request_body = {} - response_body = {} - - if "amazon.titan" in model_id: - request_body = { - "textGenerationConfig": { - "maxTokenCount": max_tokens, - "temperature": temperature, - "topP": top_p, - } - } - - response_body = { - "inputTextTokenCount": input_tokens, - "results": [ - { - "tokenCount": output_tokens, - "outputText": "testing", - "completionReason": finish_reason, - } - ], - } - - if "amazon.nova" in model_id: - request_body = { - "inferenceConfig": { - "max_new_tokens": max_tokens, - "temperature": temperature, - "top_p": top_p, - } - } - - response_body = { - "output": {"message": {"content": [{"text": ""}], "role": "assistant"}}, - "stopReason": finish_reason, - "usage": {"inputTokens": input_tokens, "outputTokens": output_tokens}, - } - - if "anthropic.claude" in model_id: - request_body = { - "anthropic_version": "bedrock-2023-05-31", - "max_tokens": max_tokens, - "temperature": temperature, - "top_p": top_p, - } - - response_body = { - "stop_reason": finish_reason, - "stop_sequence": None, - "usage": {"input_tokens": input_tokens, "output_tokens": output_tokens}, - } - - if "ai21.jamba" in model_id: - request_body = { - "max_tokens": max_tokens, - "temperature": temperature, - "top_p": top_p, - } - - response_body = { - "choices": [{"finish_reason": finish_reason}], - "usage": { - "prompt_tokens": input_tokens, - "completion_tokens": output_tokens, - "total_tokens": (input_tokens + output_tokens), - }, - } - - if "meta.llama" in model_id: - request_body = { - "max_gen_len": max_tokens, - "temperature": temperature, - "top_p": top_p, - } - - response_body = { - "prompt_token_count": input_tokens, - "generation_token_count": output_tokens, - "stop_reason": finish_reason, - } - - if "cohere.command" in model_id: - request_body = { - "message": input_prompt, - "max_tokens": max_tokens, - "temperature": temperature, - "p": top_p, - } - - response_body = { - "text": output_prompt, - "finish_reason": finish_reason, - } - - if "mistral" in model_id: - request_body = { - "prompt": input_prompt, - "max_tokens": max_tokens, - "temperature": temperature, - "top_p": top_p, - } - - response_body = {"outputs": [{"text": output_prompt, "stop_reason": finish_reason}]} - - json_bytes = json.dumps(response_body).encode("utf-8") - - return json.dumps(request_body), StreamingBody(BytesIO(json_bytes), len(json_bytes)) - - request_body, response_body = get_model_response_request() - - bedrock_runtime_attributes: Dict[str, str] = _do_extract_attributes_bedrock( - "bedrock-runtime", model_id=model_id, request_body=request_body - ) - bedrock_runtime_success_attributes: Dict[str, str] = _do_on_success_bedrock( - "bedrock-runtime", model_id=model_id, streaming_body=response_body - ) - - bedrock_runtime_attributes.update(bedrock_runtime_success_attributes) - - self.assertEqual(bedrock_runtime_attributes["gen_ai.system"], _GEN_AI_SYSTEM) - self.assertEqual(bedrock_runtime_attributes["gen_ai.request.model"], model_id) - self.assertEqual(bedrock_runtime_attributes["gen_ai.request.max_tokens"], max_tokens) - self.assertEqual(bedrock_runtime_attributes["gen_ai.request.temperature"], temperature) - self.assertEqual(bedrock_runtime_attributes["gen_ai.request.top_p"], top_p) - self.assertEqual(bedrock_runtime_attributes["gen_ai.usage.input_tokens"], input_tokens) - self.assertEqual(bedrock_runtime_attributes["gen_ai.usage.output_tokens"], output_tokens) - self.assertEqual(bedrock_runtime_attributes["gen_ai.response.finish_reasons"], [finish_reason]) - def _test_patched_bedrock_agent_instrumentation(self): """For bedrock-agent service, both extract_attributes and on_success provides attributes, the attributes depend on the API being invoked.""" @@ -586,6 +356,53 @@ def _test_patched_bedrock_agent_instrumentation(self): self.assertEqual(len(bedrock_agent_success_attributes), 1) self.assertEqual(bedrock_agent_success_attributes[attribute_tuple[0]], attribute_tuple[1]) + def _test_resource_detector_patches(self): + """Test that resource detector patches are applied and work correctly""" + # Test that the functions were patched + self.assertIsNotNone(ec2_resource._aws_http_request) + self.assertIsNotNone(eks_resource._aws_http_request) + + # Test EC2 patched function + with patch("amazon.opentelemetry.distro.patches._resource_detector_patches.urlopen") as mock_urlopen: + mock_response = MagicMock() + mock_response.read.return_value = b'{"test": "ec2-data"}' + mock_urlopen.return_value.__enter__.return_value = mock_response + + result = ec2_resource._aws_http_request("GET", "/test/path", {"X-Test": "header"}) + self.assertEqual(result, '{"test": "ec2-data"}') + + # Verify the request was made correctly + args, kwargs = mock_urlopen.call_args + request = args[0] + self.assertEqual(request.full_url, "http://169.254.169.254/test/path") + self.assertEqual(request.headers, {"X-test": "header"}) + self.assertEqual(kwargs["timeout"], 5) + + # Test EKS patched function + with patch("amazon.opentelemetry.distro.patches._resource_detector_patches.urlopen") as mock_urlopen, patch( + "amazon.opentelemetry.distro.patches._resource_detector_patches.ssl.create_default_context" + ) as mock_ssl: + mock_response = MagicMock() + mock_response.read.return_value = b'{"test": "eks-data"}' + mock_urlopen.return_value.__enter__.return_value = mock_response + + mock_context = MagicMock() + mock_ssl.return_value = mock_context + + result = eks_resource._aws_http_request("GET", "/api/v1/test", "Bearer token123") + self.assertEqual(result, '{"test": "eks-data"}') + + # Verify the request was made correctly + args, kwargs = mock_urlopen.call_args + request = args[0] + self.assertEqual(request.full_url, "https://kubernetes.default.svc/api/v1/test") + self.assertEqual(request.headers, {"Authorization": "Bearer token123"}) + self.assertEqual(kwargs["timeout"], 5) + self.assertEqual(kwargs["context"], mock_context) + + # Verify SSL context was created with correct CA file + mock_ssl.assert_called_once_with(cafile="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") + def _reset_mocks(self): for method_patch in self.method_patches.values(): method_patch.reset_mock() @@ -678,6 +495,7 @@ def _do_on_success( ) -> Dict[str, str]: span_mock: Span = MagicMock() mock_call_context = MagicMock() + mock_instrumentor_context = MagicMock() span_attributes: Dict[str, str] = {} def set_side_effect(set_key, set_value): @@ -692,6 +510,6 @@ def set_side_effect(set_key, set_value): mock_call_context.params = params extension = _KNOWN_EXTENSIONS[service_name]()(mock_call_context) - extension.on_success(span_mock, result) + extension.on_success(span_mock, result, mock_instrumentor_context) return span_attributes diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py new file mode 100644 index 000000000..0839aec98 --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py @@ -0,0 +1,96 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os +from importlib.metadata import PackageNotFoundError +from unittest import TestCase +from unittest.mock import patch + +from amazon.opentelemetry.distro._utils import AGENT_OBSERVABILITY_ENABLED, is_agent_observability_enabled, is_installed + + +class TestUtils(TestCase): + def setUp(self): + # Store original env var if it exists + self.original_env = os.environ.get(AGENT_OBSERVABILITY_ENABLED) + + def tearDown(self): + # Restore original env var + if self.original_env is not None: + os.environ[AGENT_OBSERVABILITY_ENABLED] = self.original_env + elif AGENT_OBSERVABILITY_ENABLED in os.environ: + del os.environ[AGENT_OBSERVABILITY_ENABLED] + + def test_is_installed_package_not_found(self): + """Test is_installed returns False when package is not found""" + with patch("amazon.opentelemetry.distro._utils.version") as mock_version: + # Simulate package not found + mock_version.side_effect = PackageNotFoundError("test-package") + + result = is_installed("test-package>=1.0.0") + self.assertFalse(result) + + def test_is_installed(self): + """Test is_installed returns True when version matches the specifier""" + with patch("amazon.opentelemetry.distro._utils.version") as mock_version: + # Package is installed and version matches requirement + mock_version.return_value = "2.5.0" + + # Test with compatible version requirement + result = is_installed("test-package>=2.0.0") + self.assertTrue(result) + + # Test with exact version match + mock_version.return_value = "1.0.0" + result = is_installed("test-package==1.0.0") + self.assertTrue(result) + + # Test with version range + mock_version.return_value = "1.5.0" + result = is_installed("test-package>=1.0,<2.0") + self.assertTrue(result) + + def test_is_installed_version_mismatch(self): + """Test is_installed returns False when version doesn't match""" + with patch("amazon.opentelemetry.distro._utils.version") as mock_version: + # Package is installed but version doesn't match requirement + mock_version.return_value = "1.0.0" + + # Test with incompatible version requirement + result = is_installed("test-package>=2.0.0") + self.assertFalse(result) + + def test_is_agent_observability_enabled_various_values(self): + """Test is_agent_observability_enabled with various environment variable values""" + # Test with "True" (uppercase) + os.environ[AGENT_OBSERVABILITY_ENABLED] = "True" + self.assertTrue(is_agent_observability_enabled()) + + # Test with "TRUE" (all caps) + os.environ[AGENT_OBSERVABILITY_ENABLED] = "TRUE" + self.assertTrue(is_agent_observability_enabled()) + + # Test with "true" (lowercase) + os.environ[AGENT_OBSERVABILITY_ENABLED] = "true" + self.assertTrue(is_agent_observability_enabled()) + + # Test with "false" + os.environ[AGENT_OBSERVABILITY_ENABLED] = "false" + self.assertFalse(is_agent_observability_enabled()) + + # Test with "False" + os.environ[AGENT_OBSERVABILITY_ENABLED] = "False" + self.assertFalse(is_agent_observability_enabled()) + + # Test with arbitrary string + os.environ[AGENT_OBSERVABILITY_ENABLED] = "yes" + self.assertFalse(is_agent_observability_enabled()) + + # Test with empty string + os.environ[AGENT_OBSERVABILITY_ENABLED] = "" + self.assertFalse(is_agent_observability_enabled()) + + # Test when env var is not set + if AGENT_OBSERVABILITY_ENABLED in os.environ: + del os.environ[AGENT_OBSERVABILITY_ENABLED] + self.assertFalse(is_agent_observability_enabled()) diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index 6c315a4dc..80ecbc6fe 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -435,7 +435,7 @@ def get_model_request_response(path): "inferenceConfig": { "max_new_tokens": 800, "temperature": 0.9, - "top_p": 0.7, + "topP": 0.7, }, } @@ -496,32 +496,6 @@ def get_model_request_response(path): "text": "test-generation-text", } - if "ai21.jamba" in path: - model_id = "ai21.jamba-1-5-large-v1:0" - - request_body = { - "messages": [ - { - "role": "user", - "content": prompt, - }, - ], - "top_p": 0.8, - "temperature": 0.6, - "max_tokens": 512, - } - - response_body = { - "stop_reason": "end_turn", - "usage": { - "prompt_tokens": 21, - "completion_tokens": 24, - }, - "choices": [ - {"finish_reason": "stop"}, - ], - } - if "mistral" in path: model_id = "mistral.mistral-7b-instruct-v0:2" diff --git a/contract-tests/images/applications/botocore/requirements.txt b/contract-tests/images/applications/botocore/requirements.txt index 25113e3f4..61ddebf98 100644 --- a/contract-tests/images/applications/botocore/requirements.txt +++ b/contract-tests/images/applications/botocore/requirements.txt @@ -1,5 +1,3 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 typing-extensions==4.12.2 botocore==1.34.143 boto3==1.34.143 diff --git a/contract-tests/images/applications/django/requirements.txt b/contract-tests/images/applications/django/requirements.txt index 9b54a7736..84dfdeabb 100644 --- a/contract-tests/images/applications/django/requirements.txt +++ b/contract-tests/images/applications/django/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 typing-extensions==4.12.2 django==5.0.11 diff --git a/contract-tests/images/applications/mysql-connector/requirements.txt b/contract-tests/images/applications/mysql-connector/requirements.txt index 9ca44d2e4..f285dcb1f 100644 --- a/contract-tests/images/applications/mysql-connector/requirements.txt +++ b/contract-tests/images/applications/mysql-connector/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 typing-extensions==4.12.2 mysql-connector-python~=9.1.0 diff --git a/contract-tests/images/applications/mysqlclient/requirements.txt b/contract-tests/images/applications/mysqlclient/requirements.txt index 49c6b70f3..933e606b4 100644 --- a/contract-tests/images/applications/mysqlclient/requirements.txt +++ b/contract-tests/images/applications/mysqlclient/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 typing-extensions==4.12.2 mysqlclient==2.2.4 diff --git a/contract-tests/images/applications/psycopg2/requirements.txt b/contract-tests/images/applications/psycopg2/requirements.txt index f2d278475..8786aff35 100644 --- a/contract-tests/images/applications/psycopg2/requirements.txt +++ b/contract-tests/images/applications/psycopg2/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 typing-extensions==4.12.2 psycopg2==2.9.9 diff --git a/contract-tests/images/applications/pymysql/requirements.txt b/contract-tests/images/applications/pymysql/requirements.txt index ddda9b1fe..8ba76defb 100644 --- a/contract-tests/images/applications/pymysql/requirements.txt +++ b/contract-tests/images/applications/pymysql/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 typing-extensions==4.12.2 pymysql==1.1.1 diff --git a/contract-tests/images/applications/requests/requirements.txt b/contract-tests/images/applications/requests/requirements.txt index 369049d22..700b31404 100644 --- a/contract-tests/images/applications/requests/requirements.txt +++ b/contract-tests/images/applications/requests/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 typing-extensions==4.12.2 requests~=2.0 diff --git a/contract-tests/images/mock-collector/pyproject.toml b/contract-tests/images/mock-collector/pyproject.toml index 422e2a5b1..42e13c868 100644 --- a/contract-tests/images/mock-collector/pyproject.toml +++ b/contract-tests/images/mock-collector/pyproject.toml @@ -11,9 +11,9 @@ requires-python = ">=3.9" dependencies = [ "grpcio ~= 1.66.0", - "opentelemetry-proto==1.25.0", - "opentelemetry-sdk==1.25.0", - "protobuf==4.25.2", + "opentelemetry-proto==1.33.1", + "opentelemetry-sdk==1.33.1", + "protobuf==5.26.1", "typing-extensions==4.12.2" ] diff --git a/contract-tests/images/mock-collector/requirements.txt b/contract-tests/images/mock-collector/requirements.txt index a0c5454cd..12e69148b 100644 --- a/contract-tests/images/mock-collector/requirements.txt +++ b/contract-tests/images/mock-collector/requirements.txt @@ -1,5 +1,5 @@ grpcio==1.66.2 -opentelemetry-proto==1.25.0 -opentelemetry-sdk==1.25.0 -protobuf==4.25.2 +opentelemetry-proto==1.33.1 +opentelemetry-sdk==1.33.1 +protobuf==5.26.1 typing-extensions==4.12.2 diff --git a/contract-tests/tests/pyproject.toml b/contract-tests/tests/pyproject.toml index 0df6f6a1c..5c2895fab 100644 --- a/contract-tests/tests/pyproject.toml +++ b/contract-tests/tests/pyproject.toml @@ -10,8 +10,8 @@ license = "Apache-2.0" requires-python = ">=3.9" dependencies = [ - "opentelemetry-proto==1.25.0", - "opentelemetry-sdk==1.25.0", + "opentelemetry-proto==1.33.1", + "opentelemetry-sdk==1.33.1", "testcontainers==3.7.1", "grpcio==1.66.2", "docker==7.1.0", diff --git a/contract-tests/tests/test/amazon/botocore/botocore_test.py b/contract-tests/tests/test/amazon/botocore/botocore_test.py index ed04c9514..549ec3f50 100644 --- a/contract-tests/tests/test/amazon/botocore/botocore_test.py +++ b/contract-tests/tests/test/amazon/botocore/botocore_test.py @@ -440,7 +440,7 @@ def test_bedrock_runtime_invoke_model_amazon_titan(self): _GEN_AI_USAGE_INPUT_TOKENS: 15, _GEN_AI_USAGE_OUTPUT_TOKENS: 13, }, - span_name="Bedrock Runtime.InvokeModel", + span_name="text_completion amazon.titan-text-premier-v1:0", ) def test_bedrock_runtime_invoke_model_amazon_nova(self): @@ -458,6 +458,7 @@ def test_bedrock_runtime_invoke_model_amazon_nova(self): cloudformation_primary_identifier="amazon.nova-pro-v1:0", request_specific_attributes={ _GEN_AI_REQUEST_MODEL: "amazon.nova-pro-v1:0", + _GEN_AI_SYSTEM: "aws.bedrock", _GEN_AI_REQUEST_MAX_TOKENS: 800, _GEN_AI_REQUEST_TEMPERATURE: 0.9, _GEN_AI_REQUEST_TOP_P: 0.7, @@ -467,7 +468,7 @@ def test_bedrock_runtime_invoke_model_amazon_nova(self): _GEN_AI_USAGE_INPUT_TOKENS: 432, _GEN_AI_USAGE_OUTPUT_TOKENS: 681, }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat amazon.nova-pro-v1:0", ) def test_bedrock_runtime_invoke_model_anthropic_claude(self): @@ -495,7 +496,7 @@ def test_bedrock_runtime_invoke_model_anthropic_claude(self): _GEN_AI_USAGE_INPUT_TOKENS: 15, _GEN_AI_USAGE_OUTPUT_TOKENS: 13, }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat anthropic.claude-v2:1", ) def test_bedrock_runtime_invoke_model_meta_llama(self): @@ -523,7 +524,7 @@ def test_bedrock_runtime_invoke_model_meta_llama(self): _GEN_AI_USAGE_INPUT_TOKENS: 31, _GEN_AI_USAGE_OUTPUT_TOKENS: 49, }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat meta.llama2-13b-chat-v1", ) def test_bedrock_runtime_invoke_model_cohere_command(self): @@ -553,35 +554,7 @@ def test_bedrock_runtime_invoke_model_cohere_command(self): ), _GEN_AI_USAGE_OUTPUT_TOKENS: math.ceil(len("test-generation-text") / 6), }, - span_name="Bedrock Runtime.InvokeModel", - ) - - def test_bedrock_runtime_invoke_model_ai21_jamba(self): - self.do_test_requests( - "bedrock/invokemodel/invoke-model/ai21.jamba-1-5-large-v1:0", - "GET", - 200, - 0, - 0, - rpc_service="Bedrock Runtime", - remote_service="AWS::BedrockRuntime", - remote_operation="InvokeModel", - remote_resource_type="AWS::Bedrock::Model", - remote_resource_identifier="ai21.jamba-1-5-large-v1:0", - cloudformation_primary_identifier="ai21.jamba-1-5-large-v1:0", - request_specific_attributes={ - _GEN_AI_REQUEST_MODEL: "ai21.jamba-1-5-large-v1:0", - _GEN_AI_SYSTEM: "aws.bedrock", - _GEN_AI_REQUEST_MAX_TOKENS: 512, - _GEN_AI_REQUEST_TEMPERATURE: 0.6, - _GEN_AI_REQUEST_TOP_P: 0.8, - }, - response_specific_attributes={ - _GEN_AI_RESPONSE_FINISH_REASONS: ["stop"], - _GEN_AI_USAGE_INPUT_TOKENS: 21, - _GEN_AI_USAGE_OUTPUT_TOKENS: 24, - }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat cohere.command-r-v1:0", ) def test_bedrock_runtime_invoke_model_mistral(self): @@ -611,7 +584,7 @@ def test_bedrock_runtime_invoke_model_mistral(self): ), _GEN_AI_USAGE_OUTPUT_TOKENS: math.ceil(len("test-output-text") / 6), }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat mistral.mistral-7b-instruct-v0:2", ) def test_bedrock_get_guardrail(self):